/ src / library / av_processing / VideoPicture.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 *
 * This class holds one decoded frame. It keeps a brightness plane and two
 * color planes, and turns them into a picture a browser can show.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * VideoPicture holds one decoded frame: a brightness plane and two color
 * planes, with what it takes to turn them into a picture.
 */
class VideoPicture
{
    /**
     * __construct holds one decoded picture: a plane of brightness and two of
     * color, with the sizes and offsets needed to read the part that is shown.
     *
     * @param array $luma brightness samples
     * @param array $blue blue color difference samples
     * @param array $red red color difference samples
     * @param int $shown_width how wide, in pixels
     * @param int $shown_height how tall, in pixels
     * @param int $stride_y how many values one row of brightness takes
     * @param int $stride_c how many values one row of color takes
     * @param int $off_x how far across the picture the frame proper starts
     * @param int $off_y how far down the picture the frame proper starts
     * @param int $sub_x how much the color planes are shrunk across
     * @param int $sub_y how much the color planes are shrunk down
     */
    public function __construct(
        /**
         * $luma stores the brightness of every pixel, one value each, row after
         * row. This is the plane a black and white thumbnail could be drawn
         * from on its own.
         * @var array
         */
        public array $luma,
        /**
         * $blue stores how blue each pixel is, away from gray. There is one
         * value for every few pixels rather than for each, which is what the
         * shrink below describes.
         * @var array
         */
        public array $blue,
        /**
         * $red stores how red each pixel is, away from gray. There is
         * one value for every few pixels rather than for each, since a
         * picture's color is stored more coarsely than its brightness.
         * @var array
         */
        public array $red,
        /**
         * $shown_width stores how wide the picture is meant to be shown, which
         * can be less than the planes hold, since a decoder works in whole
         * blocks.
         * @var int
         */
        public int $shown_width,
        /**
         * $shown_height stores how tall the picture is meant to be
         * shown, which can be less than the planes hold, since a
         * decoder works in whole blocks.
         * @var int
         */
        public int $shown_height,
        /**
         * $stride_y stores how many values one row of brightness takes,
         * counting any the decoder added to reach a whole block. Reading pixel
         * by pixel steps by this rather than by the width.
         * @var int
         */
        public int $stride_y,
        /**
         * $stride_c stores how many values one row of a color plane takes,
     * counting any the decoder added to reach a whole block. Same
         * reason.
         * @var int
         */
        public int $stride_c,
        /**
         * $off_x stores how far across the planes the picture proper starts. A
         * decoder may write a border it does not mean to show.
         * @var int
         */
        public int $off_x = 0,
        /**
         * $off_y stores how far down the planes the picture proper starts.
     * A decoder may write a border it does not mean to show.
         * @var int
         */
        public int $off_y = 0,
        /**
         * $sub_x stores how many pixels across share one color value. One means
         * every pixel has its own; two is what most video uses.
         * @var int
         */
        public int $sub_x = 1,
        /**
         * $sub_y stores how many pixels down share one color value.
         * @var int
         */
        public int $sub_y = 1
    ) {
    }
    /**
     * toImage bT.601 limited-range YUV 4:2:0 to red, green and blue. Streams
     * that signal a different matrix or full range will come out slightly off;
     * video color metadata is not parsed.
     *
     * @return GdImage what was read
     */
    public function toImage()
    {
        /*
            The samples are turned into a bitmap in memory and handed to the
            image library in one call. Setting each sample separately costs
            a function call per sample, which for a large picture is most of
            the work. A bitmap holds its rows bottom upward and its samples
            blue first, so they are built in that order.
        */
        $wide = $this->shown_width;
        $tall = $this->shown_height;
        $padding = (4 - (($wide * 3) % 4)) % 4;
        $tail = str_repeat("\x00", $padding);
        $pixels = "";
        for ($line = $tall - 1; $line >= 0; $line--) {
            $source = $line + $this->off_y;
            $luma_row = $source * $this->stride_y;
            $chroma_row = ($source >> $this->sub_y) * $this->stride_c;
            $bytes = [];
            for ($col = 0; $col < $wide; $col++) {
                $across = $col + $this->off_x;
                $chroma_col = $chroma_row + ($across >> $this->sub_x);
                $luma = $this->luma[$luma_row + $across] - 16;
                $blue = $this->blue[$chroma_col] - 128;
                $red = $this->red[$chroma_col] - 128;
                $scaled = 298 * $luma;
                $value = ($scaled + 516 * $blue + 128) >> 8;
                $bytes[] = $value < 0 ? 0 : ($value > 255 ? 255 : $value);
                $value = ($scaled - 100 * $blue - 208 * $red + 128) >> 8;
                $bytes[] = $value < 0 ? 0 : ($value > 255 ? 255 : $value);
                $value = ($scaled + 409 * $red + 128) >> 8;
                $bytes[] = $value < 0 ? 0 : ($value > 255 ? 255 : $value);
            }
            $pixels .= pack("C*", ...$bytes) . $tail;
        }
        $header = "BM" . pack("VvvV", 54 + strlen($pixels), 0, 0, 54)
            . pack("VllvvVVllVV", 40, $wide, $tall, 1, 24, 0,
            strlen($pixels), 2835, 2835, 0, 0);
        $image = imagecreatefromstring($header . $pixels);
        if ($image === false) {
            throw new VideoException("could not build the picture");
        }
        return $image;
    }
}
X