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

/**
 * CompactFontFormat reads a font in Compact Font Format (CFF) and gives back
 * the outline of each letter as a list of points that can be filled. CFF is
 * Adobe's binary font format. It turns up in two places a crawler meets: inside
 * a PDF, where a font embedded in the file appears as a FontFile3 stream whose
 * Subtype is Type1C, CIDFontType0C or OpenType; and inside an OpenType font, as
 * the table glyph_places "CFF ". A PDF that sets words as type names which
 * charstrings to draw and how large, but the shapes themselves are only in the
 * embedded font, so anything drawing that page as a picture has to read the
 * font or draw the words in the wrong face. What this class handles, and what
 * it does not: - The file's index structures, its Top DICT, its String INDEX,
 * the charset that maps glyph names to positions, and the Private DICT's local
 * subroutines. - Type 2 charstrings, which is what a CFF glyph is: rmoveto,
 * hmoveto, vmoveto, rlineto, hlineto, vlineto, rrcurveto, hhcurveto, vvcurveto,
 * hvcurveto, vhcurveto, rcurveline, rlinecurve, the stem hints (whose widths
 * are skipped rather than applied), callsubr, callgsubr, return and endchar. -
 * It does NOT handle: CID-keyed fonts with an FDArray and FDSelect, seac-style
 * accented characters through endchar's four-argument form, flex through the
 * escape operators, or hinting of any kind. A font using those still parses;
 * the glyphs that rely on them come back empty or unhinted. Coordinates come
 * back in the font's own units, which the FontMatrix states and which is 1000
 * units to the em for nearly every CFF font, with the y axis running upward
 * from the baseline. A caller scales them to the size wanted and flips y if it
 * is drawing into a picture whose rows run downward. Bezier curves are
 * flattened to short straight segments here, since the intended use is filling
 * a polygon rather than re-emitting an outline; a caller wanting the control
 * points would need to change addCurveFromSteps. Typical use: $font = new
 * CompactFontFormat($cff_bytes); $loops = $font->outlineOf("A");   // list of
 * loops, each a list of // [x, y] points in font units $wide =
 * $font->widthOf("A");      // advance width in the same units The first loop
 * is not guaranteed to be the outer contour; a caller filling a glyph with
 * holes should treat the loop enclosing the most area as the outline and punch
 * the rest out of it. The format is specified in Adobe's "The Compact Font
 * Format Specification" (technical note 5176) and the charstrings in "The Type
 * 2 Charstring Format" (technical note 5177).
 * @author Chris Pollett
 */
class CompactFontFormat
{
    /**
     * HEADER_KEPT how many bytes of header the format keeps before its first
     * run
     * @var int
     */
    const HEADER_KEPT = 2;
    /**
     * SMALL_NUMBER the value a one-byte number is offset by in a letter's
     * program
     * @var int
     */
    const SMALL_NUMBER = 139;
    /**
     * WIDER_NUMBER the value a two-byte number is offset by. It is not the same
     * as the one-byte offset, and using that one instead put every place the
     * font names thirty-one bytes late, so nothing could be found.
     * @var int
     */
    const WIDER_NUMBER = 108;
    /**
     * UP_NUMBER where the two-byte positive numbers begin
     * @var int
     */
    const UP_NUMBER = 247;
    /**
     * DOWN_NUMBER where the two-byte negative numbers begin
     * @var int
     */
    const DOWN_NUMBER = 251;
    /**
     * FIXED_NUMBER the marker that a four-byte fixed-point number follows
     * @var int
     */
    const FIXED_NUMBER = 255;
    /**
     * WIDE_NUMBER the marker that a two-byte whole number follows
     * @var int
     */
    const WIDE_NUMBER = 28;
    /**
     * FIXED_PARTS how much a fixed-point number is divided by to give its value
     * @var int
     */
    const FIXED_PARTS = 65536;
    /**
     * CURVE_STEPS how many steps a curve is drawn in when it is turned into
     * straight lines. Few enough to stay quick, many enough that a letter's
     * round parts do not show corners at the size a thumbnail is drawn.
     * @var int
     */
    const CURVE_STEPS = 8;
    /**
     * SHARED_OFFSETS how the numbers in a letter's program are offset before
     * they are looked up among the shared pieces, by how many pieces there are
     * @var array
     */
    const SHARED_OFFSETS = [1240 => 107, 33900 => 1131, -1 => 32768];
    /**
     * PLAIN_WIDE how wide a letter is taken to be where the font does not say
     * @var int
     */
    const PLAIN_WIDE = 500;
    /**
     * widths stores how wide each letter is, by name
     * @var array
     */
    public $widths = [];
    /**
     * stated_widths stores how wide each letter is according to the page's own
     * font entry, by the letter's number
     * @var array
     */
    public $stated_widths = [];
    /**
     * How wide the letter being followed is
     * @var mixed
     */
    public $wide = null;
    /**
     * plain_wide stores how wide a letter is when its program says nothing,
     * which the font states in its own settings
     * @var float
     */
    public $plain_wide = self::PLAIN_WIDE;
    /**
     * bytes stores the font program as the document carried it, which
     * every later reading walks over.
     * @var string
     */
    public $bytes;
    /**
     * charstrings stores each letter's program, by the order it is kept in
     * @var array
     */
    public $charstrings = [];
    /**
     * local_subroutines stores pieces shared between charstrings of this font
     * @var array
     */
    public $local_subroutines = [];
    /**
     * global_subroutines stores pieces shared between all fonts in the program
     * @var array
     */
    public $global_subroutines = [];
    /**
     * glyph_places stores which letter name sits at which place among the
     * charstrings
     * @var array
     */
    public $glyph_places = [];
    /**
     * $places_by_code stores which of the font's letters each number a
     * page may write stands for. A font made for one document numbers
     * its letters itself rather than by the names a standard letter
     * goes by.
     * @var array
     */
    public $places_by_code = [];
    /**
     * $names_by_code stores the name of the letter each number a page
     * writes stands for, as the page's own table of differences gives
     * it. A font made for one document numbers its letters itself.
     * @var array
     */
    public $names_by_code = [];
    /**
     * __construct reads a font program so its charstrings can be asked for.
     * @param string $bytes the font program
     */
    public function __construct($bytes)
    {
        $this->bytes = self::compactPartOf($bytes);
        $this->read();
    }
    /**
     * compactPartOf gives back the compact font program inside a wrapper,
     * or the bytes as they came where they are already one.
     *
     * A font kept with a document may be handed over wrapped, with its
     * parts listed in a table at the front and the four letters OTTO
     * standing at the very start. The reader below wants the compact
     * program itself, so a wrapper is opened and the part named CFF is
     * taken out of it. Without this every letter of such a font came back
     * with no shape at all and a headline drawn in it was left off the
     * page.
     *
     * @param string $bytes the font program as the document keeps it
     * @return string the compact program to read
     */
    public static function compactPartOf($bytes)
    {
        if (strncmp($bytes, "OTTO", 4) != 0 || strlen($bytes) < 12) {
            return $bytes;
        }
        $counted = unpack("nhow_many", substr($bytes, 4, 2));
        $how_many = $counted["how_many"];
        for ($i = 0; $i < $how_many; $i++) {
            $at = 12 + $i * 16;
            if ($at + 16 > strlen($bytes)) {
                break;
            }
            $named = substr($bytes, $at, 4);
            $where = unpack("Nstart/Nlength", substr($bytes, $at + 8, 8));
            if ($named == "CFF " &&
                $where["start"] + $where["length"] <= strlen($bytes)) {
                return substr($bytes, $where["start"], $where["length"]);
            }
        }
        return $bytes;
    }
    /**
     * read reads the parts of the program that say where the charstrings and
     * the shared pieces are kept.
     */
    public function read()
    {
        $bytes = $this->bytes;
        if (strlen($bytes) < self::HEADER_KEPT + 2) {
            return;
        }
        $at = ord($bytes[2]);
        $names = $this->readIndex($at);
        $tops = $this->readIndex($at);
        $strings = $this->readIndex($at);
        $this->global_subroutines = $this->readIndex($at);
        if (empty($tops)) {
            return;
        }
        $top = $this->readDictionary($tops[0]);
        if (isset($top[17])) {
            $where = (int)$top[17][0];
            $this->charstrings = $this->readIndex($where);
        }
        if (isset($top[18]) && count($top[18]) >= 2) {
            $private_size = (int)$top[18][0];
            $private_at = (int)$top[18][1];
            $private = $this->readDictionary(substr($bytes, $private_at,
                $private_size));
            if (isset($private[19])) {
                $own_at = $private_at + (int)$private[19][0];
                $this->local_subroutines = $this->readIndex($own_at);
            }
        }
        if (isset($top[15]) && !empty($this->charstrings)) {
            $this->readGlyphNames((int)$top[15][0], $strings);
        }
        /* A font made for one document numbers its letters itself, and
           says which number stands for which letter in a table of its
           own. Without that table a page setting a headline in such a
           font hands back control characters, and the headline is not
           drawn at all. */
        $this->readOwnEncoding(isset($top[16]) ? (int)$top[16][0] : 0);
    }
    /**
     * readOwnEncoding reads the table in which a font says which number
     * a page writes stands for which of its letters. A font may leave
     * the table out and use one of the two the format fixes, in which
     * case the numbers are the ones the standard names use.
     *
     * @param int $where where in the font the table sits, or zero and
     *     one for the two the format fixes
     */
    public function readOwnEncoding($where)
    {
        if ($where <= 1) {
            return;
        }
        $bytes = $this->bytes;
        if ($where + 1 >= strlen($bytes)) {
            return;
        }
        $kind = ord($bytes[$where]) & 0x7F;
        $at = $where + 1;
        if ($kind == 0) {
            $count = ord($bytes[$at]);
            $at++;
            for ($place = 1; $place <= $count &&
                $at < strlen($bytes); $place++) {
                $this->places_by_code[ord($bytes[$at])] = $place;
                $at++;
            }
            return;
        }
        if ($kind == 1) {
            $runs = ord($bytes[$at]);
            $at++;
            $place = 1;
            for ($run = 0; $run < $runs && $at + 1 < strlen($bytes);
                $run++) {
                $first = ord($bytes[$at]);
                $more = ord($bytes[$at + 1]);
                $at += 2;
                for ($step = 0; $step <= $more; $step++) {
                    $this->places_by_code[$first + $step] = $place;
                    $place++;
                }
            }
        }
    }
    /**
     * readIndex reads one numbered run of pieces, and moves the reading place
     * past it. A run says how many pieces it holds, how wide its places are,
     * and then where each piece begins.
     * @param int &$at where to read from, moved past the run
     * @return array the pieces
     */
    public function readIndex(&$at)
    {
        $bytes = $this->bytes;
        if ($at + 2 > strlen($bytes)) {
            return [];
        }
        $count = (ord($bytes[$at]) << 8) | ord($bytes[$at + 1]);
        $at += 2;
        if ($count == 0) {
            return [];
        }
        $wide = ord($bytes[$at]);
        $at++;
        $places = [];
        for ($index = 0; $index <= $count; $index++) {
            $place = 0;
            for ($step = 0; $step < $wide; $step++) {
                $place = ($place << 8) | ord($bytes[$at]);
                $at++;
            }
            $places[] = $place;
        }
        $begins = $at - 1;
        $pieces = [];
        for ($index = 0; $index < $count; $index++) {
            $pieces[] = substr($bytes, $begins + $places[$index],
                $places[$index + 1] - $places[$index]);
        }
        $at = $begins + $places[$count];
        return $pieces;
    }
    /**
     * readDictionary reads a run of settings, each a list of numbers under a
     * key.
     * @param string $said the settings
     * @return array the numbers, by key
     */
    public function readDictionary($said)
    {
        $settings = [];
        $numbers = [];
        $at = 0;
        $length = strlen($said);
        while ($at < $length) {
            $byte = ord($said[$at]);
            if ($byte <= 21) {
                $key = $byte;
                $at++;
                if ($byte == 12 && $at < $length) {
                    $key = 1200 + ord($said[$at]);
                    $at++;
                }
                $settings[$key] = $numbers;
                $numbers = [];
                continue;
            }
            $numbers[] = $this->readDictionaryNumber($said, $at);
        }
        return $settings;
    }
    /**
     * readDictionaryNumber reads one number from a run of settings, and moves
     * the reading place past it.
     * @param string $said the settings
     * @param int &$at where to read from
     * @return float the number
     */
    public function readDictionaryNumber($said, &$at)
    {
        $byte = ord($said[$at]);
        if ($byte == 30) {
            /* A number written a digit at a time, which is read for its
               value and otherwise left alone. */
            $at++;
            $digits = "";
            $parts = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
                ".", "E", "E-", "", "-", ""];
            while ($at < strlen($said)) {
                $byte = ord($said[$at]);
                $at++;
                $high = ($byte >> 4) & 15;
                $low = $byte & 15;
                if ($high == 15) {
                    break;
                }
                $digits .= $parts[$high];
                if ($low == 15) {
                    break;
                }
                $digits .= $parts[$low];
            }
            return (float)$digits;
        }
        if ($byte == self::WIDE_NUMBER) {
            $value = (ord($said[$at + 1]) << 8) | ord($said[$at + 2]);
            $at += 3;
            return ($value > 32767) ? $value - 65536 : $value;
        }
        if ($byte == 29) {
            $value = 0;
            for ($step = 1; $step <= 4; $step++) {
                $value = ($value << 8) | ord($said[$at + $step]);
            }
            $at += 5;
            return ($value > 2147483647) ? $value - 4294967296 : $value;
        }
        if ($byte >= 32 && $byte <= 246) {
            $at++;
            return $byte - self::SMALL_NUMBER;
        }
        if ($byte >= self::UP_NUMBER && $byte <= 250) {
            $value = ($byte - self::UP_NUMBER) * 256 +
                ord($said[$at + 1]) + self::WIDER_NUMBER;
            $at += 2;
            return $value;
        }
        if ($byte >= self::DOWN_NUMBER && $byte <= 254) {
            $value = -(($byte - self::DOWN_NUMBER) * 256) -
                ord($said[$at + 1]) - self::WIDER_NUMBER;
            $at += 2;
            return $value;
        }
        $at++;
        return 0;
    }
    /**
     * readGlyphNames reads which letter name sits at which place, so a letter
     * can be asked for by the name a page uses.
     * @param int $at where the names are kept
     * @param array $strings the names the program adds to the settled ones
     */
    public function readGlyphNames($at, $strings)
    {
        $bytes = $this->bytes;
        if ($at < 3 || $at >= strlen($bytes)) {
            return;
        }
        $kind = ord($bytes[$at]);
        $at++;
        $ids = [0];
        $count = count($this->charstrings);
        if ($kind == 0) {
            while (count($ids) < $count && $at + 1 < strlen($bytes)) {
                $ids[] = (ord($bytes[$at]) << 8) | ord($bytes[$at + 1]);
                $at += 2;
            }
        } else if ($kind == 1 || $kind == 2) {
            $wide = ($kind == 1) ? 1 : 2;
            while (count($ids) < $count && $at + 1 + $wide < strlen($bytes)) {
                $first = (ord($bytes[$at]) << 8) | ord($bytes[$at + 1]);
                $at += 2;
                $more = ($wide == 1) ? ord($bytes[$at]) :
                    ((ord($bytes[$at]) << 8) | ord($bytes[$at + 1]));
                $at += $wide;
                for ($step = 0; $step <= $more &&
                    count($ids) < $count; $step++) {
                    $ids[] = $first + $step;
                }
            }
        }
        foreach ($ids as $place => $id) {
            $name = self::standardGlyphName($id, $strings);
            if ($name !== "") {
                $this->glyph_places[$name] = $place;
            }
        }
    }
    /**
     * standardGlyphName gives the name a numbered letter goes by, whether it is
     * one of the names every such font shares or one the program adds.
     * @param int $id the number
     * @param array $strings the names the program adds
     * @return string the name, empty if it cannot be told
     */
    public static function standardGlyphName($id, $strings)
    {
        $settled = self::standardGlyphNames();
        if (isset($settled[$id])) {
            return $settled[$id];
        }
        $added = $id - count($settled);
        return $strings[$added] ?? "";
    }
    /**
     * standardGlyphNames gives the names every font of this kind shares, in
     * their settled order. Only the ones a page is likely to set are
     * glyph_places; the rest stand as blanks so the numbering stays right.
     * @return array the names, by number
     */
    public static function standardGlyphNames()
    {
        $names = [".notdef", "space", "exclam", "quotedbl", "numbersign",
            "dollar", "percent", "ampersand", "quoteright", "parenleft",
            "parenright", "asterisk", "plus", "comma", "hyphen", "period",
            "slash", "zero", "one", "two", "three", "four", "five", "six",
            "seven", "eight", "nine", "colon", "semicolon", "less",
            "equal", "greater", "question", "at", "A", "B", "C", "D", "E",
            "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
            "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft",
            "backslash", "bracketright", "asciicircum", "underscore",
            "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
            "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
            "w", "x", "y", "z", "braceleft", "bar", "braceright",
            "asciitilde"];
        return $names;
    }
    /**
     * widthOf how wide a letter is, in the thousandths of an em a font is drawn
     * in, so the next letter can be set beside it. A font that does not say
     * gives back a middling width rather than none, which would pile every
     * letter on the same spot.
     * @param string $name which letter
     * @return float how wide it is
     */
    public function widthOf($name)
    {
        if (!isset($this->widths[$name])) {
            $this->outlineOf($name);
        }
        return $this->widths[$name] ?? $this->plain_wide;
    }
    /**
     * outlineOf gives the outline of one letter, as loops of points in the
     * thousandths of an em that fonts are drawn in.
     * @param string $name which letter
     * @return array the loops, empty where the letter is not in this font
     */
    public function outlineOf($name)
    {
        if (!isset($this->glyph_places[$name])) {
            return [];
        }
        $where = $this->glyph_places[$name];
        $loops = $this->outlineFromCharstring(
            $this->charstrings[$where] ?? "");
        $this->widths[$name] = ($this->wide === null) ? $this->plain_wide :
            $this->wide;
        return $loops;
    }
    /**
     * outlineFromCharstring follows one letter's program, which says where to
     * move and what curves to draw, and gives back the loops it draws.
     * @param string $program the letter's program
     * @return array the loops of points
     */
    public function outlineFromCharstring($program)
    {
        $where = ["across" => 0, "down" => 0];
        $loops = [];
        $loop = [];
        $stems = 0;
        $this->wide = null;
        $this->runCharstring($program, $where, $loops, $loop, $stems, 0);
        if (count($loop) > 2) {
            $loops[] = $loop;
        }
        return $loops;
    }
    /**
     * runCharstring follows a piece of a letter's program, which may call on
     * further pieces shared with other charstrings.
     * @param string $program the piece to runCharstring
     * @param array &$where the pen's place
     * @param array &$loops the loops drawn so far
     * @param array &$loop the loop being drawn
     * @param int &$stems how many stem hints have been given
     * @param int $depth how many pieces deep this is, to stop a piece that
     *     calls itself
     */
    public function runCharstring($program, &$where, &$loops, &$loop, &$stems,
        $depth)
    {
        if ($depth > 10) {
            return;
        }
        $numbers = [];
        $at = 0;
        $length = strlen($program);
        while ($at < $length) {
            $byte = ord($program[$at]);
            if ($byte >= 32 || $byte == self::WIDE_NUMBER) {
                $numbers[] = $this->readCharstringNumber($program, $at);
                continue;
            }
            $at++;
            switch ($byte) {
                case 1:
                case 3:
                case 18:
                case 23:
                    $stems += (int)(count($numbers) / 2);
                    $numbers = [];
                    break;
                case 19:
                case 20:
                    $stems += (int)(count($numbers) / 2);
                    $numbers = [];
                    $at += (int)(($stems + 7) / 8);
                    break;
                case 21:
                    $this->noteGlyphWidth($numbers, 2);
                    $this->closeContour($loops, $loop);
                    $count = count($numbers);
                    $where["across"] += $numbers[$count - 2] ?? 0;
                    $where["down"] += $numbers[$count - 1] ?? 0;
                    $loop[] = [$where["across"], $where["down"]];
                    $numbers = [];
                    break;
                case 22:
                    $this->noteGlyphWidth($numbers, 1);
                    $this->closeContour($loops, $loop);
                    $where["across"] += end($numbers) ?: 0;
                    $loop[] = [$where["across"], $where["down"]];
                    $numbers = [];
                    break;
                case 4:
                    $this->noteGlyphWidth($numbers, 1);
                    $this->closeContour($loops, $loop);
                    $where["down"] += end($numbers) ?: 0;
                    $loop[] = [$where["across"], $where["down"]];
                    $numbers = [];
                    break;
                case 5:
                    for ($step = 0; $step + 1 < count($numbers);
                        $step += 2) {
                        $where["across"] += $numbers[$step];
                        $where["down"] += $numbers[$step + 1];
                        $loop[] = [$where["across"], $where["down"]];
                    }
                    $numbers = [];
                    break;
                case 6:
                case 7:
                    $sideways = ($byte == 6);
                    foreach ($numbers as $step) {
                        if ($sideways) {
                            $where["across"] += $step;
                        } else {
                            $where["down"] += $step;
                        }
                        $loop[] = [$where["across"], $where["down"]];
                        $sideways = !$sideways;
                    }
                    $numbers = [];
                    break;
                case 8:
                    for ($step = 0; $step + 5 < count($numbers);
                        $step += 6) {
                        $this->addCurveFromSteps($where, $loop,
                            array_slice($numbers, $step, 6));
                    }
                    $numbers = [];
                    break;
                case 24:
                    $step = 0;
                    while (count($numbers) - $step > 7) {
                        $this->addCurveFromSteps($where, $loop,
                            array_slice($numbers, $step, 6));
                        $step += 6;
                    }
                    $where["across"] += $numbers[$step] ?? 0;
                    $where["down"] += $numbers[$step + 1] ?? 0;
                    $loop[] = [$where["across"], $where["down"]];
                    $numbers = [];
                    break;
                case 25:
                    $step = 0;
                    while (count($numbers) - $step > 7) {
                        $where["across"] += $numbers[$step];
                        $where["down"] += $numbers[$step + 1];
                        $loop[] = [$where["across"], $where["down"]];
                        $step += 2;
                    }
                    $this->addCurveFromSteps($where, $loop,
                        array_slice($numbers, $step, 6));
                    $numbers = [];
                    break;
                case 26:
                case 27:
                    $this->addAlternatingCurves($byte, $numbers, $where,
                        $loop);
                    $numbers = [];
                    break;
                case 30:
                case 31:
                    $this->addTurningCurves($byte, $numbers, $where, $loop);
                    $numbers = [];
                    break;
                case 10:
                    $this->runSubroutine($this->local_subroutines,
                        $numbers, $where, $loops, $loop, $stems, $depth);
                    break;
                case 29:
                    $this->runSubroutine($this->global_subroutines,
                        $numbers, $where, $loops, $loop, $stems, $depth);
                    break;
                case 11:
                    return;
                case 14:
                    $this->noteGlyphWidth($numbers, 0);
                    $this->closeContour($loops, $loop);
                    return;
                case 12:
                    $at++;
                    $numbers = [];
                    break;
                default:
                    $numbers = [];
                    break;
            }
        }
    }
    /**
     * runSubroutine runs a subroutine shared between charstrings, named by the
     * last number given.
     * @param array $pieces the pieces to choose from
     * @param array &$numbers the numbers given so far
     * @param array &$where the pen's place
     * @param array &$loops the loops drawn so far
     * @param array &$loop the loop being drawn
     * @param int &$stems how many stem hints have been given
     * @param int $depth how many pieces deep this is
     */
    public function runSubroutine($pieces, &$numbers, &$where, &$loops, &$loop,
        &$stems, $depth)
    {
        $which = (int)array_pop($numbers);
        $count = count($pieces);
        $offset = self::SHARED_OFFSETS[-1];
        if ($count < 1240) {
            $offset = self::SHARED_OFFSETS[1240];
        } else if ($count < 33900) {
            $offset = self::SHARED_OFFSETS[33900];
        }
        $which += $offset;
        if (isset($pieces[$which])) {
            $this->runCharstring($pieces[$which], $where, $loops, $loop, $stems,
                $depth + 1);
        }
    }
    /**
     * addAlternatingCurves draws a run of curves that alternate between running
     * mostly sideways and mostly up and down.
     * @param int $byte which of the two the run begins with
     * @param array $numbers the numbers given
     * @param array &$where the pen's place
     * @param array &$loop the loop being drawn
     */
    public function addAlternatingCurves($byte, $numbers, &$where, &$loop)
    {
        $upright = ($byte == 26);
        $step = 0;
        $odd = (count($numbers) % 4) == 1;
        $first = 0;
        if ($odd) {
            $first = $numbers[0];
            $step = 1;
        }
        while ($step + 3 < count($numbers)) {
            if ($upright) {
                $this->addCurveFromSteps($where, $loop,
                    [$first, $numbers[$step], $numbers[$step + 1],
                    $numbers[$step + 2], 0, $numbers[$step + 3]]);
            } else {
                $this->addCurveFromSteps($where, $loop,
                    [$numbers[$step], $first, $numbers[$step + 1],
                    $numbers[$step + 2], $numbers[$step + 3], 0]);
            }
            $first = 0;
            $step += 4;
        }
    }
    /**
     * addTurningCurves draws a run of curves that turn between running sideways
     * and running up and down, each taking its turn from the one before.
     * @param int $byte which of the two the run begins with
     * @param array $numbers the numbers given
     * @param array &$where the pen's place
     * @param array &$loop the loop being drawn
     */
    public function addTurningCurves($byte, $numbers, &$where, &$loop)
    {
        $sideways = ($byte == 31);
        $step = 0;
        $count = count($numbers);
        while ($step + 3 < $count) {
            $last = ($count - $step == 5);
            $extra = $last ? $numbers[$step + 4] : 0;
            if ($sideways) {
                $this->addCurveFromSteps($where, $loop, [$numbers[$step], 0,
                    $numbers[$step + 1], $numbers[$step + 2], $extra,
                    $numbers[$step + 3]]);
            } else {
                $this->addCurveFromSteps($where, $loop, [0, $numbers[$step],
                    $numbers[$step + 1], $numbers[$step + 2],
                    $numbers[$step + 3], $extra]);
            }
            $sideways = !$sideways;
            $step += 4;
        }
    }
    /**
     * addCurveFromSteps draws one curve from where the pen is, given as three
     * steps: two pulling the curve and one ending it. The curve is drawn as a
     * few straight lines, which at the size a thumbnail is drawn cannot be told
     * from the curve itself.
     * @param array &$where the pen's place
     * @param array &$loop the loop being drawn
     * @param array $steps the six numbers saying how far each step goes
     */
    public function addCurveFromSteps(&$where, &$loop, $steps)
    {
        if (count($steps) < 6) {
            return;
        }
        $from_across = $where["across"];
        $from_down = $where["down"];
        $pull_across = $from_across + $steps[0];
        $pull_down = $from_down + $steps[1];
        $next_across = $pull_across + $steps[2];
        $next_down = $pull_down + $steps[3];
        $to_across = $next_across + $steps[4];
        $to_down = $next_down + $steps[5];
        for ($step = 1; $step <= self::CURVE_STEPS; $step++) {
            $part = $step / self::CURVE_STEPS;
            $rest = 1 - $part;
            $loop[] = [
                $rest * $rest * $rest * $from_across +
                3 * $rest * $rest * $part * $pull_across +
                3 * $rest * $part * $part * $next_across +
                $part * $part * $part * $to_across,
                $rest * $rest * $rest * $from_down +
                3 * $rest * $rest * $part * $pull_down +
                3 * $rest * $part * $part * $next_down +
                $part * $part * $part * $to_down];
        }
        $where["across"] = $to_across;
        $where["down"] = $to_down;
    }
    /**
     * noteGlyphWidth notes how wide a letter is, which its program gives as one
     * number more than the step it is attached to needs. A letter that gives
     * none is as wide as the font's usual.
     * @param array $numbers the numbers given to the step
     * @param int $wanted how many the step itself needs
     */
    public function noteGlyphWidth($numbers, $wanted)
    {
        if ($this->wide === null && count($numbers) > $wanted) {
            $this->wide = $this->plain_wide + $numbers[0];
        }
    }
    /**
     * closeContour closes the loop being drawn, if it has enough points to
     * enclose anything, and starts the next one.
     * @param array &$loops the loops drawn so far
     * @param array &$loop the loop being drawn
     */
    public function closeContour(&$loops, &$loop)
    {
        if (count($loop) > 2) {
            $loops[] = $loop;
        }
        $loop = [];
    }
    /**
     * readCharstringNumber reads one number from a letter's program, and moves
     * the reading place past it.
     * @param string $program the letter's program
     * @param int &$at where to read from
     * @return float the number
     */
    public function readCharstringNumber($program, &$at)
    {
        $byte = ord($program[$at]);
        if ($byte == self::WIDE_NUMBER) {
            $value = (ord($program[$at + 1]) << 8) | ord($program[$at + 2]);
            $at += 3;
            return ($value > 32767) ? $value - 65536 : $value;
        }
        if ($byte <= 246) {
            $at++;
            return $byte - self::SMALL_NUMBER;
        }
        if ($byte <= 250) {
            $value = ($byte - self::UP_NUMBER) * 256 +
                ord($program[$at + 1]) + self::WIDER_NUMBER;
            $at += 2;
            return $value;
        }
        if ($byte <= 254) {
            $value = -(($byte - self::DOWN_NUMBER) * 256) -
                ord($program[$at + 1]) - self::WIDER_NUMBER;
            $at += 2;
            return $value;
        }
        $value = 0;
        for ($step = 1; $step <= 4; $step++) {
            $value = ($value << 8) | ord($program[$at + $step]);
        }
        $at += 5;
        if ($value > 2147483647) {
            $value -= 4294967296;
        }
        return $value / self::FIXED_PARTS;
    }
}
X