OutlineSimple.php 9.44 KB
<?php
/**
 * @package php-font-lib
 * @link    https://github.com/PhenX/php-font-lib
 * @author  Fabien Ménager <fabien.menager@gmail.com>
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @version $Id: Font_Table_glyf.php 46 2012-04-02 20:22:38Z fabien.menager $
 */

namespace FontLib\Glyph;

/**
 * `glyf` font table.
 *
 * @package php-font-lib
 */
class OutlineSimple extends Outline
{
    const ON_CURVE = 0x01;
    const X_SHORT_VECTOR = 0x02;
    const Y_SHORT_VECTOR = 0x04;
    const REPEAT = 0x08;
    const THIS_X_IS_SAME = 0x10;
    const THIS_Y_IS_SAME = 0x20;

    public $instructions;
    public $points;

    function parseData()
    {
        parent::parseData();

        if (!$this->size) {
            return;
        }

        $font = $this->getFont();

        $noc = $this->numberOfContours;

        if ($noc == 0) {
            return;
        }

        $endPtsOfContours = $font->r(array(self::uint16, $noc));

        $instructionLength = $font->readUInt16();
        $this->instructions = $font->r(array(self::uint8, $instructionLength));

        $count = $endPtsOfContours[$noc - 1] + 1;

        // Flags
        $flags = array();
        for ($index = 0; $index < $count; $index++) {
            $flags[$index] = $font->readUInt8();

            if ($flags[$index] & self::REPEAT) {
                $repeats = $font->readUInt8();

                for ($i = 1; $i <= $repeats; $i++) {
                    $flags[$index + $i] = $flags[$index];
                }

                $index += $repeats;
            }
        }

        $points = array();
        foreach ($flags as $i => $flag) {
            $points[$i]["onCurve"] = $flag & self::ON_CURVE;
            $points[$i]["endOfContour"] = in_array($i, $endPtsOfContours);
        }

        // X Coords
        $x = 0;
        for ($i = 0; $i < $count; $i++) {
            $flag = $flags[$i];

            if ($flag & self::THIS_X_IS_SAME) {
                if ($flag & self::X_SHORT_VECTOR) {
                    $x += $font->readUInt8();
                }
            } else {
                if ($flag & self::X_SHORT_VECTOR) {
                    $x -= $font->readUInt8();
                } else {
                    $x += $font->readInt16();
                }
            }

            $points[$i]["x"] = $x;
        }

        // Y Coords
        $y = 0;
        for ($i = 0; $i < $count; $i++) {
            $flag = $flags[$i];

            if ($flag & self::THIS_Y_IS_SAME) {
                if ($flag & self::Y_SHORT_VECTOR) {
                    $y += $font->readUInt8();
                }
            } else {
                if ($flag & self::Y_SHORT_VECTOR) {
                    $y -= $font->readUInt8();
                } else {
                    $y += $font->readInt16();
                }
            }

            $points[$i]["y"] = $y;
        }

        $this->points = $points;
    }

    public function splitSVGPath($path)
    {
        preg_match_all('/([a-z])|(-?\d+(?:\.\d+)?)/i', $path, $matches, PREG_PATTERN_ORDER);

        return $matches[0];
    }

    public function makePoints($path)
    {
        $path = $this->splitSVGPath($path);
        $l = count($path);
        $i = 0;

        $points = array();

        while ($i < $l) {
            switch ($path[$i]) {
                // moveTo
                case "M":
                    $points[] = array(
                        "onCurve" => true,
                        "x" => $path[++$i],
                        "y" => $path[++$i],
                        "endOfContour" => false,
                    );
                    break;

                // lineTo
                case "L":
                    $points[] = array(
                        "onCurve" => true,
                        "x" => $path[++$i],
                        "y" => $path[++$i],
                        "endOfContour" => false,
                    );
                    break;

                // quadraticCurveTo
                case "Q":
                    $points[] = array(
                        "onCurve" => false,
                        "x" => $path[++$i],
                        "y" => $path[++$i],
                        "endOfContour" => false,
                    );
                    $points[] = array(
                        "onCurve" => true,
                        "x" => $path[++$i],
                        "y" => $path[++$i],
                        "endOfContour" => false,
                    );
                    break;

                // closePath
                /** @noinspection PhpMissingBreakStatementInspection */
                case "z":
                    $points[count($points) - 1]["endOfContour"] = true;

                default:
                    $i++;
                    break;
            }
        }

        return $points;
    }

    function encode()
    {
        if (empty($this->points)) {
            return parent::encode();
        }

        return $this->size = $this->encodePoints($this->points);
    }

    public function encodePoints($points)
    {
        $endPtsOfContours = array();
        $flags = array();
        $coords_x = array();
        $coords_y = array();

        $last_x = 0;
        $last_y = 0;
        $xMin = $yMin = 0xFFFF;
        $xMax = $yMax = -0xFFFF;
        foreach ($points as $i => $point) {
            $flag = 0;
            if ($point["onCurve"]) {
                $flag |= self::ON_CURVE;
            }

            if ($point["endOfContour"]) {
                $endPtsOfContours[] = $i;
            }

            // Simplified, we could do some optimizations
            if ($point["x"] == $last_x) {
                $flag |= self::THIS_X_IS_SAME;
            } else {
                $x = intval($point["x"]);
                $xMin = min($x, $xMin);
                $xMax = max($x, $xMax);
                $coords_x[] = $x - $last_x; // int16
            }

            // Simplified, we could do some optimizations
            if ($point["y"] == $last_y) {
                $flag |= self::THIS_Y_IS_SAME;
            } else {
                $y = intval($point["y"]);
                $yMin = min($y, $yMin);
                $yMax = max($y, $yMax);
                $coords_y[] = $y - $last_y; // int16
            }

            $flags[] = $flag;
            $last_x = $point["x"];
            $last_y = $point["y"];
        }

        $font = $this->getFont();

        $l = 0;
        $l += $font->writeInt16(count($endPtsOfContours)); // endPtsOfContours
        $l += $font->writeFWord(isset($this->xMin) ? $this->xMin : $xMin); // xMin
        $l += $font->writeFWord(isset($this->yMin) ? $this->yMin : $yMin); // yMin
        $l += $font->writeFWord(isset($this->xMax) ? $this->xMax : $xMax); // xMax
        $l += $font->writeFWord(isset($this->yMax) ? $this->yMax : $yMax); // yMax

        // Simple glyf
        $l += $font->w(array(self::uint16, count($endPtsOfContours)), $endPtsOfContours); // endPtsOfContours
        $l += $font->writeUInt16(0); // instructionLength
        $l += $font->w(array(self::uint8, count($flags)), $flags); // flags
        $l += $font->w(array(self::int16, count($coords_x)), $coords_x); // xCoordinates
        $l += $font->w(array(self::int16, count($coords_y)), $coords_y); // yCoordinates
        return $l;
    }

    public function getSVGContours($points = null)
    {
        $path = "";

        if (!$points) {
            if (empty($this->points)) {
                $this->parseData();
            }

            $points = $this->points;
        }

        $length = count($points);
        $firstIndex = 0;
        $count = 0;

        for ($i = 0; $i < $length; $i++) {
            $count++;

            if ($points[$i]["endOfContour"]) {
                $path .= $this->getSVGPath($points, $firstIndex, $count);
                $firstIndex = $i + 1;
                $count = 0;
            }
        }

        return $path;
    }

    protected function getSVGPath($points, $startIndex, $count)
    {
        $offset = 0;
        $path = "";

        while ($offset < $count) {
            $point = $points[$startIndex + $offset % $count];
            $point_p1 = $points[$startIndex + ($offset + 1) % $count];

            if ($offset == 0) {
                $path .= "M{$point['x']},{$point['y']} ";
            }

            if ($point["onCurve"]) {
                if ($point_p1["onCurve"]) {
                    $path .= "L{$point_p1['x']},{$point_p1['y']} ";
                    $offset++;
                } else {
                    $point_p2 = $points[$startIndex + ($offset + 2) % $count];

                    if ($point_p2["onCurve"]) {
                        $path .= "Q{$point_p1['x']},{$point_p1['y']},{$point_p2['x']},{$point_p2['y']} ";
                    } else {
                        $path .= "Q{$point_p1['x']},{$point_p1['y']}," . $this->midValue($point_p1['x'], $point_p2['x']) . "," . $this->midValue($point_p1['y'], $point_p2['y']) . " ";
                    }

                    $offset += 2;
                }
            } else {
                if ($point_p1["onCurve"]) {
                    $path .= "Q{$point['x']},{$point['y']},{$point_p1['x']},{$point_p1['y']} ";
                } else {
                    $path .= "Q{$point['x']},{$point['y']}," . $this->midValue($point['x'], $point_p1['x']) . "," . $this->midValue($point['y'], $point_p1['y']) . " ";
                }

                $offset++;
            }
        }

        $path .= "z ";

        return $path;
    }

    function midValue($a, $b)
    {
        return $a + ($b - $a) / 2;
    }
}