<?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 makes the crawl summary of a JPEG picture, and reads the
* picture's bytes into an image where the drawing library cannot.
* @author Chris Pollett
*/
class JpgProcessor extends ImageProcessor
{
/**
* 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 is what is added back to each 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
* it is read one shade per block rather than in full. Reading in full
* takes about a second for every two million points. The drawing
* library is not used instead, since it drops the black ink of a four
* ink picture.
* @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 the part of the redness plane that 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 the part of the blueness plane that is taken
* out of green.
* @var float
*/
const GREEN_FROM_BLUENESS = 0.344136;
/**
* GREEN_FROM_REDNESS is the part of the redness plane that is taken out
* of green.
* @var float
*/
const GREEN_FROM_REDNESS = 0.714136;
/**
* BLUE_FROM_BLUENESS is the part of the blueness plane that goes into
* blue.
* @var float
*/
const BLUE_FROM_BLUENESS = 1.772;
/**
* JPEG2000_END is the mark the standard calls EOC (end of code
* stream), which ends a JPEG 2000 code stream.
*/
const JPEG2000_END = 0xFFD9;
/**
* JPEG2000_DATA is the mark the standard calls SOD (start of data),
* after which a tile part's coded bytes follow.
*/
const JPEG2000_DATA = 0xFF93;
/**
* JPEG2000_SIZE is the mark the standard calls SIZ (image and tile
* size), which gives a JPEG 2000 picture's size, tiles and planes.
*/
const JPEG2000_SIZE = 0xFF51;
/**
* JPEG2000_CODING is the mark the standard calls COD (coding style
* default), which says how each plane is coded.
*/
const JPEG2000_CODING = 0xFF52;
/**
* JPEG2000_COMPONENT_CODING is the mark the standard calls COC (coding
* style component), which says how one plane is coded where it
* differs from the rest.
*/
const JPEG2000_COMPONENT_CODING = 0xFF53;
/**
* JPEG2000_ROUNDING is the mark the standard calls QCD (quantization
* default), which says how finely each plane's values are rounded.
*/
const JPEG2000_ROUNDING = 0xFF5C;
/**
* JPEG2000_COMPONENT_ROUNDING is the mark called QCC (quantization
* component), which says how finely one plane's values are rounded.
*/
const JPEG2000_COMPONENT_ROUNDING = 0xFF5D;
/**
* JPEG2000_TILE_PART is the mark the standard calls SOT (start of tile
* part), which starts a tile part.
*/
const JPEG2000_TILE_PART = 0xFF90;
/**
* JPEG2000_REGION is the mark the standard calls RGN (region), which
* marks a region of interest. Such a picture is not read here.
*/
const JPEG2000_REGION = 0xFF5E;
/**
* JPEG2000_ORDER_CHANGE is the mark the standard calls POC (progression
* order change), which changes the order of packets part way. Such a
* picture is not read here.
*/
const JPEG2000_ORDER_CHANGE = 0xFF5F;
/**
* JPEG2000_PACKED_HEADERS is the mark the standard calls PPM (packed
* packet headers, main header), which gathers packet headers in the
* main header. Such a picture is not read here.
*/
const JPEG2000_PACKED_HEADERS = 0xFF60;
/**
* JPEG2000_TILE_PACKED_HEADERS is the mark the standard calls PPT
* (packed packet headers, tile header), which gathers packet headers
* in a tile header. Such a picture is not read here.
*/
const JPEG2000_TILE_PACKED_HEADERS = 0xFF61;
/**
* JPEG2000_SOP_LENGTH is how many bytes the mark called SOP (start of
* packet) takes, which may start a packet: two for the mark and four
* after it.
*/
const JPEG2000_SOP_LENGTH = 6;
/**
* JPEG2000_BYPASS is the code block style bit that says some passes are
* written without the arithmetic coder. Such a picture is not read
* here.
*/
const JPEG2000_BYPASS = 1;
/**
* JPEG2000_RESET is the code block style bit that says each context
* starts afresh after each coding pass.
*/
const JPEG2000_RESET = 2;
/**
* JPEG2000_TERMINATE_ALL is the code block style bit that says the coder
* is ended after each coding pass, so each pass has its own length.
*/
const JPEG2000_TERMINATE_ALL = 4;
/**
* JPEG2000_CAUSAL is the code block style bit that says a stripe's last
* row does not look at the row below it.
*/
const JPEG2000_CAUSAL = 8;
/**
* JPEG2000_SEGMENT_MARK is the code block style bit that says each
* cleanup pass ends with four bits written as a check.
*/
const JPEG2000_SEGMENT_MARK = 32;
/**
* JPEG2000_REFINE_QUIET is the context for reading one more bit of a
* significant value the first time, when none of its neighbors are
* significant.
*/
const JPEG2000_REFINE_QUIET = 14;
/**
* JPEG2000_REFINE_BUSY is the context for reading one more bit of a
* significant value the first time, when some of its neighbors are
* significant.
*/
const JPEG2000_REFINE_BUSY = 15;
/**
* JPEG2000_REFINE_LATER is the context for reading one more bit of a
* significant value after the first time.
*/
const JPEG2000_REFINE_LATER = 16;
/**
* JPEG2000_RUN_CONTEXT is the context for the bit that says if a column
* of four quiet values holds any that turn significant.
*/
const JPEG2000_RUN_CONTEXT = 17;
/**
* JPEG2000_UNIFORM_CONTEXT is the context whose two answers are equally
* likely, used for which of the four values turned significant first.
*/
const JPEG2000_UNIFORM_CONTEXT = 18;
/**
* JPEG2000_FIRST_CONTEXTS gives the contexts that do not start in the
* coder's first state, each as its state and its more likely bit.
*/
const JPEG2000_FIRST_CONTEXTS = [0 => [4, 0], 17 => [3, 0],
18 => [46, 0]];
/**
* JPEG2000_SIGN_CONTEXTS gives the context for a sign and if the bit
* read is turned over, by the lean of the neighbors beside a value and
* then of those above and below it, each from -1 to 1.
*/
const JPEG2000_SIGN_CONTEXTS = [[[13, 1], [12, 1], [11, 1]],
[[10, 1], [9, 0], [10, 0]], [[11, 0], [12, 0], [13, 0]]];
/**
* MANTISSA_SCALE is what a rounding step's eleven bit mantissa is
* divided by: a step is 2 to a power times one plus mantissa / 2048.
*/
const MANTISSA_SCALE = 2048;
/**
* WAVELET_ALPHA is the last weight the 9/7 wavelet (small wave)
* uses in undoing its split of a picture into a half size picture and
* the detail needed to get back the full one. Each step adds to each
* value its two neighbors times a weight.
*/
const WAVELET_ALPHA = -1.586134342059924;
/**
* WAVELET_BETA is the third weight the 9/7 wavelet (small wave)
* uses in undoing its split of a picture into a half size picture and
* the detail needed to get back the full one. Each step adds to each
* value its two neighbors times a weight.
*/
const WAVELET_BETA = -0.052980118572961;
/**
* WAVELET_GAMMA is the second weight the 9/7 wavelet (small wave)
* uses in undoing its split of a picture into a half size picture and
* the detail needed to get back the full one. Each step adds to each
* value its two neighbors times a weight.
*/
const WAVELET_GAMMA = 0.882911075530934;
/**
* WAVELET_DELTA is the first weight the 9/7 wavelet (small wave)
* uses in undoing its split of a picture into a half size picture and
* the detail needed to get back the full one. Each step adds to each
* value its two neighbors times a weight.
*/
const WAVELET_DELTA = 0.443506852043971;
/**
* WAVELET_SCALE is K, what the 9/7 wavelet (small wave) multiplies its
* low values by, and divides its high values by, before it undoes its
* split of a picture into a half size picture and its detail.
*/
const WAVELET_SCALE = 1.230174104914001;
/**
* 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];
/**
* __construct sets up the indexing plugins that go 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 (L\programPath("TESSERACT", "tesseract") !== "") {
set_error_handler(null);
$temp_file = $this->saveTempFile($page, $url, "jpg");
$lang = UrlParser::getLang($url);
$ocr_data = ComputerVision::recognizeText($temp_file, [$lang],
self::$temp_dir);
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;
}
/**
* toImage turns a jpeg into an image, in any of the colors it may be
* 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
* @param bool $inverted true where a jpeg in four inks stores each ink
* turned over, 255 for no ink and 0 for full ink. A PDF says so
* with a Decode array that runs from 1 to 0, as Photoshop's
* pictures placed by macOS do.
* @return object|bool the image, or false if it cannot be read
*/
public static function toImage($bytes, $inverted = false)
{
$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) {
$worked_out = self::fromInkColors($bytes,
$points > self::MOST_POINTS_READ_HERE, $inverted);
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
* @param bool $small true to keep one shade for each block and hand
* back a picture an eighth the size, which reads in a moment
* @param bool $inverted true where each ink is stored turned over, 255
* for no ink, so each value is taken from 255 before it is used
* @return object|bool the image, or false if it cannot be read
*/
public static function fromInkColors($bytes, $small = false,
$inverted = false)
{
$read = self::readJpeg($bytes, $small);
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"]);
}
if ($inverted) {
foreach ($read["planes"] as $plane => $values) {
$read["planes"][$plane] = array_map(fn($value) =>
self::MOST_SHADE - $value, $values);
}
}
$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 if 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
* @param bool $small true to keep one shade for each block of sixty
* four, giving planes an eighth as wide and tall
* @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, $small = false)
{
$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, $small);
}
$at += 2 + $size;
}
return false;
}
/**
* readQuantities reads the tables that say what detail was dropped,
* 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
* @param bool $small true to keep one shade for each block, so the
* planes come back an eighth as wide and tall
* @return array|bool the width, height and a plane per color
*/
public static function readScan($bytes, $from, $body, $frame,
$quantities, $trees, $restart, $small = false)
{
$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));
$kept = ($small) ? 1 : $side;
$planes = [];
$plane_widths = [];
foreach ($frame["colors"] as $place => $color) {
$plane_widths[$place] = $across_groups * $kept *
$color["wide"];
$planes[$place] = array_fill(0, $plane_widths[$place] *
$down_groups * $kept * $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], $small);
if ($block === false) {
return false;
}
self::placeBlock($planes[$place],
$plane_widths[$place], $block,
($across * $color["wide"] + $wide) *
$kept,
($down * $color["high"] + $tall) * $kept,
$kept);
}
}
}
}
}
if ($small) {
$frame["width"] = (int)ceil($frame["width"] / $side);
$frame["height"] = (int)ceil($frame["height"] / $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 the bits read 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
* @param bool $small true to hand back only the block's one flat
* shade rather than its sixty four
* @return array|bool the block's sixty-four values, or false
*/
public static function readBlock(&$reader, $values_tree, $runs_tree,
$quantity, &$carried, $small = false)
{
$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++;
}
if ($small) {
return [max(0, min(self::MOST_SHADE, (int)round($block[0] /
self::BLOCK_SIDE) + self::MIDDLE_SHADE))];
}
return self::fromWaves($block);
}
/**
* fromWaves turns a block written as waves back into shades, 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 shades, 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
* @param int $side how many values a block has on a side, eight,
* or one where only its flat shade was kept
*/
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];
}
/**
* fromJpeg2000 reads a picture coded as JPEG 2000, the coding a PDF
* names JPXDecode, and gives it back as an image. A JPEG 2000 picture is
* kept as several resolutions, each twice the size of the one before,
* so a picture wanted only as a thumbnail is read at the smallest
* resolution whose longer side is at least $least_side, which skips most
* of the work. One, three and four planes are read as gray, red green
* and blue, and the four inks of print. Only what the standard calls
* part 1 is read, and a picture coded with arithmetic coding bypass, a
* region of interest or a changed progression order part way is not
* read; false comes back for those.
*
* @param string $bytes the picture, as a JP2 file or a bare code stream
* @param int $least_side the least number of points the longer side of
* the image should have, or 0 for the full picture
* @return object|bool the image, or false where it cannot be read
*/
public static function fromJpeg2000($bytes, $least_side = 0)
{
$stream = self::jpeg2000CodeStream($bytes);
if ($stream === "") {
return false;
}
$coding = self::jpeg2000Headers($stream);
if ($coding === false) {
return false;
}
$levels = $coding["cod"]["levels"];
foreach ($coding["coc"] as $component_cod) {
$levels = min($levels, $component_cod["levels"]);
}
$wide = $coding["x1"] - $coding["x0"];
$high = $coding["y1"] - $coding["y0"];
$reduce = 0;
while ($least_side > 0 && $reduce < $levels &&
max($wide, $high) / (2 << $reduce) >= $least_side) {
$reduce++;
}
$planes = self::jpeg2000Planes($coding, $reduce);
if ($planes === false) {
return false;
}
return self::imageFromJpeg2000Planes($planes);
}
/**
* jpeg2000CodeStream finds the code stream inside a JPEG 2000 picture.
* A JP2 file is a run of boxes, each starting with its length and a four
* letter kind, and the code stream is the box of kind jp2c. A bare code
* stream starts with the marks FF4F and FF51 and is handed back as it is.
*
* @param string $bytes the picture
* @return string the code stream, or an empty string where there is none
*/
public static function jpeg2000CodeStream($bytes)
{
if (strncmp($bytes, "\xFF\x4F\xFF\x51", 4) == 0) {
return $bytes;
}
$at = 0;
$length = strlen($bytes);
while ($at + 8 <= $length) {
$box = unpack("N", substr($bytes, $at, 4))[1];
$kind = substr($bytes, $at + 4, 4);
$head = 8;
if ($box == 1) {
$box = unpack("J", substr($bytes, $at + 8, 8))[1];
$head = 16;
} else if ($box == 0) {
$box = $length - $at;
}
if ($kind == "jp2c") {
return substr($bytes, $at + $head, $box - $head);
}
if ($box < $head) {
return "";
}
$at += $box;
}
return "";
}
/**
* jpeg2000Headers reads the main header of a JPEG 2000 code stream and
* gathers each tile's coded bytes. The main header gives the picture's
* size, its tiles and planes (SIZ), how it is coded (COD and COC) and
* how its values were rounded (QCD and QCC). Each tile part then starts
* with SOT, which says the tile and the part's length, and its coded
* bytes follow SOD. A tile's header may change the coding for that tile.
*
* @param string $stream the code stream
* @return array|bool what the headers say, with the coded bytes of each
* tile under tiles, or false where the stream uses something not
* read here
*/
public static function jpeg2000Headers($stream)
{
$coding = ["coc" => [], "qcc" => [], "tiles" => []];
$at = 2;
$length = strlen($stream);
$tile = -1;
$tile_coding = [];
$tile_part_ends = 0;
while ($at + 2 <= $length) {
$mark = unpack("n", substr($stream, $at, 2))[1];
if ($mark == self::JPEG2000_END) {
break;
}
if ($mark == self::JPEG2000_DATA) {
$data_end = ($tile_part_ends > $at) ? $tile_part_ends :
$length;
$coding["tiles"][$tile]["data"] = ($coding["tiles"][$tile][
"data"] ?? "") . substr($stream, $at + 2,
$data_end - $at - 2);
$at = $data_end;
continue;
}
if ($at + 4 > $length) {
break;
}
$size = unpack("n", substr($stream, $at + 2, 2))[1];
$body = substr($stream, $at + 4, $size - 2);
$target = & $coding;
if ($tile >= 0) {
$coding["tiles"][$tile] ??= ["coc" => [], "qcc" => []];
$target = & $coding["tiles"][$tile];
}
switch ($mark) {
case self::JPEG2000_SIZE:
$sizes = unpack("nrsiz/Nx1/Ny1/Nx0/Ny0/Ntile_wide/" .
"Ntile_high/Ntile_x0/Ntile_y0/ncomponents", $body);
foreach (["x1", "y1", "x0", "y0", "tile_wide",
"tile_high", "tile_x0", "tile_y0"] as $field) {
$coding[$field] = $sizes[$field];
}
$coding["components"] = [];
for ($i = 0; $i < $sizes["components"]; $i++) {
$depth = ord($body[36 + 3 * $i]);
$coding["components"][] = [
"depth" => ($depth & 127) + 1,
"signed" => ($depth & 128) > 0,
"x_step" => max(1, ord($body[37 + 3 * $i])),
"y_step" => max(1, ord($body[38 + 3 * $i]))];
}
break;
case self::JPEG2000_CODING:
$target["cod"] = self::jpeg2000CodingStyle(
ord($body[0]), substr($body, 5));
$target["cod"]["order"] = ord($body[1]);
$target["cod"]["layers"] =
unpack("n", substr($body, 2, 2))[1];
$target["cod"]["component_transform"] =
ord($body[4]) == 1;
break;
case self::JPEG2000_COMPONENT_CODING:
$wide_index = count($coding["components"] ?? []) > 256;
$component = $wide_index ?
unpack("n", substr($body, 0, 2))[1] : ord($body[0]);
$skip = $wide_index ? 2 : 1;
$target["coc"][$component] = self::jpeg2000CodingStyle(
ord($body[$skip]), substr($body, $skip + 1));
break;
case self::JPEG2000_ROUNDING:
$target["qcd"] = self::jpeg2000Rounding($body);
break;
case self::JPEG2000_COMPONENT_ROUNDING:
$wide_index = count($coding["components"] ?? []) > 256;
$component = $wide_index ?
unpack("n", substr($body, 0, 2))[1] : ord($body[0]);
$target["qcc"][$component] = self::jpeg2000Rounding(
substr($body, $wide_index ? 2 : 1));
break;
case self::JPEG2000_TILE_PART:
$parts = unpack("ntile/Nlength", substr($body, 0, 6));
$tile = $parts["tile"];
$coding["tiles"][$tile] ??= ["coc" => [], "qcc" => []];
$tile_part_ends = ($parts["length"] > 0) ?
$at + $parts["length"] : $length;
break;
case self::JPEG2000_REGION:
case self::JPEG2000_ORDER_CHANGE:
case self::JPEG2000_PACKED_HEADERS:
case self::JPEG2000_TILE_PACKED_HEADERS:
return false;
}
unset($target);
$at += 2 + $size;
}
if (empty($coding["components"]) || empty($coding["cod"]) ||
empty($coding["qcd"])) {
return false;
}
return $coding;
}
/**
* jpeg2000CodingStyle reads part of a COD (coding style) or COC (
* component coding style) mark: how many times a plane was halved
* (its levels), the size of the blocks its values are coded in, how
* those blocks were coded, which wavelet was used, and the size of
* the precincts at each resolution, which are the groups of blocks
* sent together.
*
* @param int $style the mark's first byte, whose lowest bit says that
* precinct sizes follow, the next that each packet starts with an
* SOP mark and the next that each packet header ends with EPH
* @param string $body the rest of the mark
* @return array the coding style
*/
public static function jpeg2000CodingStyle($style, $body)
{
$levels = ord($body[0]);
$sizes = [];
for ($i = 0; $i <= $levels; $i++) {
$packed = (($style & 1) > 0 && isset($body[5 + $i])) ?
ord($body[5 + $i]) : 0xFF;
$sizes[] = [$packed & 15, $packed >> 4];
}
return ["levels" => $levels, "block_wide" => (ord($body[1]) & 15) +
2, "block_high" => (ord($body[2]) & 15) + 2,
"block_style" => ord($body[3]),
"reversible" => ord($body[4]) == 1,
"precincts" => $sizes, "sop" => ($style & 2) > 0,
"eph" => ($style & 4) > 0];
}
/**
* jpeg2000Rounding reads a QCD (quantization) or QCC (component
* quantization) mark, which says how many extra bits guard against
* overflow and how finely each subband's values are rounded. A
* subband's step is an exponent and an eleven bit mantissa, or for a
* lossless picture an exponent alone. Where only the first
* step is given, the others are worked out from it.
*
* @param string $body the mark's body
* @return array the guard bits, the style and the steps, by subband
*/
public static function jpeg2000Rounding($body)
{
$style = ord($body[0]);
$steps = [];
$kind = $style & 31;
if ($kind == 0) {
for ($i = 1; $i < strlen($body); $i++) {
$steps[] = [ord($body[$i]) >> 3, 0];
}
} else {
for ($i = 1; $i + 1 < strlen($body); $i += 2) {
$value = unpack("n", substr($body, $i, 2))[1];
$steps[] = [$value >> 11, $value & 2047];
}
}
return ["guard" => $style >> 5, "kind" => $kind, "steps" => $steps];
}
/**
* jpeg2000Planes reads each tile of a JPEG 2000 picture and puts each
* plane together at the resolution asked for, $reduce halvings below
* the full size. Each value comes back as a shade from 0 to 255.
*
* @param array $coding what jpeg2000Headers read, with each tile's bytes
* @param int $reduce how many times to halve the picture
* @return array|bool each plane's width, height and shades, row by row,
* or false where a tile cannot be read
*/
public static function jpeg2000Planes($coding, $reduce)
{
$tiles_across = (int)ceil(($coding["x1"] - $coding["tile_x0"]) /
$coding["tile_wide"]);
$planes = [];
foreach ($coding["components"] as $index => $component) {
$style = $coding["coc"][$index] ?? $coding["cod"];
$shrink = 1 << min($reduce, $style["levels"]);
$left = (int)ceil(ceil($coding["x0"] / $component["x_step"]) /
$shrink);
$top = (int)ceil(ceil($coding["y0"] / $component["y_step"]) /
$shrink);
$wide = (int)ceil(ceil($coding["x1"] / $component["x_step"]) /
$shrink) - $left;
$high = (int)ceil(ceil($coding["y1"] / $component["y_step"]) /
$shrink) - $top;
$planes[$index] = ["left" => $left, "top" => $top,
"wide" => $wide, "high" => $high,
"shades" => array_fill(0, max(1, $wide * $high), 0)];
}
foreach ($coding["tiles"] as $number => $tile) {
$across = $number % $tiles_across;
$down = intdiv($number, $tiles_across);
$bounds = [max($coding["tile_x0"] + $across *
$coding["tile_wide"], $coding["x0"]),
max($coding["tile_y0"] + $down * $coding["tile_high"],
$coding["y0"]),
min($coding["tile_x0"] + ($across + 1) *
$coding["tile_wide"], $coding["x1"]),
min($coding["tile_y0"] + ($down + 1) *
$coding["tile_high"], $coding["y1"])];
$parts = self::jpeg2000Tile($coding, $tile, $bounds, $reduce);
if ($parts === false) {
return false;
}
foreach ($parts as $index => $part) {
$plane = & $planes[$index];
$depth = $coding["components"][$index]["depth"];
$shift = $coding["components"][$index]["signed"] ? 0 :
1 << ($depth - 1);
$most = (1 << $depth) - 1;
$scale = self::MOST_SHADE / $most;
for ($row = 0; $row < $part["high"]; $row++) {
$to_row = $part["top"] + $row - $plane["top"];
if ($to_row < 0 || $to_row >= $plane["high"]) {
continue;
}
for ($column = 0; $column < $part["wide"]; $column++) {
$to_column = $part["left"] + $column -
$plane["left"];
if ($to_column < 0 || $to_column >= $plane["wide"]) {
continue;
}
$value = (int)round($part["values"][$row *
$part["wide"] + $column] + $shift);
$value = max(0, min($most, $value));
$plane["shades"][$to_row * $plane["wide"] +
$to_column] = (int)round($value * $scale);
}
}
unset($plane);
}
}
return $planes;
}
/**
* imageFromJpeg2000Planes makes an image from the planes of a JPEG 2000
* picture. One plane is gray, three are red, green and blue, and four
* are the inks of print, cyan, magenta, yellow and black, whose light
* is worked out as fromInkColors does. A plane kept at a lower
* resolution than the first, as a picture's color often is, is spread
* to the first plane's size.
*
* @param array $planes the planes jpeg2000Planes gives
* @return object|bool the image, or false where the planes are empty
*/
public static function imageFromJpeg2000Planes($planes)
{
$wide = $planes[0]["wide"];
$high = $planes[0]["high"];
if ($wide < 1 || $high < 1) {
return false;
}
$count = count($planes);
$image = imagecreatetruecolor($wide, $high);
for ($down = 0; $down < $high; $down++) {
for ($across = 0; $across < $wide; $across++) {
$values = [];
foreach ($planes as $plane) {
$column = min($plane["wide"] - 1,
intdiv($across * $plane["wide"], $wide));
$row = min($plane["high"] - 1,
intdiv($down * $plane["high"], $high));
$values[] = $plane["shades"][$row * $plane["wide"] +
$column];
}
if ($count >= 4) {
$black = self::MOST_SHADE - $values[3];
$red = intdiv((self::MOST_SHADE - $values[0]) * $black,
self::MOST_SHADE);
$green = intdiv((self::MOST_SHADE - $values[1]) *
$black, self::MOST_SHADE);
$blue = intdiv((self::MOST_SHADE - $values[2]) *
$black, self::MOST_SHADE);
} else if ($count == 3) {
[$red, $green, $blue] = $values;
} else {
$red = $green = $blue = $values[0];
}
imagesetpixel($image, $across, $down,
($red << 16) | ($green << 8) | $blue);
}
}
return $image;
}
/**
* jpeg2000Tile reads one tile of a JPEG 2000 picture. It lays out each
* plane's resolutions, subbands, precincts and code blocks, reads the
* packets that carry the code blocks' bytes, decodes the blocks of the
* resolutions wanted, undoes the wavelet, and undoes the transform that
* mixed the first three planes where the picture used one.
*
* @param array $coding what jpeg2000Headers read
* @param array $tile the tile's bytes and any coding of its own
* @param array $bounds the tile's left, top, right and bottom on the
* picture's grid
* @param int $reduce how many times to halve the picture
* @return array|bool each plane's part of the tile, as the place of its
* top left corner, its size and its values, or false where the tile
* uses something not read here
*/
public static function jpeg2000Tile($coding, $tile, $bounds, $reduce)
{
$main = $tile["cod"] ?? $coding["cod"];
$layout = [];
foreach ($coding["components"] as $index => $component) {
$style = $tile["coc"][$index] ?? (isset($tile["cod"]) ?
$tile["cod"] : ($coding["coc"][$index] ?? $coding["cod"]));
$rounding = $tile["qcc"][$index] ?? ($tile["qcd"] ??
($coding["qcc"][$index] ?? $coding["qcd"]));
if (($style["block_style"] & self::JPEG2000_BYPASS) > 0) {
return false;
}
$layout[$index] = self::jpeg2000Layout($bounds, $component,
$style, $rounding, min($reduce, $style["levels"]));
}
if (!self::jpeg2000ReadPackets($tile["data"] ?? "", $main, $layout,
$bounds)) {
return false;
}
$parts = [];
foreach ($layout as $index => $plane) {
$parts[$index] = self::jpeg2000PlaneFromBlocks($plane,
$coding["components"][$index]["depth"]);
}
if ($main["component_transform"] && count($parts) >= 3 &&
count($parts[0]["values"]) == count($parts[1]["values"]) &&
count($parts[0]["values"]) == count($parts[2]["values"])) {
$reversible = $layout[0]["style"]["reversible"];
$count = count($parts[0]["values"]);
for ($i = 0; $i < $count; $i++) {
$bright = $parts[0]["values"][$i];
$blueness = $parts[1]["values"][$i];
$redness = $parts[2]["values"][$i];
if ($reversible) {
$green = $bright - floor(($blueness + $redness) / 4);
$red = $redness + $green;
$blue = $blueness + $green;
} else {
$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;
}
$parts[0]["values"][$i] = $red;
$parts[1]["values"][$i] = $green;
$parts[2]["values"][$i] = $blue;
}
}
return $parts;
}
/**
* jpeg2000Layout works out where each resolution, subband (a part of a
* resolution holding one kind of detail), precinct and code block of
* one plane of a tile lies. Resolution r of a plane halved
* L times covers the plane's part of the tile shrunk 2 to the L - r
* times; resolution 0 holds the LL subband and each later one the HL, LH
* and HH subbands, the detail added in going up a resolution. A subband
* is cut into code blocks, coded on their own, and the blocks are
* grouped by precinct, the part of a resolution a packet carries.
*
* @param array $bounds the tile's left, top, right and bottom
* @param array $component how the plane is sampled
* @param array $style how the plane is coded
* @param array $rounding how its values were rounded
* @param int $reduce how many of its resolutions are not wanted
* @return array the plane's layout, with an empty list of bytes for
* each code block, to be filled from the packets
*/
public static function jpeg2000Layout($bounds, $component, $style,
$rounding, $reduce)
{
$levels = $style["levels"];
$plane_bounds = [(int)ceil($bounds[0] / $component["x_step"]),
(int)ceil($bounds[1] / $component["y_step"]),
(int)ceil($bounds[2] / $component["x_step"]),
(int)ceil($bounds[3] / $component["y_step"])];
$resolutions = [];
for ($resolution = 0; $resolution <= $levels; $resolution++) {
$scale = 1 << ($levels - $resolution);
$edges = array_map(fn($edge) => (int)ceil($edge / $scale),
$plane_bounds);
[$precinct_wide, $precinct_high] =
$style["precincts"][$resolution] ?? [15, 15];
$across = ($edges[2] > $edges[0]) ? (int)ceil($edges[2] /
(1 << $precinct_wide)) - intdiv($edges[0],
1 << $precinct_wide) : 0;
$down = ($edges[3] > $edges[1]) ? (int)ceil($edges[3] /
(1 << $precinct_high)) - intdiv($edges[1],
1 << $precinct_high) : 0;
$band_kinds = ($resolution == 0) ? [0] : [1, 2, 3];
$smaller = ($resolution == 0) ? 0 : 1;
$block_wide = min($style["block_wide"], $precinct_wide - $smaller);
$block_high = min($style["block_high"], $precinct_high -
$smaller);
$bands = [];
foreach ($band_kinds as $kind) {
$bands[] = self::jpeg2000Band($plane_bounds, $levels,
$resolution, $kind, $style, $rounding, $component,
[$block_wide, $block_high],
[$precinct_wide - $smaller, $precinct_high - $smaller],
[intdiv($edges[0], 1 << $precinct_wide),
intdiv($edges[1], 1 << $precinct_high)], $across);
}
$resolutions[] = ["edges" => $edges, "across" => $across,
"down" => $down, "precinct_wide" => $precinct_wide,
"precinct_high" => $precinct_high, "bands" => $bands,
"wanted" => $resolution <= $levels - $reduce];
}
return ["style" => $style, "resolutions" => $resolutions,
"levels" => $levels, "reduce" => $reduce,
"x_step" => $component["x_step"],
"y_step" => $component["y_step"]];
}
/**
* jpeg2000Band lays out one subband (a part of a resolution holding one
* kind of detail) of a resolution: where it lies,
* its code blocks, and for each precinct the blocks in it with the two
* tag trees a packet header uses for them. It also works out the
* subband's rounding step and how many magnitude bits its values have.
*
* @param array $plane_bounds the plane's part of the tile
* @param int $levels how many times the plane was halved
* @param int $resolution which resolution the subband is in
* @param int $kind 0 for LL, 1 for HL, 2 for LH and 3 for HH
* @param array $style how the plane is coded
* @param array $rounding how its values were rounded
* @param array $component how the plane is sampled
* @param array $block_sizes the code block width and height, as powers
* of two
* @param array $precinct_sizes the precinct width and height in the
* subband, as powers of two
* @param array $first_precinct the column and row of the resolution's
* first precinct on the precinct grid
* @param int $precincts_across how many precincts a row holds
* @return array the subband
*/
public static function jpeg2000Band($plane_bounds, $levels, $resolution,
$kind, $style, $rounding, $component, $block_sizes, $precinct_sizes,
$first_precinct, $precincts_across)
{
$halvings = ($resolution == 0) ? $levels : $levels - $resolution + 1;
$x_offset = ($kind == 1 || $kind == 3) ? 1 : 0;
$y_offset = ($kind == 2 || $kind == 3) ? 1 : 0;
$scale = 1 << $halvings;
$half = ($halvings > 0) ? 1 << ($halvings - 1) : 0;
$edges = [(int)ceil(($plane_bounds[0] - $half * $x_offset) / $scale),
(int)ceil(($plane_bounds[1] - $half * $y_offset) / $scale),
(int)ceil(($plane_bounds[2] - $half * $x_offset) / $scale),
(int)ceil(($plane_bounds[3] - $half * $y_offset) / $scale)];
$step_index = ($resolution == 0) ? 0 : 1 + 3 * ($resolution - 1) +
$kind - 1;
if ($rounding["kind"] == 1) {
[$exponent, $mantissa] = $rounding["steps"][0] ?? [0, 0];
$exponent = $exponent - $levels + $halvings;
} else {
[$exponent, $mantissa] = $rounding["steps"][$step_index] ??
[0, 0];
}
$gain = [0, 1, 1, 2][$kind];
$step = $style["reversible"] ? 1 : (2 ** ($component["depth"] +
$gain - $exponent)) * (1 + $mantissa / self::MANTISSA_SCALE);
$blocks = [];
$precincts = [];
$block_wide = 1 << $block_sizes[0];
$block_high = 1 << $block_sizes[1];
if ($edges[2] > $edges[0] && $edges[3] > $edges[1]) {
for ($row = intdiv($edges[1], $block_high);
$row * $block_high < $edges[3]; $row++) {
for ($column = intdiv($edges[0], $block_wide);
$column * $block_wide < $edges[2]; $column++) {
$left = max($edges[0], $column * $block_wide);
$top = max($edges[1], $row * $block_high);
$precinct = (intdiv($top, 1 << $precinct_sizes[1]) -
$first_precinct[1]) * $precincts_across +
intdiv($left, 1 << $precinct_sizes[0]) -
$first_precinct[0];
$precincts[$precinct][$row][$column] = count($blocks);
$blocks[] = ["left" => $left, "top" => $top,
"right" => min($edges[2], ($column + 1) *
$block_wide), "bottom" => min($edges[3], ($row + 1) *
$block_high), "included" => false, "zero" => 0,
"lengths" => 3, "passes" => 0, "bytes" => "",
"segments" => []];
}
}
}
$grids = [];
foreach ($precincts as $precinct => $rows) {
$order = [];
$first_row = min(array_keys($rows));
$first_column = PHP_INT_MAX;
$last_column = 0;
foreach ($rows as $columns) {
$first_column = min($first_column, min(array_keys($columns)));
$last_column = max($last_column, max(array_keys($columns)));
}
foreach ($rows as $row => $columns) {
foreach ($columns as $column => $block) {
$order[] = [$block, $column - $first_column,
$row - $first_row];
}
}
$wide = $last_column - $first_column + 1;
$high = count($rows);
$grids[$precinct] = ["order" => $order,
"inclusion" => self::jpeg2000TagTree($wide, $high),
"zero" => self::jpeg2000TagTree($wide, $high)];
}
return ["kind" => $kind, "edges" => $edges, "blocks" => $blocks,
"precincts" => $grids, "step" => $step,
"magnitude_bits" => $rounding["guard"] + $exponent - 1];
}
/**
* jpeg2000TagTree makes an empty tag tree over a grid of code blocks. A
* tag tree codes one number for each place of a grid, such as the first
* layer a block is in, by coding the least of each square of four, then
* the least of each square of those, up to one number for the whole
* grid, so that a number shared by neighbors is sent once.
*
* @param int $wide how many places across
* @param int $high how many places down
* @return array the tree's levels, the grid first, each with its width
* and, for each place, what is known of its number so far
*/
public static function jpeg2000TagTree($wide, $high)
{
$levels = [];
do {
$levels[] = ["wide" => $wide,
"low" => array_fill(0, $wide * $high, 0),
"known" => array_fill(0, $wide * $high, false)];
$done = ($wide <= 1 && $high <= 1);
$wide = (int)ceil($wide / 2);
$high = (int)ceil($high / 2);
} while (!$done);
return $levels;
}
/**
* jpeg2000TagTreeValue reads from a packet header the number a tag tree
* holds for one place, as far as is needed to tell whether it is at most
* $most. The tree is walked from its top; at each level a 0 bit says the
* number is higher than the least known so far and a 1 bit says it is
* that.
*
* @param array &$tree the tag tree, updated with what is read
* @param int $column the place's column in the grid
* @param int $row the place's row
* @param int $most the highest number of interest
* @param array &$reader the packet header being read
* @return int the number, or -1 where it is higher than $most
*/
public static function jpeg2000TagTreeValue(&$tree, $column, $row, $most,
&$reader)
{
$least = 0;
for ($level = count($tree) - 1; $level >= 0; $level--) {
$at = ($row >> $level) * $tree[$level]["wide"] +
($column >> $level);
if ($tree[$level]["low"][$at] < $least) {
$tree[$level]["low"][$at] = $least;
}
while (!$tree[$level]["known"][$at] &&
$tree[$level]["low"][$at] <= $most) {
if (self::jpeg2000Bits($reader, 1) == 1) {
$tree[$level]["known"][$at] = true;
} else {
$tree[$level]["low"][$at]++;
}
}
$least = $tree[$level]["low"][$at];
if (!$tree[$level]["known"][$at]) {
return -1;
}
}
return $least;
}
/**
* jpeg2000Bits reads bits from a packet header. A header is read a bit
* at a time, highest bit first, and after a byte of all ones the next
* byte carries only seven bits, its top bit being a stuffed 0, so the
* header can never look like a marker.
*
* @param array &$reader the header's bytes, place and bits in hand
* @param int $count how many bits to read
* @return int the bits as a number
*/
public static function jpeg2000Bits(&$reader, $count)
{
$value = 0;
for ($i = 0; $i < $count; $i++) {
if ($reader["left"] == 0) {
$byte = ($reader["at"] < strlen($reader["bytes"])) ?
ord($reader["bytes"][$reader["at"]]) : 0;
$reader["at"]++;
$reader["left"] = $reader["after_all_ones"] ? 7 : 8;
$reader["after_all_ones"] = ($byte == 0xFF);
$reader["byte"] = $byte;
}
$reader["left"]--;
$value = ($value << 1) | (($reader["byte"] >> $reader["left"]) &
1);
}
return $value;
}
/**
* jpeg2000ReadPackets reads a tile's packets and gives each code block
* its bytes and its count of coding passes. A packet holds, for one
* layer, one resolution, one plane and one precinct, a header saying
* which blocks it adds to and by how much, then those bytes. The order
* of packets is the tile's progression order: layer, resolution, plane
* and position nested in one of five ways. Only the blocks of wanted
* resolutions keep their bytes.
*
* @param string $data the tile's coded bytes
* @param array $main the tile's coding style, for the order and layers
* @param array &$layout each plane's layout, filled in
* @param array $bounds the tile's place on the picture's grid
* @return bool true where the packets were read
*/
public static function jpeg2000ReadPackets($data, $main, &$layout,
$bounds)
{
$packets = self::jpeg2000PacketOrder($main, $layout, $bounds);
$at = 0;
$length = strlen($data);
$last_wanted = [];
foreach ($layout as $index => $plane) {
$last_wanted[$index] = $plane["levels"] - $plane["reduce"];
}
$resolution_first = in_array($main["order"], [1, 2]);
foreach ($packets as [$layer, $resolution, $index, $precinct]) {
if ($at >= $length) {
break;
}
if ($resolution_first && $resolution > max($last_wanted)) {
break;
}
$style = $layout[$index]["style"];
if ($main["sop"] && substr($data, $at, 2) == "\xFF\x91") {
$at += self::JPEG2000_SOP_LENGTH;
}
$reader = ["bytes" => $data, "at" => $at, "left" => 0,
"byte" => 0, "after_all_ones" => false];
$taken = [];
if (self::jpeg2000Bits($reader, 1) == 1) {
foreach ($layout[$index]["resolutions"][$resolution]["bands"]
as $band_index => $band) {
if (!isset($band["precincts"][$precinct])) {
continue;
}
$grid = & $layout[$index]["resolutions"][$resolution][
"bands"][$band_index]["precincts"][$precinct];
foreach ($grid["order"] as [$block_index, $column,
$row]) {
$block = & $layout[$index]["resolutions"][
$resolution]["bands"][$band_index]["blocks"][
$block_index];
if ($block["included"]) {
$in = self::jpeg2000Bits($reader, 1) == 1;
} else {
$in = self::jpeg2000TagTreeValue(
$grid["inclusion"], $column, $row, $layer,
$reader) >= 0;
if ($in) {
$block["zero"] = self::jpeg2000TagTreeValue(
$grid["zero"], $column, $row,
PHP_INT_MAX, $reader);
$block["included"] = true;
}
}
if (!$in) {
unset($block);
continue;
}
$passes = self::jpeg2000PassCount($reader);
while (self::jpeg2000Bits($reader, 1) == 1) {
$block["lengths"]++;
}
$sizes = [];
if (($style["block_style"] &
self::JPEG2000_TERMINATE_ALL) > 0) {
for ($i = 0; $i < $passes; $i++) {
$sizes[] = self::jpeg2000Bits($reader,
$block["lengths"]);
}
} else {
$more_bits = 0;
while ((2 << $more_bits) <= $passes) {
$more_bits++;
}
$sizes[] = self::jpeg2000Bits($reader,
$block["lengths"] + $more_bits);
}
$taken[] = [$band_index, $block_index, $passes,
$sizes];
unset($block);
}
unset($grid);
}
}
$at = $reader["at"];
if ($reader["after_all_ones"]) {
$at++;
}
if ($main["eph"] && substr($data, $at, 2) == "\xFF\x92") {
$at += 2;
}
$wanted = $resolution <= $last_wanted[$index];
foreach ($taken as [$band_index, $block_index, $passes,
$sizes]) {
$block = & $layout[$index]["resolutions"][$resolution][
"bands"][$band_index]["blocks"][$block_index];
foreach ($sizes as $size) {
if ($wanted) {
$block["bytes"] .= substr($data, $at, $size);
$block["segments"][] = $size;
}
$at += $size;
}
$block["passes"] += $passes;
unset($block);
}
}
return true;
}
/**
* jpeg2000PassCount reads how many coding passes a packet adds to a code
* block. The count is coded so small counts are short: 0 for one, 10 for
* two, 11 and two bits for three to five, 1111 and five bits for six to
* thirty-six, and 1111 11111 and seven bits for more.
*
* @param array &$reader the packet header being read
* @return int the number of passes
*/
public static function jpeg2000PassCount(&$reader)
{
if (self::jpeg2000Bits($reader, 1) == 0) {
return 1;
}
if (self::jpeg2000Bits($reader, 1) == 0) {
return 2;
}
$value = self::jpeg2000Bits($reader, 2);
if ($value < 3) {
return 3 + $value;
}
$value = self::jpeg2000Bits($reader, 5);
if ($value < 31) {
return 6 + $value;
}
return 37 + self::jpeg2000Bits($reader, 7);
}
/**
* jpeg2000PacketOrder lists a tile's packets in the order its
* progression puts them, each as its layer, resolution, plane and
* precinct. The orders are layer-resolution-plane-position (0),
* resolution-layer-plane-position (1), resolution-position-plane-layer
* (2), position-plane-resolution-layer (3) and
* plane-position-resolution-layer (4). A precinct's position is where
* its top left corner falls on the picture's grid.
*
* @param array $main the tile's coding style
* @param array $layout each plane's layout
* @param array $bounds the tile's place on the picture's grid
* @return array the packets in order
*/
public static function jpeg2000PacketOrder($main, $layout, $bounds)
{
$layers = max(1, $main["layers"]);
$places = [];
foreach ($layout as $index => $plane) {
foreach ($plane["resolutions"] as $resolution => $level) {
$scale = 1 << ($plane["levels"] - $resolution);
for ($down = 0; $down < $level["down"]; $down++) {
for ($across = 0; $across < $level["across"]; $across++) {
$place_across = max($bounds[0],
((intdiv($level["edges"][0],
1 << $level["precinct_wide"]) + $across) <<
$level["precinct_wide"]) * $scale *
$plane["x_step"]);
$place_down = max($bounds[1],
((intdiv($level["edges"][1],
1 << $level["precinct_high"]) + $down) <<
$level["precinct_high"]) * $scale *
$plane["y_step"]);
$places[] = ["plane" => $index,
"resolution" => $resolution,
"precinct" => $down * $level["across"] + $across,
"x" => $place_across, "y" => $place_down];
}
}
}
}
$keys = match ($main["order"]) {
0, 1 => ["resolution", "plane", "precinct"],
2 => ["resolution", "y", "x", "plane"],
3 => ["y", "x", "plane", "resolution"],
default => ["plane", "y", "x", "resolution"],
};
usort($places, function ($first, $second) use ($keys) {
foreach ($keys as $key) {
if ($first[$key] != $second[$key]) {
return $first[$key] <=> $second[$key];
}
}
return $first["precinct"] <=> $second["precinct"];
});
$packets = [];
if ($main["order"] == 0) {
for ($layer = 0; $layer < $layers; $layer++) {
foreach ($places as $place) {
$packets[] = [$layer, $place["resolution"],
$place["plane"], $place["precinct"]];
}
}
} else if ($main["order"] == 1) {
$by_resolution = [];
foreach ($places as $place) {
$by_resolution[$place["resolution"]][] = $place;
}
ksort($by_resolution);
foreach ($by_resolution as $group) {
for ($layer = 0; $layer < $layers; $layer++) {
foreach ($group as $place) {
$packets[] = [$layer, $place["resolution"],
$place["plane"], $place["precinct"]];
}
}
}
} else {
foreach ($places as $place) {
for ($layer = 0; $layer < $layers; $layer++) {
$packets[] = [$layer, $place["resolution"],
$place["plane"], $place["precinct"]];
}
}
}
return $packets;
}
/**
* jpeg2000PlaneFromBlocks decodes the code blocks of the wanted
* resolutions of one plane of a tile, places their values in their
* subbands, and undoes the wavelet one resolution at a time: each step
* takes the image so far as the LL subband of the next resolution and
* adds its HL, LH and HH detail.
*
* @param array $plane the plane's layout, with its blocks' bytes
* @param int $depth how many bits the plane's values had
* @return array the plane's part of the tile at the wanted resolution,
* as the place of its top left corner, its size and its values
*/
public static function jpeg2000PlaneFromBlocks($plane, $depth)
{
$last = $plane["levels"] - $plane["reduce"];
$style = $plane["style"];
$image = null;
for ($resolution = 0; $resolution <= $last; $resolution++) {
$level = $plane["resolutions"][$resolution];
$subbands = [];
foreach ($level["bands"] as $band) {
$subbands[$band["kind"]] = self::jpeg2000BandValues($band,
$style);
}
if ($resolution == 0) {
$image = $subbands[0];
continue;
}
$image = self::jpeg2000Combine($image, $subbands, $level["edges"],
$style["reversible"]);
}
$edges = $plane["resolutions"][$last]["edges"];
return ["left" => $edges[0], "top" => $edges[1],
"wide" => $edges[2] - $edges[0], "high" => $edges[3] - $edges[1],
"values" => $image["values"]];
}
/**
* jpeg2000BandValues decodes each code block of a subband (a part of a
* resolution holding one kind of detail) and gives its values, row by
* row.
*
* @param array $band the subband, with its blocks' bytes
* @param array $style how the plane is coded
* @return array the subband's width, height and values
*/
public static function jpeg2000BandValues($band, $style)
{
[$left, $top, $right, $bottom] = $band["edges"];
$wide = max(0, $right - $left);
$high = max(0, $bottom - $top);
$values = array_fill(0, max(1, $wide * $high), 0.0);
foreach ($band["blocks"] as $block) {
if ($block["passes"] < 1 || $block["bytes"] === "") {
continue;
}
$decoded = self::jpeg2000DecodeBlock($block, $band["kind"],
$band["magnitude_bits"], $style["block_style"],
$band["step"], $style["reversible"]);
$block_wide = $block["right"] - $block["left"];
foreach ($decoded as $at => $value) {
if ($value == 0) {
continue;
}
$row = intdiv($at, $block_wide) + $block["top"] - $top;
$column = $at % $block_wide + $block["left"] - $left;
$values[$row * $wide + $column] = $value;
}
}
return ["wide" => $wide, "high" => $high, "values" => $values];
}
/**
* jpeg2000DecodeBlock decodes one code block. Its bytes are written by
* an arithmetic coder, the MQ coder, which JBig2 also reads, over the
* block's bit planes from the highest: each plane is read in three
* passes, one for values that may become significant because a
* neighbor is, one refining values already significant, and one
* cleaning up the rest, with the first plane read by a cleanup pass
* alone. Values are read in stripes four rows high, column by column.
* Each bit is read under a context, a number saying what its neighbors
* are, so the coder can guess it well. A value's sign is read when it
* first becomes significant. Each value then gets the middle of what its
* undecoded bits could hold and is scaled by the subband's step.
*
* @param array $block the block's size, bytes and passes
* @param int $kind the subband, 0 for LL, 1 for HL, 2 for LH and 3 for HH
* @param int $magnitude_bits how many magnitude bits the subband's
* values have
* @param int $block_style the code block style bits from COD or COC
* @param float $step the subband's rounding step
* @param bool $reversible whether the plane is lossless
* @return array the block's values, row by row
*/
public static function jpeg2000DecodeBlock($block, $kind,
$magnitude_bits, $block_style, $step, $reversible)
{
$wide = $block["right"] - $block["left"];
$high = $block["bottom"] - $block["top"];
$values = array_fill(0, max(1, $wide * $high), 0);
$plane = $magnitude_bits - 1 - $block["zero"];
if ($plane < 0 || $wide < 1 || $high < 1) {
return $values;
}
$row_size = $wide + 2;
$size = $row_size * ($high + 2);
$significant = array_fill(0, $size, 0);
$negative = array_fill(0, $size, 0);
$magnitude = array_fill(0, $size, 0);
$visited = array_fill(0, $size, -1);
$refined = array_fill(0, $size, false);
$labels = self::jpeg2000SignificanceLabels($kind);
$causal = ($block_style & self::JPEG2000_CAUSAL) > 0;
$terminate_all = ($block_style & self::JPEG2000_TERMINATE_ALL) > 0;
$starts = [0];
foreach ($block["segments"] as $segment) {
$starts[] = end($starts) + $segment;
}
$coder = new L\JBig2($block["bytes"], 0);
$coder->contexts = self::JPEG2000_FIRST_CONTEXTS;
$last_plane = $plane;
for ($pass = 0; $pass < $block["passes"]; $pass++) {
$at_plane = $plane - intdiv($pass + 2, 3);
if ($at_plane < 0) {
break;
}
if ($terminate_all && $pass > 0 && isset($starts[$pass])) {
$contexts = $coder->contexts;
$coder = new L\JBig2($block["bytes"], $starts[$pass]);
$coder->contexts = $contexts;
}
$last_plane = $at_plane;
$bit = 1 << $at_plane;
$pass_kind = ($pass == 0) ? 2 : ($pass - 1) % 3;
for ($stripe = 0; $stripe < $high; $stripe += 4) {
$stripe_end = min($stripe + 4, $high);
for ($column = 0; $column < $wide; $column++) {
$first = $stripe;
if ($pass_kind == 2 && $stripe_end - $stripe == 4) {
$quiet = true;
for ($row = $stripe; $row < $stripe_end && $quiet;
$row++) {
$i = ($row + 1) * $row_size + $column + 1;
$quiet = $significant[$i] == 0 &&
$visited[$i] != $at_plane &&
self::jpeg2000Neighbors($significant, $i,
$row_size, $causal && $row == $stripe + 3) ==
[0, 0, 0];
}
if ($quiet) {
if ($coder->readDecisionBit(
self::JPEG2000_RUN_CONTEXT) == 0) {
continue;
}
$first = $stripe + ($coder->readDecisionBit(
self::JPEG2000_UNIFORM_CONTEXT) << 1 |
$coder->readDecisionBit(
self::JPEG2000_UNIFORM_CONTEXT));
$i = ($first + 1) * $row_size + $column + 1;
$significant[$i] = 1;
$magnitude[$i] |= $bit;
$negative[$i] = self::jpeg2000Sign($coder,
$significant, $negative, $i, $row_size,
$causal && $first == $stripe + 3);
$first++;
}
}
for ($row = $first; $row < $stripe_end; $row++) {
$i = ($row + 1) * $row_size + $column + 1;
$edge = $causal && $row == $stripe + 3;
if ($pass_kind == 1) {
if ($significant[$i] == 0 ||
$visited[$i] == $at_plane) {
continue;
}
if ($refined[$i]) {
$context = self::JPEG2000_REFINE_LATER;
} else {
[$across, $up, $corners] =
self::jpeg2000Neighbors($significant,
$i, $row_size, $edge);
$context = ($across + $up + $corners > 0) ?
self::JPEG2000_REFINE_BUSY :
self::JPEG2000_REFINE_QUIET;
}
if ($coder->readDecisionBit($context) == 1) {
$magnitude[$i] |= $bit;
}
$refined[$i] = true;
continue;
}
if ($significant[$i] == 1 ||
$visited[$i] == $at_plane) {
continue;
}
[$across, $up, $corners] = self::jpeg2000Neighbors(
$significant, $i, $row_size, $edge);
if ($pass_kind == 0) {
if ($across + $up + $corners == 0) {
continue;
}
$visited[$i] = $at_plane;
}
if ($coder->readDecisionBit(
$labels[$across][$up][$corners]) == 1) {
$significant[$i] = 1;
$magnitude[$i] |= $bit;
$negative[$i] = self::jpeg2000Sign($coder,
$significant, $negative, $i, $row_size,
$edge);
}
}
}
}
if ($pass_kind == 2 &&
($block_style & self::JPEG2000_SEGMENT_MARK) > 0) {
for ($i = 0; $i < 4; $i++) {
$coder->readDecisionBit(self::JPEG2000_UNIFORM_CONTEXT);
}
}
if (($block_style & self::JPEG2000_RESET) > 0) {
$coder->contexts = self::JPEG2000_FIRST_CONTEXTS;
}
}
$middle = $reversible ? (($last_plane > 0) ?
1 << ($last_plane - 1) : 0) : (1 << $last_plane) / 2;
for ($row = 0; $row < $high; $row++) {
for ($column = 0; $column < $wide; $column++) {
$i = ($row + 1) * $row_size + $column + 1;
if ($magnitude[$i] == 0) {
continue;
}
$value = ($magnitude[$i] + $middle) * $step;
$values[$row * $wide + $column] = ($negative[$i] == 1) ?
-$value : $value;
}
}
return $values;
}
/**
* jpeg2000Neighbors counts the significant neighbors of a value: the
* two beside it, the two above and below it, and the four at its
* corners. In vertically causal coding, the row below a stripe is not
* looked at from the stripe's last row.
*
* @param array $significant which values are significant, with a border
* @param int $i where the value is
* @param int $row_size how long a row is, border included
* @param bool $edge true to leave out the row below
* @return array the counts beside, above and below, and at the corners
*/
public static function jpeg2000Neighbors($significant, $i, $row_size,
$edge)
{
$above = $i - $row_size;
$below = $i + $row_size;
$across = $significant[$i - 1] + $significant[$i + 1];
if ($edge) {
return [$across, $significant[$above],
$significant[$above - 1] + $significant[$above + 1]];
}
return [$across, $significant[$above] + $significant[$below],
$significant[$above - 1] + $significant[$above + 1] +
$significant[$below - 1] + $significant[$below + 1]];
}
/**
* jpeg2000Sign reads the sign of a value that has just turned
* significant. The context is chosen from the signs of the significant
* neighbors beside it and above and below it, and the bit read is
* turned over where the neighbors lean negative.
*
* @param object $coder the MQ coder
* @param array $significant which values are significant
* @param array $negative which significant values are negative
* @param int $i where the value is
* @param int $row_size how long a row is, border included
* @param bool $edge true to leave out the row below
* @return int 1 for negative, 0 for positive
*/
public static function jpeg2000Sign($coder, $significant, $negative, $i,
$row_size, $edge)
{
$lean = fn($at) => $significant[$at] * (1 - 2 * $negative[$at]);
$across = max(-1, min(1, $lean($i - 1) + $lean($i + 1)));
$up = max(-1, min(1, $lean($i - $row_size) +
($edge ? 0 : $lean($i + $row_size))));
[$context, $flip] = self::JPEG2000_SIGN_CONTEXTS[$across + 1][$up +
1];
return $coder->readDecisionBit($context) ^ $flip;
}
/**
* jpeg2000SignificanceLabels gives the context for reading if a value
* turns significant, from how many of its neighbors beside it
* (0 to 2), above and below it (0 to 2) and at its corners (0 to 4) are
* significant. The table depends on the subband: HL turns beside and
* above round, since its detail runs the other way, and HH counts the
* corners first.
*
* @param int $kind the subband, 0 for LL, 1 for HL, 2 for LH, 3 for HH
* @return array the contexts, by count beside, above and below, and at
* the corners
*/
public static function jpeg2000SignificanceLabels($kind)
{
$labels = [];
for ($beside = 0; $beside <= 2; $beside++) {
for ($over = 0; $over <= 2; $over++) {
for ($corners = 0; $corners <= 4; $corners++) {
[$first, $second] = ($kind == 1) ? [$over, $beside] :
[$beside, $over];
if ($kind == 3) {
$sides = $beside + $over;
$label = ($corners >= 3) ? 8 : (($corners == 2) ?
(($sides >= 1) ? 7 : 6) : (($corners == 1) ?
(($sides >= 2) ? 5 : (($sides == 1) ? 4 : 3)) :
(($sides >= 2) ? 2 : $sides)));
} else if ($first == 2) {
$label = 8;
} else if ($first == 1) {
$label = ($second >= 1) ? 7 : (($corners >= 1) ? 6 :
5);
} else {
$label = ($second == 2) ? 4 : (($second == 1) ? 3 :
(($corners >= 2) ? 2 : $corners));
}
$labels[$beside][$over][$corners] = $label;
}
}
}
return $labels;
}
/**
* jpeg2000Combine undoes one level of the wavelet (small wave) that
* split the picture into a half size picture and its detail. The LL
* image of the
* level below and the HL, LH and HH subbands are interleaved, low values
* at the even places of the grid and high values at the odd ones, and
* each row and then each column is run through the synthesis filter.
*
* @param array $low the LL image, as width, height and values
* @param array $subbands the HL, LH and HH subbands, by kind
* @param array $edges the resolution's left, top, right and bottom
* @param bool $reversible whether the lossless 5/3 filter was used in
* place of the 9/7 one
* @return array the resolution's image
*/
public static function jpeg2000Combine($low, $subbands, $edges,
$reversible)
{
[$left, $top, $right, $bottom] = $edges;
$wide = max(0, $right - $left);
$high = max(0, $bottom - $top);
$values = array_fill(0, max(1, $wide * $high), 0.0);
$low_left = (int)ceil($left / 2);
$low_top = (int)ceil($top / 2);
$high_left = (int)ceil(($left - 1) / 2);
$high_top = (int)ceil(($top - 1) / 2);
for ($row = 0; $row < $high; $row++) {
$grid_row = $top + $row;
$odd_row = $grid_row & 1;
for ($column = 0; $column < $wide; $column++) {
$grid_column = $left + $column;
$odd_column = $grid_column & 1;
$kind = $odd_column + 2 * $odd_row;
$source = ($kind == 0) ? $low : $subbands[$kind];
$source_column = ($odd_column ?
($grid_column - 1) / 2 - $high_left :
$grid_column / 2 - $low_left);
$source_row = ($odd_row ? ($grid_row - 1) / 2 - $high_top :
$grid_row / 2 - $low_top);
if ($source_column >= 0 && $source_row >= 0 &&
$source_column < $source["wide"] &&
$source_row < $source["high"]) {
$values[$row * $wide + $column] = $source["values"][
$source_row * $source["wide"] + $source_column];
}
}
}
for ($row = 0; $row < $high; $row++) {
$line = array_slice($values, $row * $wide, $wide);
$line = self::jpeg2000Synthesize($line, $left, $reversible);
array_splice($values, $row * $wide, $wide, $line);
}
for ($column = 0; $column < $wide; $column++) {
$line = [];
for ($row = 0; $row < $high; $row++) {
$line[] = $values[$row * $wide + $column];
}
$line = self::jpeg2000Synthesize($line, $top, $reversible);
for ($row = 0; $row < $high; $row++) {
$values[$row * $wide + $column] = $line[$row];
}
}
return ["wide" => $wide, "high" => $high, "values" => $values];
}
/**
* jpeg2000Synthesize runs one line of low and high values, taking turns,
* through the wavelet's synthesis filter, by lifting (adding to each
* value a weighted sum of its two neighbors). The line is first
* extended past each end by mirroring it, without repeating the end
* value, so the filter has neighbors to read. Whether a place is even
* is judged by its place on the grid, so a line that starts at an odd
* place starts with a high value.
*
* @param array $line the interleaved values
* @param int $start the grid place of the line's first value
* @param bool $reversible true for the lossless 5/3 filter, false for
* the 9/7 one
* @return array the line's values
*/
public static function jpeg2000Synthesize($line, $start, $reversible)
{
$count = count($line);
if ($count == 1) {
if (($start & 1) == 1) {
$line[0] = $reversible ? intdiv((int)$line[0], 2) :
$line[0] / 2;
}
return $line;
}
$pad = $reversible ? 2 : 4;
$period = 2 * ($count - 1);
$extended = [];
for ($i = -$pad; $i < $count + $pad; $i++) {
$mirror = (($i % $period) + $period) % $period;
if ($mirror >= $count) {
$mirror = $period - $mirror;
}
$extended[] = $line[$mirror];
}
$length = count($extended);
$even_first = (($start - $pad) & 1) == 0 ? 0 : 1;
if ($reversible) {
for ($i = 1 + (1 - $even_first) % 2; $i < $length - 1; $i += 2) {
$extended[$i] -= floor(($extended[$i - 1] +
$extended[$i + 1] + 2) / 4);
}
for ($i = 1 + $even_first; $i < $length - 1; $i += 2) {
$extended[$i] += floor(($extended[$i - 1] +
$extended[$i + 1]) / 2);
}
} else {
for ($i = 0; $i < $length; $i++) {
$is_even = (($i + $even_first) & 1) == 0;
$extended[$i] *= $is_even ? self::WAVELET_SCALE :
1 / self::WAVELET_SCALE;
}
foreach ([[0, self::WAVELET_DELTA], [1, self::WAVELET_GAMMA],
[0, self::WAVELET_BETA], [1, self::WAVELET_ALPHA]] as
[$odd, $weight]) {
$first = (($even_first + $odd) & 1) == 0 ? 2 : 1;
for ($i = $first; $i < $length - 1; $i += 2) {
$extended[$i] -= $weight * ($extended[$i - 1] +
$extended[$i + 1]);
}
}
}
return array_slice($extended, $pad, $count);
}
}