/ src / library / QrCode.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\library;

/**
 * Draws the square black-and-white symbol a phone camera reads, from a
 * piece of text, without any program outside Yioop.
 *
 * Yioop used to ask the qrencode command to make these, which meant a site
 * that wanted them had to install it and say where it was. Everything the
 * symbol needs is arithmetic, so it is done here instead, and the same
 * arithmetic is written again in scripts/qr.js so a page being written can
 * show its symbol before it is saved.
 *
 * The text is carried in the eight-bit mode, which takes any bytes, at the
 * middle of the four levels of damage the symbol can survive. Sizes one
 * through nine are made, holding up to 182 bytes, which covers an address
 * or a short note; longer text is refused rather than drawn wrongly.
 */
class QrCode
{
    /**
     * The polynomial the arithmetic of this symbol is done over. Every
     * multiplication of two values wraps by this when it overflows a byte.
     */
    const FIELD_POLYNOMIAL = 0x11D;
    /**
     * What each power of the generator comes to.
     * @var array
     */
    public static $powers = null;
    /**
     * Which power of the generator each value is.
     * @var array
     */
    public static $logs = null;
    /**
     * How many bytes of the message each size holds, at the middle level
     * of damage tolerance, from size one.
     * @var array
     */
    public static $message_bytes = [16, 28, 44, 64, 86, 108, 124, 154, 182];
    /**
     * How many bytes of correction each block of each size carries.
     * @var array
     */
    public static $correction_bytes = [10, 16, 26, 18, 24, 16, 18, 22, 22];
    /**
     * How many blocks the message of each size is split into.
     * @var array
     */
    public static $block_counts = [1, 1, 1, 2, 2, 4, 4, 4, 5];
    /**
     * Where the small square guides sit, by size, so a reader can find its
     * bearings across a larger symbol. Size one has none.
     * @var array
     */
    public static $guide_centers = [[], [6, 18], [6, 22], [6, 26], [6, 30],
        [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46]];
    /**
     * Makes the symbol for a piece of text as a grid of true and false,
     * true being a dark square.
     *
     * @param string $text what the symbol should say
     * @return array|bool the grid, row by row, or false when the text is
     *      longer than the largest size made here can hold
     */
    public static function grid($text)
    {
        $size = self::sizeFor($text);
        if ($size < 1) {
            return false;
        }
        $message = self::messageBytes($text, $size);
        $whole = self::withCorrection($message, $size);
        $width = 17 + 4 * $size;
        $grid = [];
        $fixed = [];
        for ($row = 0; $row < $width; $row++) {
            $grid[$row] = array_fill(0, $width, false);
            $fixed[$row] = array_fill(0, $width, false);
        }
        self::placePatterns($grid, $fixed, $size, $width);
        self::placeMessage($grid, $fixed, $whole, $width);
        $mask = self::bestMask($grid, $fixed, $width);
        self::applyMask($grid, $fixed, $mask, $width);
        self::placeFormat($grid, $mask, $width);
        return $grid;
    }
    /**
     * Draws the symbol as a picture made of squares, which needs no image
     * library and stays sharp at any size.
     *
     * @param string $text what the symbol should say
     * @param int $quiet how many squares of clear space to leave around it
     * @return string|bool the picture, or false when the text is too long
     */
    public static function svg($text, $quiet = 4)
    {
        $grid = self::grid($text);
        if ($grid === false) {
            return false;
        }
        $width = count($grid);
        $whole = $width + 2 * $quiet;
        $dark = "";
        for ($row = 0; $row < $width; $row++) {
            for ($column = 0; $column < $width; $column++) {
                if ($grid[$row][$column]) {
                    $dark .= "M" . ($column + $quiet) . "," .
                        ($row + $quiet) . "h1v1h-1z";
                }
            }
        }
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' .
            $whole . ' ' . $whole . '" shape-rendering="crispEdges">' .
            '<rect width="' . $whole . '" height="' . $whole .
            '" fill="#fff"/><path d="' . $dark . '" fill="#000"/></svg>';
    }
    /**
     * The smallest size that holds a piece of text.
     *
     * @param string $text the text to be carried
     * @return int the size, or zero when none of them holds it
     */
    public static function sizeFor($text)
    {
        $length = strlen($text);
        foreach (self::$message_bytes as $index => $bytes) {
            if ($length + 2 <= $bytes) {
                return $index + 1;
            }
        }
        return 0;
    }
    /**
     * Turns the text into the bytes of the message: a mark saying how it
     * is written, how long it is, the text itself, and padding out to the
     * length this size expects.
     *
     * @param string $text the text to carry
     * @param int $size which size of symbol
     * @return array the message as byte values
     */
    public static function messageBytes($text, $size)
    {
        $bits = "0100" . str_pad(decbin(strlen($text)), 8, "0",
            STR_PAD_LEFT);
        for ($index = 0; $index < strlen($text); $index++) {
            $bits .= str_pad(decbin(ord($text[$index])), 8, "0",
                STR_PAD_LEFT);
        }
        $wanted = self::$message_bytes[$size - 1] * 8;
        $bits .= str_repeat("0", min(4, $wanted - strlen($bits)));
        if (strlen($bits) % 8 != 0) {
            $bits .= str_repeat("0", 8 - strlen($bits) % 8);
        }
        $bytes = [];
        for ($index = 0; $index < strlen($bits); $index += 8) {
            $bytes[] = bindec(substr($bits, $index, 8));
        }
        $fillers = [0xEC, 0x11];
        $which = 0;
        while (count($bytes) < self::$message_bytes[$size - 1]) {
            $bytes[] = $fillers[$which % 2];
            $which++;
        }
        return $bytes;
    }
    /**
     * Splits the message into its blocks, works out the correction bytes
     * for each, and puts them back together in the order the symbol wants:
     * the first byte of every block, then the second of every block, and
     * so on, so damage in one place is spread across all of them.
     *
     * @param array $message the message bytes
     * @param int $size which size of symbol
     * @return array every byte the symbol carries, in order
     */
    public static function withCorrection($message, $size)
    {
        $blocks = self::$block_counts[$size - 1];
        $per_block = intdiv(count($message), $blocks);
        $longer = count($message) % $blocks;
        $pieces = [];
        $corrections = [];
        $at = 0;
        for ($which = 0; $which < $blocks; $which++) {
            $take = $per_block + (($which >= $blocks - $longer) ? 1 : 0);
            $piece = array_slice($message, $at, $take);
            $at += $take;
            $pieces[] = $piece;
            $corrections[] = self::correctionFor($piece,
                self::$correction_bytes[$size - 1]);
        }
        $whole = [];
        $longest = 0;
        foreach ($pieces as $piece) {
            $longest = max($longest, count($piece));
        }
        for ($index = 0; $index < $longest; $index++) {
            foreach ($pieces as $piece) {
                if (isset($piece[$index])) {
                    $whole[] = $piece[$index];
                }
            }
        }
        for ($index = 0; $index < self::$correction_bytes[$size - 1];
            $index++) {
            foreach ($corrections as $correction) {
                $whole[] = $correction[$index];
            }
        }
        return $whole;
    }
    /**
     * Works out the correction bytes for one block, by dividing it by the
     * polynomial that belongs to that many correction bytes and keeping
     * what is left over.
     *
     * @param array $block the block's message bytes
     * @param int $wanted how many correction bytes to make
     * @return array the correction bytes
     */
    public static function correctionFor($block, $wanted)
    {
        $divisor = self::correctionPolynomial($wanted);
        $working = array_merge($block, array_fill(0, $wanted, 0));
        for ($index = 0; $index < count($block); $index++) {
            $leading = $working[$index];
            if ($leading == 0) {
                continue;
            }
            $scale = self::logOf($leading);
            for ($step = 0; $step <= $wanted; $step++) {
                $working[$index + $step] ^= self::exponentOf(
                    ($divisor[$step] + $scale) % 255);
            }
        }
        return array_slice($working, count($block), $wanted);
    }
    /**
     * The polynomial used to make a given number of correction bytes, with
     * each term given as a power rather than a value, which is how the
     * division above wants it.
     *
     * @param int $wanted how many correction bytes it is for
     * @return array the polynomial's terms as powers
     */
    public static function correctionPolynomial($wanted)
    {
        $terms = [0];
        for ($step = 0; $step < $wanted; $step++) {
            $next = array_fill(0, count($terms) + 1, 0);
            foreach ($terms as $index => $term) {
                $next[$index] ^= self::exponentOf(($term + $step) % 255);
                $next[$index + 1] ^= self::exponentOf($term);
            }
            $terms = [];
            foreach ($next as $value) {
                $terms[] = self::logOf($value);
            }
        }
        /* Built from the smallest term upward; the division wants the
           largest first. */
        return array_reverse($terms);
    }
    /**
     * The value a power of the field's generator comes to.
     *
     * @param int $power which power
     * @return int the value
     */
    public static function exponentOf($power)
    {
        self::buildTables();
        return self::$powers[$power % 255];
    }
    /**
     * Which power of the generator a value is.
     *
     * @param int $value the value to look up
     * @return int the power
     */
    public static function logOf($value)
    {
        self::buildTables();
        return self::$logs[$value] ?? 0;
    }
    /**
     * Works out both tables once. Every value in this arithmetic is some
     * power of the same number, so walking the powers once gives both the
     * value of each power and the power of each value; looking either up
     * by searching would be done tens of thousands of times to draw one
     * symbol.
     */
    public static function buildTables()
    {
        if (self::$powers !== null) {
            return;
        }
        self::$powers = [];
        self::$logs = [];
        $value = 1;
        for ($power = 0; $power < 255; $power++) {
            self::$powers[$power] = $value;
            self::$logs[$value] = $power;
            $value <<= 1;
            if ($value > 255) {
                $value ^= self::FIELD_POLYNOMIAL;
            }
        }
    }
    /**
     * Puts in everything that is the same whatever the text says: the
     * three big squares at the corners, the guides, the two dotted lines,
     * the one square that is always dark, and the space kept for the
     * information about how the symbol is written.
     *
     * @param array &$grid the grid being drawn
     * @param array &$fixed which squares may not be changed afterwards
     * @param int $size which size of symbol
     * @param int $width how many squares across it is
     */
    public static function placePatterns(&$grid, &$fixed, $size, $width)
    {
        $corners = [[0, 0], [0, $width - 7], [$width - 7, 0]];
        foreach ($corners as $corner) {
            for ($row = -1; $row < 8; $row++) {
                for ($column = -1; $column < 8; $column++) {
                    $at_row = $corner[0] + $row;
                    $at_column = $corner[1] + $column;
                    if ($at_row < 0 || $at_row >= $width ||
                        $at_column < 0 || $at_column >= $width) {
                        continue;
                    }
                    $edge = max(abs($row - 3), abs($column - 3));
                    /* Dark around the outside and again in the middle,
                       with a clear ring between: the shape a reader looks
                       for to find the symbol's corners. */
                    $grid[$at_row][$at_column] = ($row >= 0 && $row < 7 &&
                        $column >= 0 && $column < 7 &&
                        ($edge == 3 || $edge <= 1));
                    $fixed[$at_row][$at_column] = true;
                }
            }
        }
        foreach (self::$guide_centers[$size - 1] as $down) {
            foreach (self::$guide_centers[$size - 1] as $across) {
                if (($down < 8 && $across < 8) ||
                    ($down < 8 && $across > $width - 9) ||
                    ($down > $width - 9 && $across < 8)) {
                    continue;
                }
                for ($row = -2; $row <= 2; $row++) {
                    for ($column = -2; $column <= 2; $column++) {
                        $edge = max(abs($row), abs($column));
                        $grid[$down + $row][$across + $column] =
                            ($edge != 1);
                        $fixed[$down + $row][$across + $column] = true;
                    }
                }
            }
        }
        for ($along = 8; $along < $width - 8; $along++) {
            $grid[6][$along] = ($along % 2 == 0);
            $fixed[6][$along] = true;
            $grid[$along][6] = ($along % 2 == 0);
            $fixed[$along][6] = true;
        }
        $grid[$width - 8][8] = true;
        $fixed[$width - 8][8] = true;
        for ($along = 0; $along < 9; $along++) {
            if (!$fixed[8][$along]) {
                $fixed[8][$along] = true;
            }
            if (!$fixed[$along][8]) {
                $fixed[$along][8] = true;
            }
        }
        for ($along = $width - 8; $along < $width; $along++) {
            $fixed[8][$along] = true;
            $fixed[$along][8] = true;
        }
    }
    /**
     * Lays the bytes into the grid, two squares wide at a time, going up
     * the right-hand side then down the next pair, skipping everything
     * already spoken for.
     *
     * @param array &$grid the grid being drawn
     * @param array $fixed which squares may not be written into
     * @param array $whole every byte the symbol carries
     * @param int $width how many squares across it is
     */
    public static function placeMessage(&$grid, $fixed, $whole, $width)
    {
        $bits = "";
        foreach ($whole as $byte) {
            $bits .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
        }
        $at = 0;
        $upward = true;
        for ($right = $width - 1; $right > 0; $right -= 2) {
            if ($right == 6) {
                $right--;
            }
            for ($step = 0; $step < $width; $step++) {
                $row = $upward ? $width - 1 - $step : $step;
                for ($side = 0; $side < 2; $side++) {
                    $column = $right - $side;
                    if ($fixed[$row][$column]) {
                        continue;
                    }
                    $grid[$row][$column] = ($at < strlen($bits)) ?
                        ($bits[$at] === "1") : false;
                    $at++;
                }
            }
            $upward = !$upward;
        }
    }
    /**
     * Whether a square is turned over by a given pattern.
     *
     * @param int $pattern which of the eight patterns
     * @param int $row the square's row
     * @param int $column the square's column
     * @return bool whether it is turned over
     */
    public static function maskAt($pattern, $row, $column)
    {
        switch ($pattern) {
            case 0:
                return ($row + $column) % 2 == 0;
            case 1:
                return $row % 2 == 0;
            case 2:
                return $column % 3 == 0;
            case 3:
                return ($row + $column) % 3 == 0;
            case 4:
                return (intdiv($row, 2) + intdiv($column, 3)) % 2 == 0;
            case 5:
                return ($row * $column) % 2 + ($row * $column) % 3 == 0;
            case 6:
                return (($row * $column) % 2 + ($row * $column) % 3) % 2 == 0;
            default:
                return (($row + $column) % 2 + ($row * $column) % 3) % 2 == 0;
        }
    }
    /**
     * Turns the squares over according to one of the eight patterns, so a
     * symbol does not end up with large blank areas a reader would
     * struggle with. The squares that were put in beforehand are left
     * alone.
     *
     * @param array &$grid the grid being drawn
     * @param array $fixed which squares may not be turned over
     * @param int $pattern which of the eight patterns
     * @param int $width how many squares across it is
     */
    public static function applyMask(&$grid, $fixed, $pattern, $width)
    {
        for ($row = 0; $row < $width; $row++) {
            for ($column = 0; $column < $width; $column++) {
                if ($fixed[$row][$column]) {
                    continue;
                }
                if (self::maskAt($pattern, $row, $column)) {
                    $grid[$row][$column] = !$grid[$row][$column];
                }
            }
        }
    }
    /**
     * Tries each of the eight patterns and keeps the one that leaves the
     * symbol easiest to read, judged by the rules the standard gives:
     * long runs of one color, blocks of one color, anything resembling a
     * corner square, and too much or too little dark overall.
     *
     * @param array $grid the grid as laid out, before any turning over
     * @param array $fixed which squares may not be turned over
     * @param int $width how many squares across it is
     * @return int which pattern to use
     */
    public static function bestMask($grid, $fixed, $width)
    {
        $best = 0;
        $best_score = -1;
        for ($pattern = 0; $pattern < 8; $pattern++) {
            $trial = $grid;
            self::applyMask($trial, $fixed, $pattern, $width);
            $score = self::penalty($trial, $width);
            if ($best_score < 0 || $score < $best_score) {
                $best_score = $score;
                $best = $pattern;
            }
        }
        return $best;
    }
    /**
     * How hard a grid would be to read, lower being better.
     *
     * @param array $grid the grid to judge
     * @param int $width how many squares across it is
     * @return int the score
     */
    public static function penalty($grid, $width)
    {
        $score = 0;
        for ($row = 0; $row < $width; $row++) {
            for ($column = 0; $column < $width; $column++) {
                foreach ([[0, 1], [1, 0]] as $way) {
                    $run = 1;
                    while (true) {
                        $next_row = $row + $way[0] * $run;
                        $next_column = $column + $way[1] * $run;
                        if ($next_row >= $width || $next_column >= $width ||
                            $grid[$next_row][$next_column] !=
                            $grid[$row][$column]) {
                            break;
                        }
                        $run++;
                    }
                    $before_row = $row - $way[0];
                    $before_column = $column - $way[1];
                    $starts = ($before_row < 0 || $before_column < 0 ||
                        $grid[$before_row][$before_column] !=
                        $grid[$row][$column]);
                    if ($starts && $run >= 5) {
                        $score += 3 + ($run - 5);
                    }
                }
                if ($row + 1 < $width && $column + 1 < $width &&
                    $grid[$row][$column] == $grid[$row + 1][$column] &&
                    $grid[$row][$column] == $grid[$row][$column + 1] &&
                    $grid[$row][$column] == $grid[$row + 1][$column + 1]) {
                    $score += 3;
                }
            }
        }
        $dark = 0;
        foreach ($grid as $row_values) {
            foreach ($row_values as $value) {
                if ($value) {
                    $dark++;
                }
            }
        }
        $portion = intdiv(abs($dark * 100 - $width * $width * 50),
            $width * $width);
        $score += intdiv($portion, 5) * 10;
        return $score;
    }
    /**
     * Writes in the fifteen squares that say which level of damage
     * tolerance and which pattern were used, twice over so a reader that
     * cannot make out one copy can use the other.
     *
     * @param array &$grid the grid being drawn
     * @param int $mask which pattern was used
     * @param int $width how many squares across it is
     */
    public static function placeFormat(&$grid, $mask, $width)
    {
        $said = (0 << 3) | $mask;
        $working = $said << 10;
        for ($step = 4; $step >= 0; $step--) {
            if ($working & (1 << ($step + 10))) {
                $working ^= 0x537 << $step;
            }
        }
        $format = (($said << 10) | $working) ^ 0x5412;
        for ($step = 0; $step < 15; $step++) {
            $on = (($format >> $step) & 1) == 1;
            if ($step < 6) {
                $grid[8][$step] = $on;
            } else if ($step < 8) {
                $grid[8][$step + 1] = $on;
            } else if ($step == 8) {
                $grid[7][8] = $on;
            } else {
                $grid[14 - $step][8] = $on;
            }
            if ($step < 8) {
                $grid[$width - 1 - $step][8] = $on;
            } else {
                $grid[8][$width - 15 + $step] = $on;
            }
        }
        $grid[$width - 8][8] = true;
    }
}
X