<?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;
}
}