<?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\tests;
use seekquarry\yioop\library\JBig2;
use seekquarry\yioop\library\UnitTest;
/**
* Checks that a picture coded the way scanned text is coded comes back
* as the picture it started as.
*
* Each case codes a small picture here with the arithmetic coder of T.88,
* guessing each pixel from the same pixels around it that the decoder
* looks at, then decodes it and compares row by row. The decoder keeps
* the pixels around the one it reads as runs that slide along the row
* when its template lies the way the standard puts it, and looks each
* pixel up when a region has moved its adaptive pixels, so both ways are
* coded here.
*
* @author Chris Pollett
*/
class JBig2Test extends UnitTest
{
/**
* Sets up nothing; each case builds the little picture it needs.
*/
public function setUp()
{
}
/**
* Tears down nothing; no case leaves anything behind.
*/
public function tearDown()
{
}
/**
* pictureToCode makes a picture with marks of several sizes and with
* rows that repeat the row above, so that typical prediction has rows
* to skip.
*
* @param int $wide how many pixels across
* @param int $high how many rows
* @return array each row as a list of 1 for ink and 0 for blank
*/
public function pictureToCode($wide, $high)
{
$rows = [];
for ($down = 0; $down < $high; $down++) {
if ($down % 5 == 3) {
$rows[] = $rows[$down - 1];
continue;
}
$row = [];
for ($across = 0; $across < $wide; $across++) {
$row[] = (($across * $across + 3 * $down) % 7 == 0 ||
($across > 10 && $across < 18 && $down > 4)) ? 1 : 0;
}
$rows[] = $row;
}
return $rows;
}
/**
* codeRegion codes a picture as a generic region with the arithmetic
* coder T.88 gives, the counterpart of what JBig2::readGenericRegion
* reads. Each pixel's guess is strung together from the pixels the
* template names, in the template's order, and a row the same as the
* one above is skipped when typical prediction is on.
*
* @param array $rows the picture, each row a list of 1 for ink and 0
* for blank
* @param int $template which set of surrounding pixels to look at
* @param array $adaptive the surrounding pixels the region moved
* @param bool $typical whether a row may say it repeats the one above
* @return string the coded bytes
*/
public function codeRegion($rows, $template, $adaptive, $typical)
{
$places = JBig2::templateOffsets($template, $adaptive);
$wide = count($rows[0]);
$coder = ["range" => 0x8000, "sum" => 0, "count" => 12,
"out" => [0], "states" => [], "likely" => []];
$same = 0;
foreach ($rows as $down => $row) {
if ($typical) {
$repeats = ($down > 0 && $row === $rows[$down - 1]) ? 1 : 0;
$this->codeDecision($coder,
[0x9b25, 0x0795, 0x00e5, 0x0195][$template],
$repeats ^ $same);
$same = $repeats;
if ($repeats == 1) {
continue;
}
}
for ($across = 0; $across < $wide; $across++) {
$which = 0;
foreach ($places as $place) {
$look_down = $down + $place[1];
$look_across = $across + $place[0];
$which = ($which << 1) | (($look_down < 0 ||
$look_across < 0 || $look_across >= $wide) ? 0 :
($rows[$look_down][$look_across] ?? 0));
}
$this->codeDecision($coder, $which, $row[$across]);
}
}
$total = $coder["sum"] + $coder["range"];
$coder["sum"] |= 0xffff;
if ($coder["sum"] >= $total) {
$coder["sum"] -= 0x8000;
}
$coder["sum"] <<= $coder["count"];
$this->codeByteOut($coder);
$coder["sum"] <<= $coder["count"];
$this->codeByteOut($coder);
array_shift($coder["out"]);
return implode("", array_map("chr", $coder["out"])) . "\xff\xac";
}
/**
* codeDecision codes one answer under one guess, the counterpart of
* JBig2::readDecisionBit, moving the guess to its next state as T.88
* says.
*
* @param array &$coder the coder's range, sum, bits left, bytes out
* and guesses
* @param int $which which guess
* @param int $answer the answer to code, 0 or 1
*/
public function codeDecision(&$coder, $which, $answer)
{
$state = $coder["states"][$which] ?? 0;
$likely = $coder["likely"][$which] ?? 0;
$chance = JBig2::STATES[$state][0];
$coder["range"] -= $chance;
if ($answer == $likely) {
if (($coder["range"] & 0x8000) != 0) {
$coder["sum"] += $chance;
return;
}
if ($coder["range"] < $chance) {
$coder["range"] = $chance;
} else {
$coder["sum"] += $chance;
}
$coder["states"][$which] = JBig2::STATES[$state][1];
} else {
if ($coder["range"] < $chance) {
$coder["sum"] += $chance;
} else {
$coder["range"] = $chance;
}
if (JBig2::STATES[$state][3] == 1) {
$coder["likely"][$which] = 1 - $likely;
}
$coder["states"][$which] = JBig2::STATES[$state][2];
}
do {
$coder["range"] <<= 1;
$coder["sum"] <<= 1;
$coder["count"]--;
if ($coder["count"] == 0) {
$this->codeByteOut($coder);
}
} while (($coder["range"] & 0x8000) == 0);
}
/**
* codeByteOut moves the top of the coder's sum out as a byte, carrying
* into the byte before it where the sum overflowed, and leaving room
* after a 0xff byte as T.88 says.
*
* @param array &$coder the coder's range, sum, bits left, bytes out
* and guesses
*/
public function codeByteOut(&$coder)
{
$last = count($coder["out"]) - 1;
if ($coder["out"][$last] != 0xff && $coder["sum"] >= 0x8000000) {
$coder["out"][$last]++;
$coder["sum"] &= 0x7ffffff;
}
if ($coder["out"][$last] == 0xff) {
$coder["out"][] = $coder["sum"] >> 20;
$coder["sum"] &= 0xfffff;
$coder["count"] = 7;
} else {
$coder["out"][] = $coder["sum"] >> 19;
$coder["sum"] &= 0x7ffff;
$coder["count"] = 8;
}
}
/**
* roundTrip codes a picture, decodes it and checks every row.
*
* @param int $template which set of surrounding pixels to look at
* @param array $adaptive the surrounding pixels the region moved
* @param bool $typical whether a row may say it repeats the one above
* @param string $what what the picture is, for the messages
*/
public function roundTrip($template, $adaptive, $typical, $what)
{
$rows = $this->pictureToCode(41, 23);
$bytes = $this->codeRegion($rows, $template, $adaptive, $typical);
$reader = new JBig2($bytes, 0);
$read = $reader->readGenericRegion(41, 23, $template, $adaptive,
$typical);
$this->assertEqual(23, count($read), "$what: every row comes back");
foreach ($rows as $down => $row) {
$this->assertEqual(strtr(implode("", $row), "01", "10"),
$read[$down] ?? "", "$what: row $down is the row it began as");
}
}
/**
* A picture comes back under each of the four templates with its
* adaptive pixels where the standard puts them, with and without
* typical prediction, which is the way the pixels around each one
* slide along the row.
*/
public function standardTemplatesComeBackTestCase()
{
$standard = [[[3, -1], [-3, -1], [2, -2], [-2, -2]], [[3, -1]],
[[2, -1]], [[2, -1]]];
foreach ($standard as $template => $adaptive) {
foreach ([false, true] as $typical) {
$this->roundTrip($template, $adaptive, $typical,
"template $template, typical " . (int)$typical);
}
}
}
/**
* A picture comes back when the region has moved its adaptive pixels,
* including to a row further up than the template reaches and to the
* left on the row being read, which is the way each pixel around the
* one being read is looked up.
*/
public function movedAdaptivePixelsComeBackTestCase()
{
$moved = [[0, [[-1, -4], [5, -1], [0, -3], [-7, 0]]],
[1, [[-5, -3]]], [2, [[1, -6]]], [3, [[-6, 0]]]];
foreach ($moved as list($template, $adaptive)) {
foreach ([false, true] as $typical) {
$this->roundTrip($template, $adaptive, $typical,
"template $template moved, typical " . (int)$typical);
}
}
}
}