<?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;
/**
* Decodes a black-and-white image coded the way a fax machine codes one,
* giving back one byte per pixel: 0 for black, 1 for white.
*
* This coding is defined by ITU-T Recommendations T.4 (Group 3) and T.6
* (Group 4), written for sending a page over a telephone line. It is met
* in two places a crawler cares about: a PDF stream whose Filter is
* CCITTFaxDecode, which is how a magazine cover's lettering is often
* carried as a stencil, and a TIFF whose Compression tag is 3 or 4.
*
* The coding writes each row as the differences from the row above it,
* since two neighboring rows of a page are usually nearly the same. A row
* is a list of runs of one color, and each run is written in one of three
* ways: vertical mode, where the run ends within three pixels of where the
* one above it ended; pass mode, where the run above ends before this one
* does; and horizontal mode, where two run lengths are written outright
* using the code tables T.4 gives for white and black runs.
*
* What is handled and what is not:
* - Two-dimensional coding with no end-of-line markers, which is what
* K less than zero means and is what a PDF almost always carries.
* - The extended run lengths above 1728 pixels, so a wide image decodes.
* - It does NOT handle: one-dimensional rows (K of zero), mixed coding
* (K above zero), byte-aligned rows, or uncompressed mode. A stream
* using those returns nothing rather than a wrong picture.
*
* Typical use:
* $rows = CcittFax::decode($bytes, $width, $height, $black_is_one);
* // $rows[$y][$x] is 0 or 1
* Nothing comes back where the stream cannot be followed, so a caller
* should check for an empty result rather than assume a picture.
*
* @author Chris Pollett
*/
class CcittFax
{
/**
* How far to either side of the run above a vertical-mode run may
* end
* @var int
*/
const VERTICAL_REACH = 3;
/**
* How many rows may be read before the decoder gives up, so a stream
* that does not end cannot run forever
* @var int
*/
const MOST_ROWS = 20000;
/**
* The run lengths white runs are written with, by the code that
* stands for each. A code is given as its length in bits and its
* value, since a code of three zero bits is not the same as one.
* @var array
*/
public static $white_codes = [
"00110101" => 0, "000111" => 1, "0111" => 2, "1000" => 3,
"1011" => 4, "1100" => 5, "1110" => 6, "1111" => 7,
"10011" => 8, "10100" => 9, "00111" => 10, "01000" => 11,
"001000" => 12, "000011" => 13, "110100" => 14, "110101" => 15,
"101010" => 16, "101011" => 17, "0100111" => 18, "0001100" => 19,
"0001000" => 20, "0010111" => 21, "0000011" => 22, "0000100" => 23,
"0101000" => 24, "0101011" => 25, "0010011" => 26, "0100100" => 27,
"0011000" => 28, "00000010" => 29, "00000011" => 30,
"00011010" => 31, "00011011" => 32, "00010010" => 33,
"00010011" => 34, "00010100" => 35, "00010101" => 36,
"00010110" => 37, "00010111" => 38, "00101000" => 39,
"00101001" => 40, "00101010" => 41, "00101011" => 42,
"00101100" => 43, "00101101" => 44, "00000100" => 45,
"00000101" => 46, "00001010" => 47, "00001011" => 48,
"01010010" => 49, "01010011" => 50, "01010100" => 51,
"01010101" => 52, "00100100" => 53, "00100101" => 54,
"01011000" => 55, "01011001" => 56, "01011010" => 57,
"01011011" => 58, "01001010" => 59, "01001011" => 60,
"00110010" => 61, "00110011" => 62, "00110100" => 63,
"11011" => 64, "10010" => 128, "010111" => 192, "0110111" => 256,
"00110110" => 320, "00110111" => 384, "01100100" => 448,
"01100101" => 512, "01101000" => 576, "01100111" => 640,
"011001100" => 704, "011001101" => 768, "011010010" => 832,
"011010011" => 896, "011010100" => 960, "011010101" => 1024,
"011010110" => 1088, "011010111" => 1152, "011011000" => 1216,
"011011001" => 1280, "011011010" => 1344, "011011011" => 1408,
"010011000" => 1472, "010011001" => 1536, "010011010" => 1600,
"011000" => 1664, "010011011" => 1728];
/**
* The run lengths black runs are written with, by the code that
* stands for each
* @var array
*/
public static $black_codes = [
"0000110111" => 0, "010" => 1, "11" => 2, "10" => 3, "011" => 4,
"0011" => 5, "0010" => 6, "00011" => 7, "000101" => 8,
"000100" => 9, "0000100" => 10, "0000101" => 11, "0000111" => 12,
"00000100" => 13, "00000111" => 14, "000011000" => 15,
"0000010111" => 16, "0000011000" => 17, "0000001000" => 18,
"00001100111" => 19, "00001101000" => 20, "00001101100" => 21,
"00000110111" => 22, "00000101000" => 23, "00000010111" => 24,
"00000011000" => 25, "000011001010" => 26, "000011001011" => 27,
"000011001100" => 28, "000011001101" => 29, "000001101000" => 30,
"000001101001" => 31, "000001101010" => 32, "000001101011" => 33,
"000011010010" => 34, "000011010011" => 35, "000011010100" => 36,
"000011010101" => 37, "000011010110" => 38, "000011010111" => 39,
"000001101100" => 40, "000001101101" => 41, "000011011010" => 42,
"000011011011" => 43, "000001010100" => 44, "000001010101" => 45,
"000001010110" => 46, "000001010111" => 47, "000001100100" => 48,
"000001100101" => 49, "000001010010" => 50, "000001010011" => 51,
"000000100100" => 52, "000000110111" => 53, "000000111000" => 54,
"000000100111" => 55, "000000101000" => 56, "000001011000" => 57,
"000001011001" => 58, "000000101011" => 59, "000000101100" => 60,
"000001011010" => 61, "000001100110" => 62, "000001100111" => 63,
"0000001111" => 64, "000011001000" => 128, "000011001001" => 192,
"000001011011" => 256, "000000110011" => 320,
"000000110100" => 384, "000000110101" => 448,
"0000001101100" => 512, "0000001101101" => 576,
"0000001001010" => 640, "0000001001011" => 704,
"0000001001100" => 768, "0000001001101" => 832,
"0000001110010" => 896, "0000001110011" => 960,
"0000001110100" => 1024, "0000001110101" => 1088,
"0000001110110" => 1152, "0000001110111" => 1216,
"0000001010010" => 1280, "0000001010011" => 1344,
"0000001010100" => 1408, "0000001010101" => 1472,
"0000001011010" => 1536, "0000001011011" => 1600,
"0000001100100" => 1664, "0000001100101" => 1728];
/**
* The run lengths both colors share for runs above 1728 pixels
* @var array
*/
public static $long_codes = [
"00000001000" => 1792, "00000001100" => 1856,
"00000001101" => 1920, "000000010010" => 1984,
"000000010011" => 2048, "000000010100" => 2112,
"000000010101" => 2176, "000000010110" => 2240,
"000000010111" => 2304, "000000011100" => 2368,
"000000011101" => 2432, "000000011110" => 2496,
"000000011111" => 2560];
/**
* Decodes an image, giving back one byte per pixel.
*
* @param string $bytes the coded image
* @param int $wide how many pixels across each row is
* @param int $high how many rows there are, or 0 to read until the
* stream runs out
* @param bool $black_is_one whether a one bit means black, which a
* PDF says with BlackIs1
* @return array each row as a list of 0 for black and 1 for white,
* empty where the stream cannot be followed
*/
public static function decode($bytes, $wide, $high,
$black_is_one = false)
{
if ($wide < 1) {
return [];
}
$bits = self::bitsOfBytes($bytes);
$length = strlen($bits);
$at = 0;
/* The row above the first is taken to be all white, which is what
the coding assumes for the first row of a page. */
$above = [$wide, $wide];
$rows = [];
$most = ($high > 0) ? $high : self::MOST_ROWS;
while (count($rows) < $most && $at < $length) {
$changes = self::decodeRow($bits, $at, $wide, $above);
if ($changes === false) {
break;
}
$rows[] = self::rowPixelsFromChanges($changes, $wide,
$black_is_one);
$above = $changes;
}
return $rows;
}
/**
* Turns the bytes of a coded image into a string of ones and zeroes,
* which is how the codes are matched.
*
* @param string $bytes the coded image
* @return string the bits
*/
public static function bitsOfBytes($bytes)
{
$bits = "";
$length = strlen($bytes);
for ($at = 0; $at < $length; $at++) {
$bits .= str_pad(decbin(ord($bytes[$at])), 8, "0",
STR_PAD_LEFT);
}
return $bits;
}
/**
* Decodes one row, giving back the places along it where the color
* changes. A row is described against the row above it.
*
* @param string $bits the whole coded image as bits
* @param int &$at where in the bits this row begins, moved past it
* @param int $wide how many pixels across the row is
* @param array $above where the color changed on the row above
* @return mixed the places where the color changes, or false where
* the row cannot be followed
*/
public static function decodeRow($bits, &$at, $wide, $above)
{
$changes = [];
$place = 0;
/* The coding measures from an imaginary white pixel just before
the row rather than from its first pixel, so a run that ends at
the very start of the row is still to the right of where the
reading began. Starting at the first pixel instead skips such a
change and every run after it lands one boundary out. */
$behind = -1;
$white = true;
$length = strlen($bits);
while ($place < $wide) {
if ($at >= $length) {
return empty($changes) ? false :
self::closeRowChanges($changes, $wide);
}
$first = self::firstChangeOnReferenceLine($above, $behind,
$white, $wide);
$second = self::nextChangeOnReferenceLine($above, $first, $wide);
$mode = self::readCodingMode($bits, $at);
if ($mode === false) {
return empty($changes) ? false :
self::closeRowChanges($changes, $wide);
}
if ($mode === "pass") {
$place = $second;
$behind = $place;
continue;
}
if ($mode === "horizontal") {
$first_run = self::readRunLength($bits, $at, $white);
$second_run = self::readRunLength($bits, $at, !$white);
if ($first_run === false || $second_run === false) {
return empty($changes) ? false :
self::closeRowChanges($changes, $wide);
}
$place = min($wide, $place + $first_run);
$changes[] = $place;
$place = min($wide, $place + $second_run);
$changes[] = $place;
$behind = $place;
continue;
}
/* Vertical mode: the run ends within three pixels of where
the run above it ended, and the color turns over. */
$place = max(0, min($wide, $first + $mode));
$changes[] = $place;
$behind = $place;
$white = !$white;
}
return self::closeRowChanges($changes, $wide);
}
/**
* Rounds off a row's changes so every row ends at its full width,
* which the next row is described against.
*
* @param array $changes where the color changed
* @param int $wide how many pixels across the row is
* @return array the changes, ending at the row's width
*/
public static function closeRowChanges($changes, $wide)
{
$changes[] = $wide;
$changes[] = $wide;
return $changes;
}
/**
* Reads which of the three ways the next run is written.
*
* @param string $bits the coded image as bits
* @param int &$at where to read from, moved past what was read
* @return mixed "pass", "horizontal", a number from minus three to
* three for vertical mode, or false where the bits say none of
* them
*/
public static function readCodingMode($bits, &$at)
{
$modes = ["1" => 0, "011" => 1, "010" => -1, "001" => "horizontal",
"0001" => "pass", "000011" => 2, "000010" => -2,
"0000011" => 3, "0000010" => -3];
foreach ([1, 3, 4, 6, 7] as $wide) {
$said = substr($bits, $at, $wide);
if (strlen($said) < $wide) {
return false;
}
if (isset($modes[$said])) {
$at += $wide;
return $modes[$said];
}
}
return false;
}
/**
* Reads one run length, which may be written as a long run followed
* by a short one.
*
* @param string $bits the coded image as bits
* @param int &$at where to read from, moved past what was read
* @param bool $white whether this is a run of white
* @return mixed how many pixels the run covers, or false where the
* bits say no run
*/
public static function readRunLength($bits, &$at, $white)
{
$total = 0;
$steps = 0;
while ($steps < 64) {
$steps++;
$codes = $white ? self::$white_codes : self::$black_codes;
$run = self::readOneRunCode($bits, $at, $codes);
if ($run === false) {
return false;
}
$total += $run;
/* A run of 64 or more is written as a multiple of 64 and then
the rest, so reading goes on until a short run ends it. */
if ($run < 64) {
return $total;
}
}
return $total;
}
/**
* Reads one code, whether from a color's own table or from the table
* both colors share for long runs.
*
* @param string $bits the coded image as bits
* @param int &$at where to read from, moved past what was read
* @param array $codes the color's own table
* @return mixed how many pixels the code stands for, or false
*/
public static function readOneRunCode($bits, &$at, $codes)
{
for ($wide = 2; $wide <= 14; $wide++) {
$said = substr($bits, $at, $wide);
if (strlen($said) < $wide) {
return false;
}
if (isset($codes[$said])) {
$at += $wide;
return $codes[$said];
}
if (isset(self::$long_codes[$said])) {
$at += $wide;
return self::$long_codes[$said];
}
}
return false;
}
/**
* Gives where the row above next changes to the color opposite the
* one being written, which vertical and pass mode are measured from.
*
* @param array $above where the color changed on the row above
* @param int $behind where the run being written began, which is
* 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 static function firstChangeOnReferenceLine($above, $behind,
$white, $wide)
{
$count = count($above);
for ($step = 0; $step < $count; $step++) {
/* The changes above alternate in color, the first of them a
change to black, so which of them can end this run depends
on the color being written. */
$to_black = (($step % 2) == 0);
if ($above[$step] > $behind && $to_black == $white) {
return $above[$step];
}
}
return $wide;
}
/**
* Gives the change on the row above that follows a given one, which
* pass mode moves to.
*
* @param array $above where the color changed on the row above
* @param int $first the change already found
* @param int $wide how many pixels across the row is
* @return int the change after it
*/
public static function nextChangeOnReferenceLine($above, $first, $wide)
{
foreach ($above as $change) {
if ($change > $first) {
return $change;
}
}
return $wide;
}
/**
* Turns a row's changes into one byte per pixel.
*
* @param array $changes where the color changed along the row
* @param int $wide how many pixels across the row is
* @param bool $black_is_one whether a one bit means black
* @return array the row, 0 for black and 1 for white
*/
public static function rowPixelsFromChanges($changes, $wide, $black_is_one)
{
$row = array_fill(0, $wide, $black_is_one ? 0 : 1);
$white = true;
$place = 0;
foreach ($changes as $change) {
$change = max(0, min($wide, $change));
if (!$white) {
for ($step = $place; $step < $change; $step++) {
$row[$step] = $black_is_one ? 1 : 0;
}
}
$place = $change;
$white = !$white;
if ($place >= $wide) {
break;
}
}
return $row;
}
}