/ tests / CcittFaxTest.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\CcittFax;
use seekquarry\yioop\library\UnitTest;

/**
 * Checks that an image coded the way a fax machine codes one comes back
 * as the picture it started as.
 *
 * The coding writes each row as the differences from the row above it, so
 * one row read wrongly makes every row after it wrong. A decoder can
 * therefore look right on the first few rows of a picture and drift into
 * nonsense further down, which is what happened here: a magazine's
 * lettering read correctly at the top and filled in solid halfway down.
 * These cases code small pictures by hand, decode them, and compare row
 * by row, so a drift shows up as a mismatched row rather than having to
 * be spotted by eye in a thumbnail.
 *
 * @author Chris Pollett
 */
class CcittFaxTest 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()
    {
    }
    /**
     * Codes a picture the way a fax machine codes one, so a decoding can
     * be checked against the picture it began as. Each row is written as
     * the differences from the row above, using vertical mode where a run
     * ends near where the run above it ended and horizontal mode
     * otherwise.
     *
     * @param array $rows the picture, each row a list of 0 for black and
     *      1 for white
     * @return string the coded picture's bytes
     */
    public function codeRows($rows)
    {
        $bits = "";
        $wide = empty($rows) ? 0 : count($rows[0]);
        $above = $this->changesIn(array_fill(0, $wide, 1));
        foreach ($rows as $row) {
            $bits .= $this->codeRow($this->changesIn($row), $above, $wide);
            $above = $this->changesIn($row);
        }
        $bits .= str_repeat("0", (8 - (strlen($bits) % 8)) % 8);
        $bytes = "";
        for ($at = 0; $at < strlen($bits); $at += 8) {
            $bytes .= chr(bindec(substr($bits, $at, 8)));
        }
        return $bytes;
    }
    /**
     * Gives the places along a row where its color changes, which is how
     * the coding describes a row.
     *
     * @param array $row the row, 0 for black and 1 for white
     * @return array where the color changes, ending at the row's width
     */
    public function changesIn($row)
    {
        $changes = [];
        $wide = count($row);
        $white = true;
        for ($at = 0; $at < $wide; $at++) {
            $is_white = ($row[$at] == 1);
            if ($is_white != $white) {
                $changes[] = $at;
                $white = $is_white;
            }
        }
        $changes[] = $wide;
        $changes[] = $wide;
        return $changes;
    }
    /**
     * Codes one row against the row above it, writing each run in
     * vertical mode where it can and horizontal mode where it cannot.
     *
     * @param array $changes where this row's color changes
     * @param array $above where the row above changes
     * @param int $wide how many pixels across the row is
     * @return string the row's bits
     */
    public function codeRow($changes, $above, $wide)
    {
        $bits = "";
        $place = 0;
        /* The coding measures from an imaginary white pixel just before
           the row, so a run ending at the very first pixel is still to
           the right of where the reading began. */
        $behind = -1;
        $white = true;
        $step = 0;
        while ($place < $wide) {
            $ends = $changes[$step] ?? $wide;
            $first = $this->firstChangeAbove($above, $behind, $white,
                $wide);
            $offset = $ends - $first;
            if ($offset >= -3 && $offset <= 3) {
                $bits .= $this->verticalBits($offset);
                $place = $ends;
                $behind = $place;
                $white = !$white;
                $step++;
                continue;
            }
            /* Horizontal mode writes this run and the one after it
               outright, so two runs are consumed at once. */
            $next = $changes[$step + 1] ?? $wide;
            $bits .= "001" . $this->runBits($ends - $place, $white) .
                $this->runBits($next - $ends, !$white);
            $place = $next;
            $behind = $place;
            $step += 2;
        }
        return $bits;
    }
    /**
     * Gives where the row above next changes, worked out here rather than
     * asked of the decoder, so that a wrong idea of it cannot be shared
     * between the coding and the decoding and pass unnoticed.
     *
     * The rule is the one the coding is defined by: the first place the
     * row above changes that lies past where this row has got to, and
     * that changes to the color opposite the one being written. The row
     * above is described as a list of places where it changes, the first
     * of them a change from white to black.
     *
     * @param array $above where the row above changes
     * @param int $behind where the run being written began, minus one
     *      at the start of a row
     * @param bool $white whether the run being written is white
     * @param int $wide how many pixels across the row is
     * @return int where the row above changes
     */
    public function firstChangeAbove($above, $behind, $white, $wide)
    {
        foreach ($above as $step => $change) {
            $to_black = (($step % 2) == 0);
            if ($change > $behind && $to_black == $white) {
                return $change;
            }
        }
        return $wide;
    }
    /**
     * Gives the bits that say a run ends a given distance from where the
     * run above it ended.
     *
     * @param int $offset how far, from minus three to three
     * @return string the bits
     */
    public function verticalBits($offset)
    {
        $said = ["0" => "1", "1" => "011", "-1" => "010", "2" => "000011",
            "-2" => "000010", "3" => "0000011", "-3" => "0000010"];
        return $said[(string)$offset];
    }
    /**
     * Gives the bits that write a run length outright, in the color's own
     * table.
     *
     * @param int $length how many pixels the run covers
     * @param bool $white whether the run is white
     * @return string the bits
     */
    public function runBits($length, $white)
    {
        $codes = $white ? CcittFax::$white_codes : CcittFax::$black_codes;
        $bits = "";
        while ($length >= 64) {
            $most = 64 * (int)floor($length / 64);
            $found = "";
            foreach ($codes as $said => $covers) {
                if ($covers == $most) {
                    $found = $said;
                    break;
                }
            }
            if ($found === "") {
                $most = 64;
                $found = array_search(64, $codes);
            }
            $bits .= $found;
            $length -= $most;
        }
        return $bits . array_search($length, $codes);
    }
    /**
     * A picture of plain stripes comes back as the stripes it began as,
     * which is the simplest thing the coding has to get right.
     */
    public function stripesComeBackTestCase()
    {
        $rows = [];
        for ($down = 0; $down < 8; $down++) {
            $row = [];
            for ($across = 0; $across < 16; $across++) {
                $row[] = ($across >= 4 && $across < 12) ? 0 : 1;
            }
            $rows[] = $row;
        }
        $read = CcittFax::decode($this->codeRows($rows), 16, 8);
        $this->assertEqual(8, count($read), "every row comes back");
        for ($down = 0; $down < 8; $down++) {
            $this->assertEqual(implode("", $rows[$down]),
                implode("", $read[$down] ?? []),
                "row " . $down . " is the row it began as");
        }
    }
    /**
     * A picture whose rows differ from one another comes back unchanged.
     * A decoder that loses the row above it reads correctly at the top
     * and drifts lower down, so every row is compared rather than the
     * first few.
     */
    public function changingRowsComeBackTestCase()
    {
        $rows = [];
        for ($down = 0; $down < 24; $down++) {
            $row = [];
            $begins = $down % 10;
            for ($across = 0; $across < 32; $across++) {
                $row[] = ($across >= $begins && $across < $begins + 7) ?
                    0 : 1;
            }
            $rows[] = $row;
        }
        $read = CcittFax::decode($this->codeRows($rows), 32, 24);
        $this->assertEqual(24, count($read), "every row comes back");
        $wrong = 0;
        foreach ($rows as $down => $row) {
            if (implode("", $row) !== implode("", $read[$down] ?? [])) {
                $wrong++;
            }
        }
        $this->assertEqual(0, $wrong, "no row drifts from what it was");
    }
    /**
     * A row that is wholly one color comes back that color, which the
     * coding writes as a single run and a decoder can easily turn over.
     */
    public function wholeRowsComeBackTestCase()
    {
        $rows = [array_fill(0, 20, 1), array_fill(0, 20, 0),
            array_fill(0, 20, 1)];
        $read = CcittFax::decode($this->codeRows($rows), 20, 3);
        $this->assertEqual(3, count($read), "every row comes back");
        foreach ($rows as $down => $row) {
            $this->assertEqual(implode("", $row),
                implode("", $read[$down] ?? []),
                "row " . $down . " is the row it began as");
        }
    }
    /**
     * A picture of several separate marks on a row comes back with all
     * of them, since a row of lettering is many small runs rather than
     * one.
     */
    public function severalMarksOnARowComeBackTestCase()
    {
        $rows = [];
        for ($down = 0; $down < 12; $down++) {
            $row = array_fill(0, 40, 1);
            foreach ([3, 11, 19, 27, 35] as $at) {
                $row[$at + ($down % 2)] = 0;
                $row[$at + ($down % 2) + 1] = 0;
            }
            $rows[] = $row;
        }
        $read = CcittFax::decode($this->codeRows($rows), 40, 12);
        $wrong = 0;
        foreach ($rows as $down => $row) {
            if (implode("", $row) !== implode("", $read[$down] ?? [])) {
                $wrong++;
            }
        }
        $this->assertEqual(0, $wrong, "no row drifts from what it was");
    }
    /**
     * A mark that vanishes from one row to the next comes back. Where a
     * run on the row above ends before the run being written does, the
     * coding uses pass mode, and a decoder that mishandles it loses its
     * place and every row after that is wrong.
     */
    public function vanishingMarksComeBackTestCase()
    {
        $rows = [];
        /* Two marks, then one wide mark covering both, then none: the
           middle row passes over the pair above it. */
        $shapes = [[[4, 8], [14, 18]], [[2, 22]], [], [[10, 12]], []];
        for ($step = 0; $step < 5; $step++) {
            for ($again = 0; $again < 3; $again++) {
                $row = array_fill(0, 30, 1);
                foreach ($shapes[$step] as $mark) {
                    for ($at = $mark[0]; $at < $mark[1]; $at++) {
                        $row[$at] = 0;
                    }
                }
                $rows[] = $row;
            }
        }
        $read = CcittFax::decode($this->codeRows($rows), 30, count($rows));
        $wrong = 0;
        foreach ($rows as $down => $row) {
            if (implode("", $row) !== implode("", $read[$down] ?? [])) {
                $wrong++;
            }
        }
        $this->assertEqual(0, $wrong, "no row drifts from what it was");
    }
    /**
     * Ink stays a small share of a picture of lettering. A decoder that
     * loses its place fills rows in solid, so the share of ink rising
     * far above what was coded is the shape that fault takes.
     */
    public function inkStaysWhatItWasTestCase()
    {
        $rows = [];
        for ($down = 0; $down < 40; $down++) {
            $row = array_fill(0, 60, 1);
            $begins = 5 + ($down % 7);
            for ($across = $begins; $across < $begins + 6; $across++) {
                $row[$across] = 0;
            }
            for ($across = 40; $across < 44; $across++) {
                $row[$across] = 0;
            }
            $rows[] = $row;
        }
        $read = CcittFax::decode($this->codeRows($rows), 60, 40);
        $most = 0;
        foreach ($read as $row) {
            $black = 0;
            foreach ($row as $one) {
                if ($one == 0) {
                    $black++;
                }
            }
            $most = max($most, $black);
        }
        $this->assertTrue($most <= 12,
            "no row holds more ink than was coded into it");
    }
}
X