SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ tests / JBig2Test.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\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);
            }
        }
    }
}