/ src / library / processors / JpgProcessor.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
 */
namespace seekquarry\yioop\library\processors;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\ComputerVision;
use seekquarry\yioop\library\UrlParser;

ini_set("gd.jpeg_ignore_warning", 1);
/**
 * JpgProcessor used to create crawl summary information for JPEG files
 * @author Chris Pollett
 */
class JpgProcessor extends ImageProcessor
{
    /**
     * __construct set-ups the any indexing plugins associated with this page
     * processor
     * @param array $plugins an array of indexing plugins which might do further
     *     processing on the data handles by this page processor
     * @param int $max_description_len maximal length of a page summary
     * @param string $summarizer_option CRAWL_CONSTANT specifying what kind of
     *     summarizer to use self::BASIC_SUMMARIZER,
     *     self::GRAPH_BASED_SUMMARIZER and self::CENTROID_SUMMARIZER
     *     self::CENTROID_SUMMARIZER
     */
    public function __construct($plugins = [], $max_description_len = null,
        $summarizer_option = self::BASIC_SUMMARIZER)
    {
        parent::__construct($plugins, $max_description_len, $summarizer_option);
        /* The kinds of file this reader handles are named here so
           that a crawler knows to hand it a picture. */
        self::$indexed_file_types[] = "jpg";
        self::$indexed_file_types[] = "jpeg";
        self::$image_types[] = "jpg";
        self::$image_types[] = "jpeg";
        self::$mime_processor["image/jpeg"] = "JpgProcessor";
    }
    /**
     * process reads one picture that a crawl fetched and hands back
     * what an index needs from it: a small copy to show beside a
     * result, and the address it came from as its description.
     * @param string $page  the image represented as a character string
     * @param string $url  the url where the image was downloaded from
     * @return array summary information including a thumbnail and a description
     *     (where the description is just the url)
     */
    public function process($page, $url)
    {
        if (is_string($page)) {
            set_error_handler(null);
            $image = @imagecreatefromstring($page);
            restore_error_handler();
            $summary = [];
            $this->addWidthHeightSummary($summary, $page);
            if (!empty($image)) {
                $summary[self::AVERAGE_COLOR] = $this->averageColor($image);
                $summary[self::IS_BLACK_AND_WHITE] =
                    $this->isBlackAndWhite($image);
            }
            $summary[self::TITLE] = "";
            $summary[self::LANG] = "mul";
            $file_name = UrlParser::getWordsLastPathPartUrl($url);
            $summary[self::DESCRIPTION] = $file_name . "\n";
            if (ComputerVision::ocrEnabled()) {
                set_error_handler(null);
                $temp_file = $this->saveTempFile($page, $url, "jpg");
                $lang = UrlParser::getLang($url);
                $ocr_data = ComputerVision::recognizeText($temp_file, [$lang]);
                if (!empty($ocr_data)) {
                    $summary[self::DESCRIPTION] .= $ocr_data;
                }
                @unlink($temp_file);
                restore_error_handler();
            }
            if (function_exists("exif_read_data")) {
                set_error_handler(null);
                $temp_file = $this->saveTempFile($page, $url, "jpg");
                $summary[self::DESCRIPTION] .= "\nEXIF DATA\n" .
                    print_r(@exif_read_data($temp_file), true);
                @unlink($temp_file);
                restore_error_handler();
            } else {
                $summary[self::DESCRIPTION] = $file_name;
            }
            $xmp_data = $this->getXmpData($page);
            if ($xmp_data) {
                $summary[self::DESCRIPTION] .= "\nXMP Data\n".
                    $xmp_data;
            }
            $summary[self::LINKS] = [];
            $summary[self::PAGE] =
                "<html><body><div><img src='data:image/jpeg;base64," .
                base64_encode($page)."' alt='". $file_name .
                "' ></div></body></html>";
            if ($image) {
                $summary[self::WIDTH] = imagesx($image);
                $summary[self::HEIGHT] = imagesy($image);
                $golden_ratio = 1.618; //approx
                $ratio = min($golden_ratio,
                    $summary[self::WIDTH]/$summary[self::HEIGHT]);
                $thumb_string = self::createThumb($image,
                    intval($ratio * C\THUMB_DIM),  C\THUMB_DIM);
            }
            if (!empty($thumb_string)) {
                $summary[self::THUMB] = 'data:image/webp;base64,'.
                    base64_encode($thumb_string);
            }
        }
        return $summary;
    }
    /**
     * BLOCK_SIDE how many values a side of one of the blocks a jpeg is built
     * from holds. Everything about a jpeg is counted in these blocks.
     */
    const BLOCK_SIDE = 8;
    /**
     * BLOCK_VALUES how many values a whole block holds.
     */
    const BLOCK_VALUES = 64;
    /**
     * LONGEST_RUN the longest a run of bits standing for one value may be.
     */
    const LONGEST_RUN = 16;
    /**
     * MIDDLE_SHADE what is added back to every value at the end. A jpeg keeps
     * its values either side of zero and this puts them back into the range a
     * color is said in.
     */
    const MIDDLE_SHADE = 128;
    /**
     * MOST_SHADE the largest a color may be.
     */
    const MOST_SHADE = 255;
    /**
     * MOST_POINTS_READ_HERE is how many points a picture may hold
     * before its four inks are left to the drawing library rather than
     * read here. Reading them here takes about a second for every two
     * million points, and a thumbnail is not worth minutes.
     * @var int
     */
    const MOST_POINTS_READ_HERE = 4000000;
    /**
     * HALF_SHADE is the middle value of a plane, which is what a plane
     * of color holds where there is no color at all.
     * @var int
     */
    const HALF_SHADE = 128;
    /**
     * BRIGHTNESS_AND_COLOR is the number the writing program's note
     * carries where the first three planes hold one of brightness and
     * two of color rather than the first three inks.
     * @var int
     */
    const BRIGHTNESS_AND_COLOR = 2;
    /**
     * RED_FROM_REDNESS is how much of the redness plane goes into red.
     * The three numbers below are the ones the standard for storing
     * photographs fixes for turning brightness and color back into
     * light.
     * @var float
     */
    const RED_FROM_REDNESS = 1.402;
    /**
     * GREEN_FROM_BLUENESS is how much of the blueness plane is taken
     * out of green.
     * @var float
     */
    const GREEN_FROM_BLUENESS = 0.344136;
    /**
     * GREEN_FROM_REDNESS is how much of the redness plane is taken out
     * of green.
     * @var float
     */
    const GREEN_FROM_REDNESS = 0.714136;
    /**
     * BLUE_FROM_BLUENESS is how much of the blueness plane goes into
     * blue.
     * @var float
     */
    const BLUE_FROM_BLUENESS = 1.772;
    /**
     * zigzag stores how the eight by eight blocks a jpeg is built from are read
     * out: not row by row but along diagonals, which is how they were written.
     * @var array
     */
    public static $zigzag = [
        0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5,
        12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28,
        35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63];
    /**
     * toImage turns a jpeg into an image, whatever colors it was written in.
     * The image reader that comes with PHP understands the three colors a
     * screen uses and nothing else, so a jpeg written for print, in the four
     * colors ink uses, comes back from it as a black rectangle. Such a jpeg is
     * taken apart here instead and its colors worked out, and everything else
     * is left to the reader as before.
     * @param string $bytes the jpeg's bytes
     * @return object|bool the image, or false if it cannot be read
     */
    public static function toImage($bytes)
    {
        $about = @getimagesizefromstring($bytes);
        $points = ($about === false) ? 0 :
            ($about[0] ?? 0) * ($about[1] ?? 0);
        /* Reading the four inks is done here rather than by the drawing
           library, which drops the black one, but it is slow: a picture
           of a few million points takes seconds. Past that size the
           library reads it instead, since a pale picture is better than
           a thumbnail that never arrives. */
        if ($about !== false && ($about["channels"] ?? 3) == 4 &&
            $points > 0 && $points <= self::MOST_POINTS_READ_HERE) {
            $worked_out = self::fromInkColors($bytes);
            if ($worked_out !== false) {
                return $worked_out;
            }
        }
        /* The drawing library is handed only bytes that begin the way
           a picture it knows begins. Handing it anything else raised a
           warning that Yioop's own error reporting wrote into the page
           being built, and a page carrying a warning ahead of its body
           does not read. */
        $marks = ["\xFF\xD8\xFF", "\x89PNG", "GIF8", "BM", "II*\x00",
            "MM\x00*", "RIFF"];
        foreach ($marks as $mark) {
            if (strncmp($bytes, $mark, strlen($mark)) == 0) {
                return @imagecreatefromstring($bytes);
            }
        }
        return false;
    }
    /**
     * fromInkColors works out an image from a jpeg written in the four colors
     * ink uses.
     * @param string $bytes the jpeg's bytes
     * @return object|bool the image, or false if it cannot be read
     */
    public static function fromInkColors($bytes)
    {
        $read = self::readJpeg($bytes);
        if ($read === false || count($read["planes"]) != 4) {
            return false;
        }
        $wide = $read["width"];
        $high = $read["height"];
        /* A picture written for print may store its first three planes
           the way a photograph is stored, as one plane of brightness
           and two of color, rather than as the first three inks. The
           note the writing program leaves says which, and reading the
           one as the other turns a khaki figure green. */
        if (self::storedAsBrightnessAndColor($bytes)) {
            self::inksFromBrightnessAndColor($read["planes"]);
        }
        $image = imagecreatetruecolor($wide, $high);
        for ($down = 0; $down < $high; $down++) {
            for ($across = 0; $across < $wide; $across++) {
                $at = $down * $wide + $across;
                /* Each value says how much ink is laid down, the fourth
                   being black. Paper with no ink on it lets all the light
                   through, so what the eye sees is what is left after
                   each ink takes its share, and the black ink takes its
                   share of whatever the others left. */
                $black = 255 - $read["planes"][3][$at];
                $red = (int)((255 - $read["planes"][0][$at]) * $black / 255);
                $green = (int)((255 - $read["planes"][1][$at]) * $black /
                    255);
                $blue = (int)((255 - $read["planes"][2][$at]) * $black /
                    255);
                imagesetpixel($image, $across, $down,
                    ($red << 16) | ($green << 8) | $blue);
            }
        }
        return $image;
    }
    /**
     * storedAsBrightnessAndColor says whether a picture written in four
     * inks stores its first three planes as one of brightness and two
     * of color, which is how a photograph is stored, rather than as the
     * first three inks outright.
     *
     * The program that wrote the picture leaves a note saying which,
     * and the note carries a number: two means brightness and color.
     *
     * @param string $bytes the jpeg's bytes
     * @return bool true where the first three planes hold brightness
     *     and color
     */
    public static function storedAsBrightnessAndColor($bytes)
    {
        $at = strpos($bytes, "Adobe");
        if ($at === false || $at + 11 >= strlen($bytes)) {
            return false;
        }
        return ord($bytes[$at + 11]) == self::BRIGHTNESS_AND_COLOR;
    }
    /**
     * inksFromBrightnessAndColor turns the first three planes of a
     * picture from brightness and color into the first three inks, in
     * place. The fourth plane, which is black, is left as it is.
     *
     * @param array &$planes the planes read from the picture
     */
    public static function inksFromBrightnessAndColor(&$planes)
    {
        $count = count($planes[0]);
        for ($at = 0; $at < $count; $at++) {
            $bright = $planes[0][$at];
            $blueness = $planes[1][$at] - self::HALF_SHADE;
            $redness = $planes[2][$at] - self::HALF_SHADE;
            $red = $bright + self::RED_FROM_REDNESS * $redness;
            $green = $bright - self::GREEN_FROM_BLUENESS * $blueness -
                self::GREEN_FROM_REDNESS * $redness;
            $blue = $bright + self::BLUE_FROM_BLUENESS * $blueness;
            /* An ink is the light it takes away, so each plane becomes
               what is left of full light. */
            $planes[0][$at] = self::MOST_SHADE -
                self::heldInRange($red);
            $planes[1][$at] = self::MOST_SHADE -
                self::heldInRange($green);
            $planes[2][$at] = self::MOST_SHADE -
                self::heldInRange($blue);
        }
    }
    /**
     * heldInRange keeps one value within what a plane can hold, since
     * the arithmetic that turns brightness and color into light can
     * land a little outside it.
     *
     * @param float $value the value to hold
     * @return int the value, held between nothing and full
     */
    public static function heldInRange($value)
    {
        if ($value < 0) {
            return 0;
        }
        if ($value > self::MOST_SHADE) {
            return self::MOST_SHADE;
        }
        return (int)round($value);
    }
    /**
     * readJpeg reads a jpeg written the plain way, one pass over the whole
     * picture, and hands back one plane of values for each color it carries.
     * @param string $bytes the jpeg's bytes
     * @return array|bool the width, the height, and a plane per color, or false
     *     if it is written some way this cannot read
     */
    public static function readJpeg($bytes)
    {
        $quantities = [];
        $trees = [];
        $frame = null;
        $restart = 0;
        $at = 2;
        $length = strlen($bytes);
        while ($at < $length - 3) {
            if ($bytes[$at] !== "\xFF") {
                return false;
            }
            $marker = ord($bytes[$at + 1]);
            if ($marker == 0xD8 || $marker == 0x01 ||
                ($marker >= 0xD0 && $marker <= 0xD7)) {
                $at += 2;
                continue;
            }
            $size = (ord($bytes[$at + 2]) << 8) | ord($bytes[$at + 3]);
            $body = substr($bytes, $at + 4, $size - 2);
            if ($marker == 0xDB) {
                self::readQuantities($body, $quantities);
            } else if ($marker == 0xC4) {
                self::readTrees($body, $trees);
            } else if ($marker == 0xDD) {
                $restart = (ord($body[0]) << 8) | ord($body[1]);
            } else if ($marker == 0xC0 || $marker == 0xC1) {
                $frame = self::readFrame($body);
            } else if ($marker >= 0xC2 && $marker <= 0xCF &&
                $marker != 0xC4 && $marker != 0xC8 && $marker != 0xCC) {
                /* written a way this does not read, such as the kind that
                   arrives a little at a time */
                return false;
            } else if ($marker == 0xDA) {
                if ($frame === null) {
                    return false;
                }
                return self::readScan($bytes, $at + 2 + $size, $body,
                    $frame, $quantities, $trees, $restart);
            }
            $at += 2 + $size;
        }
        return false;
    }
    /**
     * readQuantities reads the tables saying how much detail was thrown away,
     * which is what the values have to be multiplied back up by.
     * @param string $body the marker's bytes
     * @param array &$quantities the tables read so far
     */
    public static function readQuantities($body, &$quantities)
    {
        $at = 0;
        while ($at < strlen($body)) {
            $precision = ord($body[$at]) >> 4;
            $which = ord($body[$at]) & 15;
            $at++;
            $table = [];
            for ($index = 0; $index < self::BLOCK_VALUES; $index++) {
                if ($precision == 0) {
                    $table[self::$zigzag[$index]] = ord($body[$at]);
                    $at++;
                } else {
                    $table[self::$zigzag[$index]] =
                        (ord($body[$at]) << 8) | ord($body[$at + 1]);
                    $at += 2;
                }
            }
            $quantities[$which] = $table;
        }
    }
    /**
     * readTrees reads the tables saying which runs of bits stand for which
     * values, turning each into a lookup from a run of bits to what it means.
     * @param string $body the marker's bytes
     * @param array &$trees the tables read so far
     */
    public static function readTrees($body, &$trees)
    {
        $at = 0;
        while ($at < strlen($body)) {
            $kind = ord($body[$at]) >> 4;
            $which = ord($body[$at]) & 15;
            $at++;
            $counts = [];
            $total = 0;
            for ($size = 1; $size <= self::LONGEST_RUN; $size++) {
                $counts[$size] = ord($body[$at]);
                $total += $counts[$size];
                $at++;
            }
            $lookup = [];
            $code = 0;
            for ($size = 1; $size <= self::LONGEST_RUN; $size++) {
                for ($index = 0; $index < $counts[$size]; $index++) {
                    $lookup[$size][$code] = ord($body[$at]);
                    $code++;
                    $at++;
                }
                $code <<= 1;
            }
            $trees[$kind][$which] = $lookup;
        }
    }
    /**
     * readFrame reads how large the picture is and which colors it carries.
     * @param string $body the marker's bytes
     * @return array the height, width and colors
     */
    public static function readFrame($body)
    {
        $height = (ord($body[1]) << 8) | ord($body[2]);
        $width = (ord($body[3]) << 8) | ord($body[4]);
        $count = ord($body[5]);
        $colors = [];
        for ($index = 0; $index < $count; $index++) {
            $at = 6 + $index * 3;
            $colors[] = ["id" => ord($body[$at]),
                "wide" => ord($body[$at + 1]) >> 4,
                "high" => ord($body[$at + 1]) & 15,
                "quantity" => ord($body[$at + 2])];
        }
        return ["height" => $height, "width" => $width,
            "colors" => $colors];
    }
    /**
     * readScan reads the picture itself: the long run of bits after the last
     * table, one eight by eight block at a time, until every block of every
     * color has been read.
     * @param string $bytes the whole jpeg
     * @param int $from where the picture's bits begin
     * @param string $body the scan marker's bytes
     * @param array $frame how large the picture is and its colors
     * @param array $quantities the tables of thrown-away detail
     * @param array $trees the tables of what runs of bits mean
     * @param int $restart how many blocks between restarts, zero for none
     * @return array|bool the width, height and a plane per color
     */
    public static function readScan($bytes, $from, $body, $frame,
        $quantities, $trees, $restart)
    {
        $count = ord($body[0]);
        $picked = [];
        for ($index = 0; $index < $count; $index++) {
            $id = ord($body[1 + $index * 2]);
            $tables = ord($body[2 + $index * 2]);
            foreach ($frame["colors"] as $place => $color) {
                if ($color["id"] == $id) {
                    $picked[$place] = ["values" => $tables >> 4,
                        "runs" => $tables & 15];
                }
            }
        }
        $widest = 1;
        $tallest = 1;
        foreach ($frame["colors"] as $color) {
            $widest = max($widest, $color["wide"]);
            $tallest = max($tallest, $color["high"]);
        }
        $side = self::BLOCK_SIDE;
        $across_groups = (int)ceil($frame["width"] / ($side * $widest));
        $down_groups = (int)ceil($frame["height"] / ($side * $tallest));
        $planes = [];
        $plane_widths = [];
        foreach ($frame["colors"] as $place => $color) {
            $plane_widths[$place] = $across_groups * $side *
                $color["wide"];
            $planes[$place] = array_fill(0, $plane_widths[$place] *
                $down_groups * $side * $color["high"], 0);
        }
        $reader = ["bytes" => $bytes, "at" => $from, "bits" => 0,
            "held" => 0];
        $carried = array_fill(0, count($frame["colors"]), 0);
        $done = 0;
        for ($down = 0; $down < $down_groups; $down++) {
            for ($across = 0; $across < $across_groups; $across++) {
                if ($restart > 0 && $done > 0 && $done % $restart == 0) {
                    self::skipToRestart($reader);
                    $carried = array_fill(0, count($frame["colors"]), 0);
                }
                $done++;
                foreach ($frame["colors"] as $place => $color) {
                    for ($tall = 0; $tall < $color["high"]; $tall++) {
                        for ($wide = 0; $wide < $color["wide"]; $wide++) {
                            $block = self::readBlock($reader,
                                $trees[0][$picked[$place]["values"]],
                                $trees[1][$picked[$place]["runs"]],
                                $quantities[$color["quantity"]],
                                $carried[$place]);
                            if ($block === false) {
                                return false;
                            }
                            self::placeBlock($planes[$place],
                                $plane_widths[$place], $block,
                                ($across * $color["wide"] + $wide) *
                                $side,
                                ($down * $color["high"] + $tall) * $side);
                        }
                    }
                }
            }
        }
        return self::spreadOut($frame, $planes, $plane_widths, $widest,
            $tallest);
    }
    /**
     * skipToRestart steps over a restart mark and starts reading bits afresh.
     * @param array &$reader where the reading has got to
     */
    public static function skipToRestart(&$reader)
    {
        $reader["bits"] = 0;
        $reader["held"] = 0;
        $bytes = $reader["bytes"];
        while ($reader["at"] < strlen($bytes) - 1) {
            if ($bytes[$reader["at"]] === "\xFF") {
                $marker = ord($bytes[$reader["at"] + 1]);
                if ($marker >= 0xD0 && $marker <= 0xD7) {
                    $reader["at"] += 2;
                    return;
                }
            }
            $reader["at"]++;
        }
    }
    /**
     * nextBit takes one bit from the picture's bits. A byte of all ones is
     * followed by a nothing byte, which is stepped over.
     * @param array &$reader where the reading has got to
     * @return int the bit, or minus one at the end
     */
    public static function nextBit(&$reader)
    {
        if ($reader["bits"] == 0) {
            if ($reader["at"] >= strlen($reader["bytes"])) {
                return -1;
            }
            $held = ord($reader["bytes"][$reader["at"]]);
            $reader["at"]++;
            if ($held == 0xFF) {
                $next = ord($reader["bytes"][$reader["at"]] ?? "\x00");
                if ($next == 0x00) {
                    $reader["at"]++;
                } else {
                    return -1;
                }
            }
            $reader["held"] = $held;
            $reader["bits"] = 8;
        }
        $reader["bits"]--;
        return ($reader["held"] >> $reader["bits"]) & 1;
    }
    /**
     * readCode reads bits until they match one of the runs in a table, and
     * hands back what that run stands for.
     * @param array &$reader where the reading has got to
     * @param array $tree the table of runs
     * @return int what the run stands for, or minus one at the end
     */
    public static function readCode(&$reader, $tree)
    {
        $code = 0;
        for ($size = 1; $size <= self::LONGEST_RUN; $size++) {
            $bit = self::nextBit($reader);
            if ($bit < 0) {
                return -1;
            }
            $code = ($code << 1) | $bit;
            if (isset($tree[$size][$code])) {
                return $tree[$size][$code];
            }
        }
        return -1;
    }
    /**
     * readNumber reads a given number of bits as a number that may be negative,
     * the way jpeg writes the difference between one value and the next.
     * @param array &$reader where the reading has got to
     * @param int $size how many bits
     * @return int the number
     */
    public static function readNumber(&$reader, $size)
    {
        if ($size == 0) {
            return 0;
        }
        $value = 0;
        for ($index = 0; $index < $size; $index++) {
            $bit = self::nextBit($reader);
            if ($bit < 0) {
                return 0;
            }
            $value = ($value << 1) | $bit;
        }
        if ($value < (1 << ($size - 1))) {
            $value -= (1 << $size) - 1;
        }
        return $value;
    }
    /**
     * readBlock reads one eight by eight block: its values, multiplied back up
     * by what was thrown away, then turned from the waves they were written as
     * back into the light and dark of the block.
     * @param array &$reader where the reading has got to
     * @param array $values_tree the table for the block's first value
     * @param array $runs_tree the table for the rest
     * @param array $quantity what was thrown away
     * @param int &$carried the running first value for this color
     * @return array|bool the block's sixty-four values, or false
     */
    public static function readBlock(&$reader, $values_tree, $runs_tree,
        $quantity, &$carried)
    {
        $side = self::BLOCK_SIDE;
        $values = self::BLOCK_VALUES;
        $block = array_fill(0, $values, 0);
        $size = self::readCode($reader, $values_tree);
        if ($size < 0) {
            return false;
        }
        $carried += self::readNumber($reader, $size);
        $block[0] = $carried * $quantity[0];
        $index = 1;
        while ($index < $values) {
            $told = self::readCode($reader, $runs_tree);
            if ($told < 0) {
                return false;
            }
            $skip = $told >> 4;
            $size = $told & 15;
            if ($size == 0) {
                if ($skip != 15) {
                    break;
                }
                $index += self::LONGEST_RUN;
                continue;
            }
            $index += $skip;
            if ($index > $values - 1) {
                break;
            }
            $where = self::$zigzag[$index];
            $block[$where] = self::readNumber($reader, $size) *
                $quantity[$where];
            $index++;
        }
        return self::fromWaves($block);
    }
    /**
     * fromWaves turns a block written as waves back into light and dark, by
     * adding up what each wave contributes at each of the sixty-four places.
     * @param array $block the block's values as waves
     * @return array the block's light and dark, zero to two hundred and fifty
     *     five
     */
    public static function fromWaves($block)
    {
        $side = self::BLOCK_SIDE;
        $values = self::BLOCK_VALUES;
        $middle = self::MIDDLE_SHADE;
        $most = self::MOST_SHADE;
        static $waves = null;
        if ($waves === null) {
            $waves = [];
            for ($place = 0; $place < $side; $place++) {
                for ($which = 0; $which < $side; $which++) {
                    $waves[$place][$which] = (($which == 0) ?
                        sqrt(1 / $side) : sqrt(2 / $side)) *
                        cos((2 * $place + 1) * $which * M_PI /
                        (2 * $side));
                }
            }
        }
        /* A block whose only value is its first is one flat shade, which
           is what most of a picture is made of. Working that out the long
           way costs a thousand multiplications for an answer that is the
           same at all sixty-four places. */
        $flat = true;
        for ($index = 1; $index < $values; $index++) {
            if ($block[$index] != 0) {
                $flat = false;
                break;
            }
        }
        if ($flat) {
            $shade = max(0, min($most,
                (int)round($block[0] / $side) + $middle));
            return array_fill(0, $values, $shade);
        }
        $rows = [];
        for ($down = 0; $down < $side; $down++) {
            /* A row with nothing in it past its first value is flat too,
               and the same saving applies along the row. */
            $only_first = true;
            for ($which = 1; $which < $side; $which++) {
                if ($block[$down * $side + $which] != 0) {
                    $only_first = false;
                    break;
                }
            }
            if ($only_first) {
                $shade = $waves[0][0] * $block[$down * $side];
                for ($across = 0; $across < $side; $across++) {
                    $rows[$down * $side + $across] = $shade;
                }
                continue;
            }
            for ($across = 0; $across < $side; $across++) {
                $total = 0;
                for ($which = 0; $which < $side; $which++) {
                    $total += $waves[$across][$which] *
                        $block[$down * $side + $which];
                }
                $rows[$down * $side + $across] = $total;
            }
        }
        $out = [];
        for ($across = 0; $across < $side; $across++) {
            for ($down = 0; $down < $side; $down++) {
                $total = 0;
                for ($which = 0; $which < $side; $which++) {
                    $total += $waves[$down][$which] *
                        $rows[$which * $side + $across];
                }
                $value = (int)round($total) + $middle;
                $out[$down * $side + $across] =
                    max(0, min($most, $value));
            }
        }
        return $out;
    }
    /**
     * placeBlock puts a read block into its place in a color's plane.
     * @param array &$plane the color's plane
     * @param int $plane_width how wide that plane is
     * @param array $block the block's values
     * @param int $across where the block begins across
     * @param int $down where it begins down
     */
    public static function placeBlock(&$plane, $plane_width, $block,
        $across, $down)
    {
        $side = self::BLOCK_SIDE;
        for ($row = 0; $row < $side; $row++) {
            $at = ($down + $row) * $plane_width + $across;
            for ($column = 0; $column < $side; $column++) {
                $plane[$at + $column] = $block[$row * $side + $column];
            }
        }
    }
    /**
     * spreadOut spreads each color's plane back out to the picture's own size.
     * A color kept at half the detail of another covers twice as much ground
     * per value, so its values are repeated to fill it.
     * @param array $frame how large the picture is and its colors
     * @param array $planes the planes as read
     * @param array $plane_widths how wide each plane is
     * @param int $widest the most detail any color has across
     * @param int $tallest the most any color has down
     * @return array the width, height and a plane per color
     */
    public static function spreadOut($frame, $planes, $plane_widths,
        $widest, $tallest)
    {
        $wide = $frame["width"];
        $high = $frame["height"];
        $spread = [];
        foreach ($frame["colors"] as $place => $color) {
            $across_step = $widest / $color["wide"];
            $down_step = $tallest / $color["high"];
            $out = array_fill(0, $wide * $high, 0);
            for ($down = 0; $down < $high; $down++) {
                $from_row = (int)($down / $down_step) *
                    $plane_widths[$place];
                for ($across = 0; $across < $wide; $across++) {
                    $out[$down * $wide + $across] =
                        $planes[$place][$from_row +
                        (int)($across / $across_step)] ?? 0;
                }
            }
            $spread[$place] = $out;
        }
        return ["width" => $wide, "height" => $high, "planes" => $spread];
    }
}
X