diff options
Diffstat (limited to 'vendor/phenx/php-svg-lib/src/Svg')
28 files changed, 10154 insertions, 0 deletions
diff --git a/vendor/phenx/php-svg-lib/src/Svg/CssLength.php b/vendor/phenx/php-svg-lib/src/Svg/CssLength.php new file mode 100644 index 0000000..88eda8c --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/CssLength.php @@ -0,0 +1,135 @@ +<?php + +namespace Svg; + +class CssLength +{ + /** + * Array of valid css length units. + * Should be pre-sorted by unit text length so no earlier length can be + * contained within a latter (eg. 'in' within 'vmin'). + * + * @var string[] + */ + protected static $units = [ + 'vmax', + 'vmin', + 'rem', + 'px', + 'pt', + 'cm', + 'mm', + 'in', + 'pc', + 'em', + 'ex', + 'ch', + 'vw', + 'vh', + '%', + 'q', + ]; + + /** + * A list of units that are inch-relative, and their unit division within an inch. + * + * @var array<string, float> + */ + protected static $inchDivisions = [ + 'in' => 1, + 'cm' => 2.54, + 'mm' => 25.4, + 'q' => 101.6, + 'pc' => 6, + 'pt' => 72, + ]; + + /** + * The CSS length unit indicator. + * Will be lower-case and one of the units listed in the '$units' array or empty. + * + * @var string + */ + protected $unit = ''; + + /** + * The numeric value of the given length. + * + * @var float + */ + protected $value = 0; + + /** + * The original unparsed length provided. + * + * @var string + */ + protected $unparsed; + + public function __construct(string $length) + { + $this->unparsed = $length; + $this->parseLengthComponents($length); + } + + /** + * Parse out the unit and value components from the given string length. + */ + protected function parseLengthComponents(string $length): void + { + $length = strtolower($length); + + foreach (self::$units as $unit) { + $pos = strpos($length, $unit); + if ($pos) { + $this->value = floatval(substr($length, 0, $pos)); + $this->unit = $unit; + return; + } + } + + $this->unit = ''; + $this->value = floatval($length); + } + + /** + * Get the unit type of this css length. + * Units are standardised to be lower-cased. + * + * @return string + */ + public function getUnit(): string + { + return $this->unit; + } + + /** + * Get this CSS length in the equivalent pixel count size. + * + * @param float $referenceSize + * @param float $dpi + * + * @return float + */ + public function toPixels(float $referenceSize = 11.0, float $dpi = 96.0): float + { + // Standard relative units + if (in_array($this->unit, ['em', 'rem', 'ex', 'ch'])) { + return $this->value * $referenceSize; + } + + // Percentage relative units + if (in_array($this->unit, ['%', 'vw', 'vh', 'vmin', 'vmax'])) { + return $this->value * ($referenceSize / 100); + } + + // Inch relative units + if (in_array($this->unit, array_keys(static::$inchDivisions))) { + $inchValue = $this->value * $dpi; + $division = static::$inchDivisions[$this->unit]; + return $inchValue / $division; + } + + return $this->value; + } +}
\ No newline at end of file diff --git a/vendor/phenx/php-svg-lib/src/Svg/DefaultStyle.php b/vendor/phenx/php-svg-lib/src/Svg/DefaultStyle.php new file mode 100644 index 0000000..4e73d29 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/DefaultStyle.php @@ -0,0 +1,29 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg; + +class DefaultStyle extends Style +{ + public $color = [0, 0, 0, 1]; + public $opacity = 1.0; + public $display = 'inline'; + + public $fill = [0, 0, 0, 1]; + public $fillOpacity = 1.0; + public $fillRule = 'nonzero'; + + public $stroke = 'none'; + public $strokeOpacity = 1.0; + public $strokeLinecap = 'butt'; + public $strokeLinejoin = 'miter'; + public $strokeMiterlimit = 4; + public $strokeWidth = 1.0; + public $strokeDasharray = 0; + public $strokeDashoffset = 0; +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Document.php b/vendor/phenx/php-svg-lib/src/Svg/Document.php new file mode 100644 index 0000000..4de226e --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Document.php @@ -0,0 +1,406 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg; + +use Svg\Surface\SurfaceInterface; +use Svg\Tag\AbstractTag; +use Svg\Tag\Anchor; +use Svg\Tag\Circle; +use Svg\Tag\Ellipse; +use Svg\Tag\Group; +use Svg\Tag\ClipPath; +use Svg\Tag\Image; +use Svg\Tag\Line; +use Svg\Tag\LinearGradient; +use Svg\Tag\Path; +use Svg\Tag\Polygon; +use Svg\Tag\Polyline; +use Svg\Tag\Rect; +use Svg\Tag\Stop; +use Svg\Tag\Text; +use Svg\Tag\StyleTag; +use Svg\Tag\UseTag; + +class Document extends AbstractTag +{ + protected $filename; + public $inDefs = false; + + protected $x; + protected $y; + protected $width; + protected $height; + + protected $subPathInit; + protected $pathBBox; + protected $viewBox; + + /** @var SurfaceInterface */ + protected $surface; + + /** @var AbstractTag[] */ + protected $stack = array(); + + /** @var AbstractTag[] */ + protected $defs = array(); + + /** @var \Sabberworm\CSS\CSSList\Document[] */ + protected $styleSheets = array(); + + public function loadFile($filename) + { + $this->filename = $filename; + } + + protected function initParser() { + $parser = xml_parser_create("utf-8"); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_set_element_handler( + $parser, + array($this, "_tagStart"), + array($this, "_tagEnd") + ); + xml_set_character_data_handler( + $parser, + array($this, "_charData") + ); + + return $parser; + } + + public function __construct() { + + } + + /** + * @return SurfaceInterface + */ + public function getSurface() + { + return $this->surface; + } + + public function getStack() + { + return $this->stack; + } + + public function getWidth() + { + return $this->width; + } + + public function getHeight() + { + return $this->height; + } + + public function getDiagonal() + { + return sqrt(($this->width)**2 + ($this->height)**2) / sqrt(2); + } + + public function getDimensions() { + $rootAttributes = null; + + $parser = xml_parser_create("utf-8"); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_set_element_handler( + $parser, + function ($parser, $name, $attributes) use (&$rootAttributes) { + if ($name === "svg" && $rootAttributes === null) { + $attributes = array_change_key_case($attributes, CASE_LOWER); + + $rootAttributes = $attributes; + } + }, + function ($parser, $name) {} + ); + + $fp = fopen($this->filename, "r"); + while ($line = fread($fp, 8192)) { + xml_parse($parser, $line, false); + + if ($rootAttributes !== null) { + break; + } + } + + xml_parser_free($parser); + + return $this->handleSizeAttributes($rootAttributes); + } + + public function handleSizeAttributes($attributes){ + if ($this->width === null) { + if (isset($attributes["width"])) { + $width = $this->convertSize($attributes["width"], 400); + $this->width = $width; + } + + if (isset($attributes["height"])) { + $height = $this->convertSize($attributes["height"], 300); + $this->height = $height; + } + + if (isset($attributes['viewbox'])) { + $viewBox = preg_split('/[\s,]+/is', trim($attributes['viewbox'])); + if (count($viewBox) == 4) { + $this->x = $viewBox[0]; + $this->y = $viewBox[1]; + + if (!$this->width) { + $this->width = $viewBox[2]; + } + if (!$this->height) { + $this->height = $viewBox[3]; + } + } + } + } + + return array( + 0 => $this->width, + 1 => $this->height, + + "width" => $this->width, + "height" => $this->height, + ); + } + + public function getDocument(){ + return $this; + } + + /** + * Append a style sheet + * + * @param \Sabberworm\CSS\CSSList\Document $stylesheet + */ + public function appendStyleSheet($stylesheet) { + $this->styleSheets[] = $stylesheet; + } + + /** + * Get the document style sheets + * + * @return \Sabberworm\CSS\CSSList\Document[] + */ + public function getStyleSheets() { + return $this->styleSheets; + } + + protected function before($attributes) + { + $surface = $this->getSurface(); + + $style = new DefaultStyle(); + $style->inherit($this); + $style->fromAttributes($attributes); + + $this->setStyle($style); + + $surface->setStyle($style); + } + + public function render(SurfaceInterface $surface) + { + $this->inDefs = false; + $this->surface = $surface; + + $parser = $this->initParser(); + + if ($this->x || $this->y) { + $surface->translate(-$this->x, -$this->y); + } + + $fp = fopen($this->filename, "r"); + while ($line = fread($fp, 8192)) { + xml_parse($parser, $line, false); + } + + xml_parse($parser, "", true); + + xml_parser_free($parser); + } + + protected function svgOffset($attributes) + { + $this->attributes = $attributes; + + $this->handleSizeAttributes($attributes); + } + + public function getDef($id) { + $id = ltrim($id, "#"); + + return isset($this->defs[$id]) ? $this->defs[$id] : null; + } + + private function _tagStart($parser, $name, $attributes) + { + $this->x = 0; + $this->y = 0; + + $tag = null; + + $attributes = array_change_key_case($attributes, CASE_LOWER); + + switch (strtolower($name)) { + case 'defs': + $this->inDefs = true; + return; + + case 'svg': + if (count($this->attributes)) { + $tag = new Group($this, $name); + } + else { + $tag = $this; + $this->svgOffset($attributes); + } + break; + + case 'path': + $tag = new Path($this, $name); + break; + + case 'rect': + $tag = new Rect($this, $name); + break; + + case 'circle': + $tag = new Circle($this, $name); + break; + + case 'ellipse': + $tag = new Ellipse($this, $name); + break; + + case 'image': + $tag = new Image($this, $name); + break; + + case 'line': + $tag = new Line($this, $name); + break; + + case 'polyline': + $tag = new Polyline($this, $name); + break; + + case 'polygon': + $tag = new Polygon($this, $name); + break; + + case 'lineargradient': + $tag = new LinearGradient($this, $name); + break; + + case 'radialgradient': + $tag = new LinearGradient($this, $name); + break; + + case 'stop': + $tag = new Stop($this, $name); + break; + + case 'style': + $tag = new StyleTag($this, $name); + break; + + case 'a': + $tag = new Anchor($this, $name); + break; + + case 'g': + case 'symbol': + $tag = new Group($this, $name); + break; + + case 'clippath': + $tag = new ClipPath($this, $name); + break; + + case 'use': + $tag = new UseTag($this, $name); + break; + + case 'text': + $tag = new Text($this, $name); + break; + + case 'desc': + return; + } + + if ($tag) { + if (isset($attributes["id"])) { + $this->defs[$attributes["id"]] = $tag; + } + else { + /** @var AbstractTag $top */ + $top = end($this->stack); + if ($top && $top != $tag) { + $top->children[] = $tag; + } + } + + $this->stack[] = $tag; + + $tag->handle($attributes); + } + } + + function _charData($parser, $data) + { + $stack_top = end($this->stack); + + if ($stack_top instanceof Text || $stack_top instanceof StyleTag) { + $stack_top->appendText($data); + } + } + + function _tagEnd($parser, $name) + { + /** @var AbstractTag $tag */ + $tag = null; + switch (strtolower($name)) { + case 'defs': + $this->inDefs = false; + return; + + case 'svg': + case 'path': + case 'rect': + case 'circle': + case 'ellipse': + case 'image': + case 'line': + case 'polyline': + case 'polygon': + case 'radialgradient': + case 'lineargradient': + case 'stop': + case 'style': + case 'text': + case 'g': + case 'symbol': + case 'clippath': + case 'use': + case 'a': + $tag = array_pop($this->stack); + break; + } + + if (!$this->inDefs && $tag) { + $tag->handleEnd(); + } + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Gradient/Stop.php b/vendor/phenx/php-svg-lib/src/Svg/Gradient/Stop.php new file mode 100644 index 0000000..a37fb97 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Gradient/Stop.php @@ -0,0 +1,16 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Gradient; + +class Stop +{ + public $offset; + public $color; + public $opacity = 1.0; +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Style.php b/vendor/phenx/php-svg-lib/src/Svg/Style.php new file mode 100644 index 0000000..14b11e9 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Style.php @@ -0,0 +1,541 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg; + +use Svg\Tag\AbstractTag; + +class Style +{ + const TYPE_COLOR = 1; + const TYPE_LENGTH = 2; + const TYPE_NAME = 3; + const TYPE_ANGLE = 4; + const TYPE_NUMBER = 5; + + private $_parentStyle; + + public $color; + public $opacity; + public $display; + + public $fill; + public $fillOpacity; + public $fillRule; + + public $stroke; + public $strokeOpacity; + public $strokeLinecap; + public $strokeLinejoin; + public $strokeMiterlimit; + public $strokeWidth; + public $strokeDasharray; + public $strokeDashoffset; + + public $fontFamily = 'serif'; + public $fontSize = 12; + public $fontWeight = 'normal'; + public $fontStyle = 'normal'; + public $textAnchor = 'start'; + + protected function getStyleMap() + { + return array( + 'color' => array('color', self::TYPE_COLOR), + 'opacity' => array('opacity', self::TYPE_NUMBER), + 'display' => array('display', self::TYPE_NAME), + + 'fill' => array('fill', self::TYPE_COLOR), + 'fill-opacity' => array('fillOpacity', self::TYPE_NUMBER), + 'fill-rule' => array('fillRule', self::TYPE_NAME), + + 'stroke' => array('stroke', self::TYPE_COLOR), + 'stroke-dasharray' => array('strokeDasharray', self::TYPE_NAME), + 'stroke-dashoffset' => array('strokeDashoffset', self::TYPE_NUMBER), + 'stroke-linecap' => array('strokeLinecap', self::TYPE_NAME), + 'stroke-linejoin' => array('strokeLinejoin', self::TYPE_NAME), + 'stroke-miterlimit' => array('strokeMiterlimit', self::TYPE_NUMBER), + 'stroke-opacity' => array('strokeOpacity', self::TYPE_NUMBER), + 'stroke-width' => array('strokeWidth', self::TYPE_NUMBER), + + 'font-family' => array('fontFamily', self::TYPE_NAME), + 'font-size' => array('fontSize', self::TYPE_NUMBER), + 'font-weight' => array('fontWeight', self::TYPE_NAME), + 'font-style' => array('fontStyle', self::TYPE_NAME), + 'text-anchor' => array('textAnchor', self::TYPE_NAME), + ); + } + + /** + * @param $attributes + * + * @return Style + */ + public function fromAttributes($attributes) + { + $this->fillStyles($attributes); + + if (isset($attributes["style"])) { + $styles = self::parseCssStyle($attributes["style"]); + $this->fillStyles($styles); + } + } + + public function inherit(AbstractTag $tag) { + $group = $tag->getParentGroup(); + if ($group) { + $parent_style = $group->getStyle(); + $this->_parentStyle = $parent_style; + foreach ($parent_style as $_key => $_value) { + if ($_value !== null) { + $this->$_key = $_value; + } + } + } + } + + public function fromStyleSheets(AbstractTag $tag, $attributes) { + $class = isset($attributes["class"]) ? preg_split('/\s+/', trim($attributes["class"])) : null; + + $stylesheets = $tag->getDocument()->getStyleSheets(); + + $styles = array(); + + foreach ($stylesheets as $_sc) { + + /** @var \Sabberworm\CSS\RuleSet\DeclarationBlock $_decl */ + foreach ($_sc->getAllDeclarationBlocks() as $_decl) { + + /** @var \Sabberworm\CSS\Property\Selector $_selector */ + foreach ($_decl->getSelectors() as $_selector) { + $_selector = $_selector->getSelector(); + + // Match class name + if ($class !== null) { + foreach ($class as $_class) { + if ($_selector === ".$_class") { + /** @var \Sabberworm\CSS\Rule\Rule $_rule */ + foreach ($_decl->getRules() as $_rule) { + $styles[$_rule->getRule()] = $_rule->getValue() . ""; + } + + break 2; + } + } + } + + // Match tag name + if ($_selector === $tag->tagName) { + /** @var \Sabberworm\CSS\Rule\Rule $_rule */ + foreach ($_decl->getRules() as $_rule) { + $styles[$_rule->getRule()] = $_rule->getValue() . ""; + } + + break; + } + } + } + } + + $this->fillStyles($styles); + } + + protected function fillStyles($styles) + { + $style_map = $this->getStyleMap(); + foreach ($style_map as $from => $spec) { + if (isset($styles[$from])) { + list($to, $type) = $spec; + $value = null; + switch ($type) { + case self::TYPE_COLOR: + $value = self::parseColor($styles[$from]); + if ($value === "currentcolor") { + if ($type === "color") { + $value = $this->_parentStyle->color; + } else { + $value = $this->color; + } + } + if ($value !== null && $value[3] !== 1 && array_key_exists("{$from}-opacity", $style_map) === true) { + $styles["{$from}-opacity"] = $value[3]; + } + break; + + case self::TYPE_NUMBER: + $value = ($styles[$from] === null) ? null : (float)$styles[$from]; + break; + + default: + $value = $styles[$from]; + } + + if ($value !== null) { + $this->$to = $value; + } + } + } + } + + static function parseColor($color) + { + $color = strtolower(trim($color)); + + $parts = preg_split('/[^,]\s+/', $color, 2); + + if (count($parts) == 2) { + $color = $parts[1]; + } else { + $color = $parts[0]; + } + + if ($color === "none") { + return "none"; + } + + if ($color === "currentcolor") { + return "currentcolor"; + } + + // SVG color name + if (isset(self::$colorNames[$color])) { + return self::parseHexColor(self::$colorNames[$color]); + } + + // Hex color + if ($color[0] === "#") { + return self::parseHexColor($color); + } + + // RGB color + if (strpos($color, "rgb") !== false) { + return self::getQuad($color); + } + + // RGB color + if (strpos($color, "hsl") !== false) { + $quad = self::getQuad($color, true); + + if ($quad == null) { + return null; + } + + list($h, $s, $l, $a) = $quad; + + $r = $l; + $g = $l; + $b = $l; + $v = ($l <= 0.5) ? ($l * (1.0 + $s)) : ($l + $s - $l * $s); + if ($v > 0) { + $m = $l + $l - $v; + $sv = ($v - $m) / $v; + $h *= 6.0; + $sextant = floor($h); + $fract = $h - $sextant; + $vsf = $v * $sv * $fract; + $mid1 = $m + $vsf; + $mid2 = $v - $vsf; + + switch ($sextant) { + case 0: + $r = $v; + $g = $mid1; + $b = $m; + break; + case 1: + $r = $mid2; + $g = $v; + $b = $m; + break; + case 2: + $r = $m; + $g = $v; + $b = $mid1; + break; + case 3: + $r = $m; + $g = $mid2; + $b = $v; + break; + case 4: + $r = $mid1; + $g = $m; + $b = $v; + break; + case 5: + $r = $v; + $g = $m; + $b = $mid2; + break; + } + } + $a = $a * 255; + + return array( + $r * 255.0, + $g * 255.0, + $b * 255.0, + $a + ); + } + + // Gradient + if (strpos($color, "url(#") !== false) { + $i = strpos($color, "("); + $j = strpos($color, ")"); + + // Bad url format + if ($i === false || $j === false) { + return null; + } + + return trim(substr($color, $i + 1, $j - $i - 1)); + } + + return null; + } + + static function getQuad($color, $percent = false) { + $i = strpos($color, "("); + $j = strpos($color, ")"); + + // Bad color value + if ($i === false || $j === false) { + return null; + } + + $quad = preg_split("/\\s*[,\\/]\\s*/", trim(substr($color, $i + 1, $j - $i - 1))); + if (!isset($quad[3])) { + $quad[3] = 1; + } + + if (count($quad) != 3 && count($quad) != 4) { + return null; + } + + foreach (array_keys($quad) as $c) { + $quad[$c] = trim($quad[$c]); + + if ($percent) { + if ($quad[$c][strlen($quad[$c]) - 1] === "%") { + $quad[$c] = floatval($quad[$c]) / 100; + } else { + $quad[$c] = $quad[$c] / 255; + } + } else { + if ($quad[$c][strlen($quad[$c]) - 1] === "%") { + $quad[$c] = round(floatval($quad[$c]) * 2.55); + } + } + } + + return $quad; + } + + static function parseHexColor($hex) + { + $c = array(0, 0, 0, 1); + + // #FFFFFF + if (isset($hex[6])) { + $c[0] = hexdec(substr($hex, 1, 2)); + $c[1] = hexdec(substr($hex, 3, 2)); + $c[2] = hexdec(substr($hex, 5, 2)); + + if (isset($hex[7])) { + $alpha = substr($hex, 7, 2); + if (ctype_xdigit($alpha)) { + $c[3] = round(hexdec($alpha)/255, 2); + } + } + } else { + $c[0] = hexdec($hex[1] . $hex[1]); + $c[1] = hexdec($hex[2] . $hex[2]); + $c[2] = hexdec($hex[3] . $hex[3]); + + if (isset($hex[4])) { + if (ctype_xdigit($hex[4])) { + $c[3] = round(hexdec($hex[4] . $hex[4])/255, 2); + } + } + } + + return $c; + } + + /** + * Simple CSS parser + * + * @param $style + * + * @return array + */ + static function parseCssStyle($style) + { + $matches = array(); + preg_match_all("/([a-z-]+)\\s*:\\s*([^;$]+)/si", $style, $matches, PREG_SET_ORDER); + + $styles = array(); + foreach ($matches as $match) { + $styles[$match[1]] = $match[2]; + } + + return $styles; + } + + static $colorNames = array( + 'antiquewhite' => '#FAEBD7', + 'aqua' => '#00FFFF', + 'aquamarine' => '#7FFFD4', + 'beige' => '#F5F5DC', + 'black' => '#000000', + 'blue' => '#0000FF', + 'brown' => '#A52A2A', + 'cadetblue' => '#5F9EA0', + 'chocolate' => '#D2691E', + 'cornflowerblue' => '#6495ED', + 'crimson' => '#DC143C', + 'darkblue' => '#00008B', + 'darkgoldenrod' => '#B8860B', + 'darkgreen' => '#006400', + 'darkmagenta' => '#8B008B', + 'darkorange' => '#FF8C00', + 'darkred' => '#8B0000', + 'darkseagreen' => '#8FBC8F', + 'darkslategray' => '#2F4F4F', + 'darkviolet' => '#9400D3', + 'deepskyblue' => '#00BFFF', + 'dodgerblue' => '#1E90FF', + 'firebrick' => '#B22222', + 'forestgreen' => '#228B22', + 'fuchsia' => '#FF00FF', + 'gainsboro' => '#DCDCDC', + 'gold' => '#FFD700', + 'gray' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#ADFF2F', + 'hotpink' => '#FF69B4', + 'indigo' => '#4B0082', + 'khaki' => '#F0E68C', + 'lavenderblush' => '#FFF0F5', + 'lemonchiffon' => '#FFFACD', + 'lightcoral' => '#F08080', + 'lightgoldenrodyellow' => '#FAFAD2', + 'lightgreen' => '#90EE90', + 'lightsalmon' => '#FFA07A', + 'lightskyblue' => '#87CEFA', + 'lightslategray' => '#778899', + 'lightyellow' => '#FFFFE0', + 'lime' => '#00FF00', + 'limegreen' => '#32CD32', + 'magenta' => '#FF00FF', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66CDAA', + 'mediumorchid' => '#BA55D3', + 'mediumseagreen' => '#3CB371', + 'mediumspringgreen' => '#00FA9A', + 'mediumvioletred' => '#C71585', + 'midnightblue' => '#191970', + 'mintcream' => '#F5FFFA', + 'moccasin' => '#FFE4B5', + 'navy' => '#000080', + 'olive' => '#808000', + 'orange' => '#FFA500', + 'orchid' => '#DA70D6', + 'palegreen' => '#98FB98', + 'palevioletred' => '#D87093', + 'peachpuff' => '#FFDAB9', + 'pink' => '#FFC0CB', + 'powderblue' => '#B0E0E6', + 'purple' => '#800080', + 'red' => '#FF0000', + 'royalblue' => '#4169E1', + 'salmon' => '#FA8072', + 'seagreen' => '#2E8B57', + 'sienna' => '#A0522D', + 'silver' => '#C0C0C0', + 'skyblue' => '#87CEEB', + 'slategray' => '#708090', + 'springgreen' => '#00FF7F', + 'steelblue' => '#4682B4', + 'tan' => '#D2B48C', + 'teal' => '#008080', + 'thistle' => '#D8BFD8', + 'turquoise' => '#40E0D0', + 'violetred' => '#D02090', + 'white' => '#FFFFFF', + 'yellow' => '#FFFF00', + 'aliceblue' => '#f0f8ff', + 'azure' => '#f0ffff', + 'bisque' => '#ffe4c4', + 'blanchedalmond' => '#ffebcd', + 'blueviolet' => '#8a2be2', + 'burlywood' => '#deb887', + 'chartreuse' => '#7fff00', + 'coral' => '#ff7f50', + 'cornsilk' => '#fff8dc', + 'cyan' => '#00ffff', + 'darkcyan' => '#008b8b', + 'darkgray' => '#a9a9a9', + 'darkgrey' => '#a9a9a9', + 'darkkhaki' => '#bdb76b', + 'darkolivegreen' => '#556b2f', + 'darkorchid' => '#9932cc', + 'darksalmon' => '#e9967a', + 'darkslateblue' => '#483d8b', + 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', + 'deeppink' => '#ff1493', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'floralwhite' => '#fffaf0', + 'ghostwhite' => '#f8f8ff', + 'goldenrod' => '#daa520', + 'grey' => '#808080', + 'honeydew' => '#f0fff0', + 'indianred' => '#cd5c5c', + 'ivory' => '#fffff0', + 'lavender' => '#e6e6fa', + 'lawngreen' => '#7cfc00', + 'lightblue' => '#add8e6', + 'lightcyan' => '#e0ffff', + 'lightgray' => '#d3d3d3', + 'lightgrey' => '#d3d3d3', + 'lightpink' => '#ffb6c1', + 'lightseagreen' => '#20b2aa', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#b0c4de', + 'linen' => '#faf0e6', + 'mediumblue' => '#0000cd', + 'mediumpurple' => '#9370db', + 'mediumslateblue' => '#7b68ee', + 'mediumturquoise' => '#48d1cc', + 'mistyrose' => '#ffe4e1', + 'navajowhite' => '#ffdead', + 'oldlace' => '#fdf5e6', + 'olivedrab' => '#6b8e23', + 'orangered' => '#ff4500', + 'palegoldenrod' => '#eee8aa', + 'paleturquoise' => '#afeeee', + 'papayawhip' => '#ffefd5', + 'peru' => '#cd853f', + 'plum' => '#dda0dd', + 'rosybrown' => '#bc8f8f', + 'saddlebrown' => '#8b4513', + 'sandybrown' => '#f4a460', + 'seashell' => '#fff5ee', + 'slateblue' => '#6a5acd', + 'slategrey' => '#708090', + 'snow' => '#fffafa', + 'tomato' => '#ff6347', + 'violet' => '#ee82ee', + 'wheat' => '#f5deb3', + 'whitesmoke' => '#f5f5f5', + 'yellowgreen' => '#9acd32', + ); +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Surface/CPdf.php b/vendor/phenx/php-svg-lib/src/Svg/Surface/CPdf.php new file mode 100644 index 0000000..caa28a8 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Surface/CPdf.php @@ -0,0 +1,6418 @@ +<?php +/** + * A PHP class to provide the basic functionality to create a pdf document without + * any requirement for additional modules. + * + * Extended by Orion Richardson to support Unicode / UTF-8 characters using + * TCPDF and others as a guide. + * + * @author Wayne Munro <pdf@ros.co.nz> + * @author Orion Richardson <orionr@yahoo.com> + * @author Helmut Tischer <htischer@weihenstephan.org> + * @author Ryan H. Masten <ryan.masten@gmail.com> + * @author Brian Sweeney <eclecticgeek@gmail.com> + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license Public Domain http://creativecommons.org/licenses/publicdomain/ + * @package Cpdf + */ + +namespace Svg\Surface; + +class CPdf +{ + const PDF_VERSION = '1.7'; + + const ACROFORM_SIG_SIGNATURESEXISTS = 0x0001; + const ACROFORM_SIG_APPENDONLY = 0x0002; + + const ACROFORM_FIELD_BUTTON = 'Btn'; + const ACROFORM_FIELD_TEXT = 'Tx'; + const ACROFORM_FIELD_CHOICE = 'Ch'; + const ACROFORM_FIELD_SIG = 'Sig'; + + const ACROFORM_FIELD_READONLY = 0x0001; + const ACROFORM_FIELD_REQUIRED = 0x0002; + + const ACROFORM_FIELD_TEXT_MULTILINE = 0x1000; + const ACROFORM_FIELD_TEXT_PASSWORD = 0x2000; + const ACROFORM_FIELD_TEXT_RICHTEXT = 0x10000; + + const ACROFORM_FIELD_CHOICE_COMBO = 0x20000; + const ACROFORM_FIELD_CHOICE_EDIT = 0x40000; + const ACROFORM_FIELD_CHOICE_SORT = 0x80000; + const ACROFORM_FIELD_CHOICE_MULTISELECT = 0x200000; + + const XOBJECT_SUBTYPE_FORM = 'Form'; + + /** + * @var integer The current number of pdf objects in the document + */ + public $numObj = 0; + + /** + * @var array This array contains all of the pdf objects, ready for final assembly + */ + public $objects = []; + + /** + * @var integer The objectId (number within the objects array) of the document catalog + */ + public $catalogId; + + /** + * @var integer The objectId (number within the objects array) of indirect references (Javascript EmbeddedFiles) + */ + protected $indirectReferenceId = 0; + + /** + * @var integer The objectId (number within the objects array) + */ + protected $embeddedFilesId = 0; + + /** + * AcroForm objectId + * + * @var integer + */ + public $acroFormId; + + /** + * @var int + */ + public $signatureMaxLen = 5000; + + /** + * @var array Array carrying information about the fonts that the system currently knows about + * Used to ensure that a font is not loaded twice, among other things + */ + public $fonts = []; + + /** + * @var string The default font metrics file to use if no other font has been loaded. + * The path to the directory containing the font metrics should be included + */ + public $defaultFont = './fonts/Helvetica.afm'; + + /** + * @string A record of the current font + */ + public $currentFont = ''; + + /** + * @var string The current base font + */ + public $currentBaseFont = ''; + + /** + * @var integer The number of the current font within the font array + */ + public $currentFontNum = 0; + + /** + * @var integer + */ + public $currentNode; + + /** + * @var integer Object number of the current page + */ + public $currentPage; + + /** + * @var integer Object number of the currently active contents block + */ + public $currentContents; + + /** + * @var integer Number of fonts within the system + */ + public $numFonts = 0; + + /** + * @var integer Number of graphic state resources used + */ + private $numStates = 0; + + /** + * @var array Number of graphic state resources used + */ + private $gstates = []; + + /** + * @var array Current color for fill operations, defaults to inactive value, + * all three components should be between 0 and 1 inclusive when active + */ + public $currentColor = null; + + /** + * @var array Current color for stroke operations (lines etc.) + */ + public $currentStrokeColor = null; + + /** + * @var string Fill rule (nonzero or evenodd) + */ + public $fillRule = "nonzero"; + + /** + * @var string Current style that lines are drawn in + */ + public $currentLineStyle = ''; + + /** + * @var array Current line transparency (partial graphics state) + */ + public $currentLineTransparency = ["mode" => "Normal", "opacity" => 1.0]; + + /** + * array Current fill transparency (partial graphics state) + */ + public $currentFillTransparency = ["mode" => "Normal", "opacity" => 1.0]; + + /** + * @var array An array which is used to save the state of the document, mainly the colors and styles + * it is used to temporarily change to another state, then change back to what it was before + */ + public $stateStack = []; + + /** + * @var integer Number of elements within the state stack + */ + public $nStateStack = 0; + + /** + * @var integer Number of page objects within the document + */ + public $numPages = 0; + + /** + * @var array Object Id storage stack + */ + public $stack = []; + + /** + * @var integer Number of elements within the object Id storage stack + */ + public $nStack = 0; + + /** + * an array which contains information about the objects which are not firmly attached to pages + * these have been added with the addObject function + */ + public $looseObjects = []; + + /** + * array contains information about how the loose objects are to be added to the document + */ + public $addLooseObjects = []; + + /** + * @var integer The objectId of the information object for the document + * this contains authorship, title etc. + */ + public $infoObject = 0; + + /** + * @var integer Number of images being tracked within the document + */ + public $numImages = 0; + + /** + * @var array An array containing options about the document + * it defaults to turning on the compression of the objects + */ + public $options = ['compression' => true]; + + /** + * @var integer The objectId of the first page of the document + */ + public $firstPageId; + + /** + * @var integer The object Id of the procset object + */ + public $procsetObjectId; + + /** + * @var array Store the information about the relationship between font families + * this used so that the code knows which font is the bold version of another font, etc. + * the value of this array is initialised in the constructor function. + */ + public $fontFamilies = []; + + /** + * @var string Folder for php serialized formats of font metrics files. + * If empty string, use same folder as original metrics files. + * This can be passed in from class creator. + * If this folder does not exist or is not writable, Cpdf will be **much** slower. + * Because of potential trouble with php safe mode, folder cannot be created at runtime. + */ + public $fontcache = ''; + + /** + * @var integer The version of the font metrics cache file. + * This value must be manually incremented whenever the internal font data structure is modified. + */ + public $fontcacheVersion = 6; + + /** + * @var string Temporary folder. + * If empty string, will attempt system tmp folder. + * This can be passed in from class creator. + */ + public $tmp = ''; + + /** + * @var string Track if the current font is bolded or italicised + */ + public $currentTextState = ''; + + /** + * @var string Messages are stored here during processing, these can be selected afterwards to give some useful debug information + */ + public $messages = ''; + + /** + * @var string The encryption array for the document encryption is stored here + */ + public $arc4 = ''; + + /** + * @var integer The object Id of the encryption information + */ + public $arc4_objnum = 0; + + /** + * @var string The file identifier, used to uniquely identify a pdf document + */ + public $fileIdentifier = ''; + + /** + * @var boolean A flag to say if a document is to be encrypted or not + */ + public $encrypted = false; + + /** + * @var string The encryption key for the encryption of all the document content (structure is not encrypted) + */ + public $encryptionKey = ''; + + /** + * @var array Array which forms a stack to keep track of nested callback functions + */ + public $callback = []; + + /** + * @var integer The number of callback functions in the callback array + */ + public $nCallback = 0; + + /** + * @var array Store label->id pairs for named destinations, these will be used to replace internal links + * done this way so that destinations can be defined after the location that links to them + */ + public $destinations = []; + + /** + * @var array Store the stack for the transaction commands, each item in here is a record of the values of all the + * publiciables within the class, so that the user can rollback at will (from each 'start' command) + * note that this includes the objects array, so these can be large. + */ + public $checkpoint = ''; + + /** + * @var array Table of Image origin filenames and image labels which were already added with o_image(). + * Allows to merge identical images + */ + public $imagelist = []; + + /** + * @var array Table of already added alpha and plain image files for transparent PNG images. + */ + protected $imageAlphaList = []; + + /** + * @var array List of temporary image files to be deleted after processing. + */ + protected $imageCache = []; + + /** + * @var boolean Whether the text passed in should be treated as Unicode or just local character set. + */ + public $isUnicode = false; + + /** + * @var string the JavaScript code of the document + */ + public $javascript = ''; + + /** + * @var boolean whether the compression is possible + */ + protected $compressionReady = false; + + /** + * @var array Current page size + */ + protected $currentPageSize = ["width" => 0, "height" => 0]; + + /** + * @var array All the chars that will be required in the font subsets + */ + protected $stringSubsets = []; + + /** + * @var string The target internal encoding + */ + protected static $targetEncoding = 'Windows-1252'; + + /** + * @var array + */ + protected $byteRange = array(); + + /** + * @var array The list of the core fonts + */ + protected static $coreFonts = [ + 'courier', + 'courier-bold', + 'courier-oblique', + 'courier-boldoblique', + 'helvetica', + 'helvetica-bold', + 'helvetica-oblique', + 'helvetica-boldoblique', + 'times-roman', + 'times-bold', + 'times-italic', + 'times-bolditalic', + 'symbol', + 'zapfdingbats' + ]; + + /** + * Class constructor + * This will start a new document + * + * @param array $pageSize Array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero. + * @param boolean $isUnicode Whether text will be treated as Unicode or not. + * @param string $fontcache The font cache folder + * @param string $tmp The temporary folder + */ + function __construct($pageSize = [0, 0, 612, 792], $isUnicode = false, $fontcache = '', $tmp = '') + { + $this->isUnicode = $isUnicode; + $this->fontcache = rtrim($fontcache, DIRECTORY_SEPARATOR."/\\"); + $this->tmp = ($tmp !== '' ? $tmp : sys_get_temp_dir()); + $this->newDocument($pageSize); + + $this->compressionReady = function_exists('gzcompress'); + + if (in_array('Windows-1252', mb_list_encodings())) { + self::$targetEncoding = 'Windows-1252'; + } + + // also initialize the font families that are known about already + $this->setFontFamily('init'); + } + + public function __destruct() + { + foreach ($this->imageCache as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + + /** + * Document object methods (internal use only) + * + * There is about one object method for each type of object in the pdf document + * Each function has the same call list ($id,$action,$options). + * $id = the object ID of the object, or what it is to be if it is being created + * $action = a string specifying the action to be performed, though ALL must support: + * 'new' - create the object with the id $id + * 'out' - produce the output for the pdf object + * $options = optional, a string or array containing the various parameters for the object + * + * These, in conjunction with the output function are the ONLY way for output to be produced + * within the pdf 'file'. + */ + + /** + * Destination object, used to specify the location for the user to jump to, presently on opening + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_destination($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'destination', 'info' => []]; + $tmp = ''; + switch ($options['type']) { + case 'XYZ': + /** @noinspection PhpMissingBreakStatementInspection */ + case 'FitR': + $tmp = ' ' . $options['p3'] . $tmp; + case 'FitH': + case 'FitV': + case 'FitBH': + /** @noinspection PhpMissingBreakStatementInspection */ + case 'FitBV': + $tmp = ' ' . $options['p1'] . ' ' . $options['p2'] . $tmp; + case 'Fit': + case 'FitB': + $tmp = $options['type'] . $tmp; + $this->objects[$id]['info']['string'] = $tmp; + $this->objects[$id]['info']['page'] = $options['page']; + } + break; + + case 'out': + $o = &$this->objects[$id]; + + $tmp = $o['info']; + $res = "\n$id 0 obj\n" . '[' . $tmp['page'] . ' 0 R /' . $tmp['string'] . "]\nendobj"; + + return $res; + } + + return null; + } + + /** + * set the viewer preferences + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + */ + protected function o_viewerPreferences($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'viewerPreferences', 'info' => []]; + break; + + case 'add': + $o = &$this->objects[$id]; + + foreach ($options as $k => $v) { + switch ($k) { + // Boolean keys + case 'HideToolbar': + case 'HideMenubar': + case 'HideWindowUI': + case 'FitWindow': + case 'CenterWindow': + case 'DisplayDocTitle': + case 'PickTrayByPDFSize': + $o['info'][$k] = (bool)$v; + break; + + // Integer keys + case 'NumCopies': + $o['info'][$k] = (int)$v; + break; + + // Name keys + case 'ViewArea': + case 'ViewClip': + case 'PrintClip': + case 'PrintArea': + $o['info'][$k] = (string)$v; + break; + + // Named with limited valid values + case 'NonFullScreenPageMode': + if (!in_array($v, ['UseNone', 'UseOutlines', 'UseThumbs', 'UseOC'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'Direction': + if (!in_array($v, ['L2R', 'R2L'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'PrintScaling': + if (!in_array($v, ['None', 'AppDefault'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'Duplex': + if (!in_array($v, ['None', 'Simplex', 'DuplexFlipShortEdge', 'DuplexFlipLongEdge'])) { + break; + } + $o['info'][$k] = $v; + break; + + // Integer array + case 'PrintPageRange': + // Cast to integer array + foreach ($v as $vK => $vV) { + $v[$vK] = (int)$vV; + } + $o['info'][$k] = array_values($v); + break; + } + } + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + + foreach ($o['info'] as $k => $v) { + if (is_string($v)) { + $v = '/' . $v; + } elseif (is_int($v)) { + $v = (string) $v; + } elseif (is_bool($v)) { + $v = ($v ? 'true' : 'false'); + } elseif (is_array($v)) { + $v = '[' . implode(' ', $v) . ']'; + } + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * define the document catalog, the overall controller for the document + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + */ + protected function o_catalog($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'catalog', 'info' => []]; + $this->catalogId = $id; + break; + + case 'acroform': + case 'outlines': + case 'pages': + case 'openHere': + case 'names': + $o['info'][$action] = $options; + break; + + case 'viewerPreferences': + if (!isset($o['info']['viewerPreferences'])) { + $this->numObj++; + $this->o_viewerPreferences($this->numObj, 'new'); + $o['info']['viewerPreferences'] = $this->numObj; + } + + $vp = $o['info']['viewerPreferences']; + $this->o_viewerPreferences($vp, 'add', $options); + + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Catalog"; + + foreach ($o['info'] as $k => $v) { + switch ($k) { + case 'outlines': + $res .= "\n/Outlines $v 0 R"; + break; + + case 'pages': + $res .= "\n/Pages $v 0 R"; + break; + + case 'viewerPreferences': + $res .= "\n/ViewerPreferences $v 0 R"; + break; + + case 'openHere': + $res .= "\n/OpenAction $v 0 R"; + break; + + case 'names': + $res .= "\n/Names $v 0 R"; + break; + + case 'acroform': + $res .= "\n/AcroForm $v 0 R"; + break; + } + } + + $res .= " >>\nendobj"; + + return $res; + } + + return null; + } + + /** + * object which is a parent to the pages in the document + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_pages($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'pages', 'info' => []]; + $this->o_catalog($this->catalogId, 'pages', $id); + break; + + case 'page': + if (!is_array($options)) { + // then it will just be the id of the new page + $o['info']['pages'][] = $options; + } else { + // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative + // and pos is either 'before' or 'after', saying where this page will fit. + if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])) { + $i = array_search($options['rid'], $o['info']['pages']); + if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i] == $options['rid']) { + + // then there is a match + // make a space + switch ($options['pos']) { + case 'before': + $k = $i; + break; + + case 'after': + $k = $i + 1; + break; + + default: + $k = -1; + break; + } + + if ($k >= 0) { + for ($j = count($o['info']['pages']) - 1; $j >= $k; $j--) { + $o['info']['pages'][$j + 1] = $o['info']['pages'][$j]; + } + + $o['info']['pages'][$k] = $options['id']; + } + } + } + } + break; + + case 'procset': + $o['info']['procset'] = $options; + break; + + case 'mediaBox': + $o['info']['mediaBox'] = $options; + // which should be an array of 4 numbers + $this->currentPageSize = ['width' => $options[2], 'height' => $options[3]]; + break; + + case 'font': + $o['info']['fonts'][] = ['objNum' => $options['objNum'], 'fontNum' => $options['fontNum']]; + break; + + case 'extGState': + $o['info']['extGStates'][] = ['objNum' => $options['objNum'], 'stateNum' => $options['stateNum']]; + break; + + case 'xObject': + $o['info']['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']]; + break; + + case 'out': + if (count($o['info']['pages'])) { + $res = "\n$id 0 obj\n<< /Type /Pages\n/Kids ["; + foreach ($o['info']['pages'] as $v) { + $res .= "$v 0 R\n"; + } + + $res .= "]\n/Count " . count($this->objects[$id]['info']['pages']); + + if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || + isset($o['info']['procset']) || + (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) + ) { + $res .= "\n/Resources <<"; + + if (isset($o['info']['procset'])) { + $res .= "\n/ProcSet " . $o['info']['procset'] . " 0 R"; + } + + if (isset($o['info']['fonts']) && count($o['info']['fonts'])) { + $res .= "\n/Font << "; + foreach ($o['info']['fonts'] as $finfo) { + $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])) { + $res .= "\n/XObject << "; + foreach ($o['info']['xObjects'] as $finfo) { + $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + if (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) { + $res .= "\n/ExtGState << "; + foreach ($o['info']['extGStates'] as $gstate) { + $res .= "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + $res .= "\n>>"; + if (isset($o['info']['mediaBox'])) { + $tmp = $o['info']['mediaBox']; + $res .= "\n/MediaBox [" . sprintf( + '%.3F %.3F %.3F %.3F', + $tmp[0], + $tmp[1], + $tmp[2], + $tmp[3] + ) . ']'; + } + } + + $res .= "\n >>\nendobj"; + } else { + $res = "\n$id 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj"; + } + + return $res; + } + + return null; + } + + /** + * define the outlines in the doc, empty for now + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_outlines($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'outlines', 'info' => ['outlines' => []]]; + $this->o_catalog($this->catalogId, 'outlines', $id); + break; + + case 'outline': + $o['info']['outlines'][] = $options; + break; + + case 'out': + if (count($o['info']['outlines'])) { + $res = "\n$id 0 obj\n<< /Type /Outlines /Kids ["; + foreach ($o['info']['outlines'] as $v) { + $res .= "$v 0 R "; + } + + $res .= "] /Count " . count($o['info']['outlines']) . " >>\nendobj"; + } else { + $res = "\n$id 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj"; + } + + return $res; + } + + return null; + } + + /** + * an object to hold the font description + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + * @throws FontNotFoundException + */ + protected function o_font($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'font', + 'info' => [ + 'name' => $options['name'], + 'fontFileName' => $options['fontFileName'], + 'SubType' => 'Type1', + 'isSubsetting' => $options['isSubsetting'] + ] + ]; + $fontNum = $this->numFonts; + $this->objects[$id]['info']['fontNum'] = $fontNum; + + // deal with the encoding and the differences + if (isset($options['differences'])) { + // then we'll need an encoding dictionary + $this->numObj++; + $this->o_fontEncoding($this->numObj, 'new', $options); + $this->objects[$id]['info']['encodingDictionary'] = $this->numObj; + } else { + if (isset($options['encoding'])) { + // we can specify encoding here + switch ($options['encoding']) { + case 'WinAnsiEncoding': + case 'MacRomanEncoding': + case 'MacExpertEncoding': + $this->objects[$id]['info']['encoding'] = $options['encoding']; + break; + + case 'none': + break; + + default: + $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; + break; + } + } else { + $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; + } + } + + if ($this->fonts[$options['fontFileName']]['isUnicode']) { + // For Unicode fonts, we need to incorporate font data into + // sub-sections that are linked from the primary font section. + // Look at o_fontGIDtoCID and o_fontDescendentCID functions + // for more information. + // + // All of this code is adapted from the excellent changes made to + // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) + + $toUnicodeId = ++$this->numObj; + $this->o_toUnicode($toUnicodeId, 'new'); + $this->objects[$id]['info']['toUnicode'] = $toUnicodeId; + + $cidFontId = ++$this->numObj; + $this->o_fontDescendentCID($cidFontId, 'new', $options); + $this->objects[$id]['info']['cidFont'] = $cidFontId; + } + + // also tell the pages node about the new font + $this->o_pages($this->currentNode, 'font', ['fontNum' => $fontNum, 'objNum' => $id]); + break; + + case 'add': + $font_options = $this->processFont($id, $o['info']); + + if ($font_options !== false) { + foreach ($font_options as $k => $v) { + switch ($k) { + case 'BaseFont': + $o['info']['name'] = $v; + break; + case 'FirstChar': + case 'LastChar': + case 'Widths': + case 'FontDescriptor': + case 'SubType': + $this->addMessage('o_font ' . $k . " : " . $v); + $o['info'][$k] = $v; + break; + } + } + + // pass values down to descendent font + if (isset($o['info']['cidFont'])) { + $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $font_options); + } + } + break; + + case 'out': + if ($this->fonts[$this->objects[$id]['info']['fontFileName']]['isUnicode']) { + // For Unicode fonts, we need to incorporate font data into + // sub-sections that are linked from the primary font section. + // Look at o_fontGIDtoCID and o_fontDescendentCID functions + // for more information. + // + // All of this code is adapted from the excellent changes made to + // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) + + $res = "\n$id 0 obj\n<</Type /Font\n/Subtype /Type0\n"; + $res .= "/BaseFont /" . $o['info']['name'] . "\n"; + + // The horizontal identity mapping for 2-byte CIDs; may be used + // with CIDFonts using any Registry, Ordering, and Supplement values. + $res .= "/Encoding /Identity-H\n"; + $res .= "/DescendantFonts [" . $o['info']['cidFont'] . " 0 R]\n"; + $res .= "/ToUnicode " . $o['info']['toUnicode'] . " 0 R\n"; + $res .= ">>\n"; + $res .= "endobj"; + } else { + $res = "\n$id 0 obj\n<< /Type /Font\n/Subtype /" . $o['info']['SubType'] . "\n"; + $res .= "/Name /F" . $o['info']['fontNum'] . "\n"; + $res .= "/BaseFont /" . $o['info']['name'] . "\n"; + + if (isset($o['info']['encodingDictionary'])) { + // then place a reference to the dictionary + $res .= "/Encoding " . $o['info']['encodingDictionary'] . " 0 R\n"; + } else { + if (isset($o['info']['encoding'])) { + // use the specified encoding + $res .= "/Encoding /" . $o['info']['encoding'] . "\n"; + } + } + + if (isset($o['info']['FirstChar'])) { + $res .= "/FirstChar " . $o['info']['FirstChar'] . "\n"; + } + + if (isset($o['info']['LastChar'])) { + $res .= "/LastChar " . $o['info']['LastChar'] . "\n"; + } + + if (isset($o['info']['Widths'])) { + $res .= "/Widths " . $o['info']['Widths'] . " 0 R\n"; + } + + if (isset($o['info']['FontDescriptor'])) { + $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n"; + } + + $res .= ">>\n"; + $res .= "endobj"; + } + + return $res; + } + + return null; + } + + protected function getFontSubsettingTag(array $font): string + { + // convert font num to hexavigesimal numeral system letters A - Z only + $base_26 = strtoupper(base_convert($font['fontNum'], 10, 26)); + for ($i = 0; $i < strlen($base_26); $i++) { + $char = $base_26[$i]; + if ($char <= "9") { + $base_26[$i] = chr(65 + intval($char)); + } else { + $base_26[$i] = chr(ord($char) + 10); + } + } + + return 'SUB' . str_pad($base_26, 3 , 'A', STR_PAD_LEFT); + } + + /** + * @param int $fontObjId + * @param array $object_info + * @return array|false + * @throws FontNotFoundException + */ + private function processFont(int $fontObjId, array $object_info) + { + $fontFileName = $object_info['fontFileName']; + if (!isset($this->fonts[$fontFileName])) { + return false; + } + + $font = &$this->fonts[$fontFileName]; + + $fileSuffix = $font['fileSuffix']; + $fileSuffixLower = strtolower($font['fileSuffix']); + $fbfile = "$fontFileName.$fileSuffix"; + $isTtfFont = $fileSuffixLower === 'ttf'; + $isPfbFont = $fileSuffixLower === 'pfb'; + + $this->addMessage('selectFont: checking for - ' . $fbfile); + + if (!$fileSuffix) { + $this->addMessage( + 'selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts' + ); + + return false; + } else { + $adobeFontName = isset($font['PostScriptName']) ? $font['PostScriptName'] : $font['FontName']; + // $fontObj = $this->numObj; + $this->addMessage("selectFont: adding font file - $fbfile - $adobeFontName"); + + // find the array of font widths, and put that into an object. + $firstChar = -1; + $lastChar = 0; + $widths = []; + $cid_widths = []; + + foreach ($font['C'] as $num => $d) { + if (intval($num) > 0 || $num == '0') { + if (!$font['isUnicode']) { + // With Unicode, widths array isn't used + if ($lastChar > 0 && $num > $lastChar + 1) { + for ($i = $lastChar + 1; $i < $num; $i++) { + $widths[] = 0; + } + } + } + + $widths[] = $d; + + if ($font['isUnicode']) { + $cid_widths[$num] = $d; + } + + if ($firstChar == -1) { + $firstChar = $num; + } + + $lastChar = $num; + } + } + + // also need to adjust the widths for the differences array + if (isset($object['differences'])) { + foreach ($object['differences'] as $charNum => $charName) { + if ($charNum > $lastChar) { + if (!$object['isUnicode']) { + // With Unicode, widths array isn't used + for ($i = $lastChar + 1; $i <= $charNum; $i++) { + $widths[] = 0; + } + } + + $lastChar = $charNum; + } + + if (isset($font['C'][$charName])) { + $widths[$charNum - $firstChar] = $font['C'][$charName]; + if ($font['isUnicode']) { + $cid_widths[$charName] = $font['C'][$charName]; + } + } + } + } + + if ($font['isUnicode']) { + $font['CIDWidths'] = $cid_widths; + } + + $this->addMessage('selectFont: FirstChar = ' . $firstChar); + $this->addMessage('selectFont: LastChar = ' . $lastChar); + + $widthid = -1; + + if (!$font['isUnicode']) { + // With Unicode, widths array isn't used + + $this->numObj++; + $this->o_contents($this->numObj, 'new', 'raw'); + $this->objects[$this->numObj]['c'] .= '[' . implode(' ', $widths) . ']'; + $widthid = $this->numObj; + } + + $missing_width = 500; + $stemV = 70; + + if (isset($font['MissingWidth'])) { + $missing_width = $font['MissingWidth']; + } + if (isset($font['StdVW'])) { + $stemV = $font['StdVW']; + } else { + if (isset($font['Weight']) && preg_match('!(bold|black)!i', $font['Weight'])) { + $stemV = 120; + } + } + + // load the pfb file, and put that into an object too. + // note that pdf supports only binary format type 1 font files, though there is a + // simple utility to convert them from pfa to pfb. + $data = file_get_contents($fbfile); + + // create the font descriptor + $this->numObj++; + $fontDescriptorId = $this->numObj; + + $this->numObj++; + $pfbid = $this->numObj; + + // determine flags (more than a little flakey, hopefully will not matter much) + $flags = 0; + + if ($font['ItalicAngle'] != 0) { + $flags += pow(2, 6); + } + + if ($font['IsFixedPitch'] === 'true') { + $flags += 1; + } + + $flags += pow(2, 5); // assume non-sybolic + $list = [ + 'Ascent' => 'Ascender', + 'CapHeight' => 'Ascender', //FIXME: php-font-lib is not grabbing this value, so we'll fake it and use the Ascender value // 'CapHeight' + 'MissingWidth' => 'MissingWidth', + 'Descent' => 'Descender', + 'FontBBox' => 'FontBBox', + 'ItalicAngle' => 'ItalicAngle' + ]; + $fdopt = [ + 'Flags' => $flags, + 'FontName' => $adobeFontName, + 'StemV' => $stemV + ]; + + foreach ($list as $k => $v) { + if (isset($font[$v])) { + $fdopt[$k] = $font[$v]; + } + } + + if ($isPfbFont) { + $fdopt['FontFile'] = $pfbid; + } elseif ($isTtfFont) { + $fdopt['FontFile2'] = $pfbid; + } + + $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt); + + // embed the font program + $this->o_contents($this->numObj, 'new'); + $this->objects[$pfbid]['c'] .= $data; + + // determine the cruicial lengths within this file + if ($isPfbFont) { + $l1 = strpos($data, 'eexec') + 6; + $l2 = strpos($data, '00000000') - $l1; + $l3 = mb_strlen($data, '8bit') - $l2 - $l1; + $this->o_contents( + $this->numObj, + 'add', + ['Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3] + ); + } elseif ($isTtfFont) { + $l1 = mb_strlen($data, '8bit'); + $this->o_contents($this->numObj, 'add', ['Length1' => $l1]); + } + + // tell the font object about all this new stuff + $options = [ + 'BaseFont' => $adobeFontName, + 'MissingWidth' => $missing_width, + 'Widths' => $widthid, + 'FirstChar' => $firstChar, + 'LastChar' => $lastChar, + 'FontDescriptor' => $fontDescriptorId + ]; + + if ($isTtfFont) { + $options['SubType'] = 'TrueType'; + } + + $this->addMessage("adding extra info to font.($fontObjId)"); + + foreach ($options as $fk => $fv) { + $this->addMessage("$fk : $fv"); + } + } + + return $options; + } + + /** + * A toUnicode section, needed for unicode fonts + * + * @param $id + * @param $action + * @return null|string + */ + protected function o_toUnicode($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'toUnicode' + ]; + break; + case 'add': + break; + case 'out': + $ordering = 'UCS'; + $registry = 'Adobe'; + + if ($this->encrypted) { + $this->encryptInit($id); + $ordering = $this->ARC4($ordering); + $registry = $this->filterText($this->ARC4($registry), false, false); + } + + $stream = <<<EOT +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<</Registry ($registry) +/Ordering ($ordering) +/Supplement 0 +>> def +/CMapName /Adobe-Identity-UCS def +/CMapType 2 def +1 begincodespacerange +<0000> <FFFF> +endcodespacerange +1 beginbfrange +<0000> <FFFF> <0000> +endbfrange +endcmap +CMapName currentdict /CMap defineresource pop +end +end +EOT; + + $res = "\n$id 0 obj\n"; + $res .= "<</Length " . mb_strlen($stream, '8bit') . " >>\n"; + $res .= "stream\n" . $stream . "\nendstream" . "\nendobj";; + + return $res; + } + + return null; + } + + /** + * a font descriptor, needed for including additional fonts + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_fontDescriptor($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontDescriptor', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /FontDescriptor\n"; + foreach ($o['info'] as $label => $value) { + switch ($label) { + case 'Ascent': + case 'CapHeight': + case 'Descent': + case 'Flags': + case 'ItalicAngle': + case 'StemV': + case 'AvgWidth': + case 'Leading': + case 'MaxWidth': + case 'MissingWidth': + case 'StemH': + case 'XHeight': + case 'CharSet': + if (mb_strlen($value, '8bit')) { + $res .= "/$label $value\n"; + } + + break; + case 'FontFile': + case 'FontFile2': + case 'FontFile3': + $res .= "/$label $value 0 R\n"; + break; + + case 'FontBBox': + $res .= "/$label [$value[0] $value[1] $value[2] $value[3]]\n"; + break; + + case 'FontName': + $res .= "/$label /$value\n"; + break; + } + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * the font encoding + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_fontEncoding($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + // the options array should contain 'differences' and maybe 'encoding' + $this->objects[$id] = ['t' => 'fontEncoding', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Encoding\n"; + if (!isset($o['info']['encoding'])) { + $o['info']['encoding'] = 'WinAnsiEncoding'; + } + + if ($o['info']['encoding'] !== 'none') { + $res .= "/BaseEncoding /" . $o['info']['encoding'] . "\n"; + } + + $res .= "/Differences \n["; + + $onum = -100; + + foreach ($o['info']['differences'] as $num => $label) { + if ($num != $onum + 1) { + // we cannot make use of consecutive numbering + $res .= "\n$num /$label"; + } else { + $res .= " /$label"; + } + + $onum = $num; + } + + $res .= "\n]\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * a descendent cid font, needed for unicode fonts + * + * @param $id + * @param $action + * @param string|array $options + * @return null|string + */ + protected function o_fontDescendentCID($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontDescendentCID', 'info' => $options]; + + // we need a CID system info section + $cidSystemInfoId = ++$this->numObj; + $this->o_cidSystemInfo($cidSystemInfoId, 'new'); + $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId; + + // and a CID to GID map + $cidToGidMapId = ++$this->numObj; + $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options); + $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId; + break; + + case 'add': + foreach ($options as $k => $v) { + switch ($k) { + case 'BaseFont': + $o['info']['name'] = $v; + break; + + case 'FirstChar': + case 'LastChar': + case 'MissingWidth': + case 'FontDescriptor': + case 'SubType': + $this->addMessage("o_fontDescendentCID $k : $v"); + $o['info'][$k] = $v; + break; + } + } + + // pass values down to cid to gid map + $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options); + break; + + case 'out': + $res = "\n$id 0 obj\n"; + $res .= "<</Type /Font\n"; + $res .= "/Subtype /CIDFontType2\n"; + $res .= "/BaseFont /" . $o['info']['name'] . "\n"; + $res .= "/CIDSystemInfo " . $o['info']['cidSystemInfo'] . " 0 R\n"; + // if (isset($o['info']['FirstChar'])) { + // $res.= "/FirstChar ".$o['info']['FirstChar']."\n"; + // } + + // if (isset($o['info']['LastChar'])) { + // $res.= "/LastChar ".$o['info']['LastChar']."\n"; + // } + if (isset($o['info']['FontDescriptor'])) { + $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n"; + } + + if (isset($o['info']['MissingWidth'])) { + $res .= "/DW " . $o['info']['MissingWidth'] . "\n"; + } + + if (isset($o['info']['fontFileName']) && isset($this->fonts[$o['info']['fontFileName']]['CIDWidths'])) { + $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths']; + $w = ''; + foreach ($cid_widths as $cid => $width) { + $w .= "$cid [$width] "; + } + $res .= "/W [$w]\n"; + } + + $res .= "/CIDToGIDMap " . $o['info']['cidToGidMap'] . " 0 R\n"; + $res .= ">>\n"; + $res .= "endobj"; + + return $res; + } + + return null; + } + + /** + * CID system info section, needed for unicode fonts + * + * @param $id + * @param $action + * @return null|string + */ + protected function o_cidSystemInfo($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'cidSystemInfo' + ]; + break; + case 'add': + break; + case 'out': + $ordering = 'UCS'; + $registry = 'Adobe'; + + if ($this->encrypted) { + $this->encryptInit($id); + $ordering = $this->ARC4($ordering); + $registry = $this->ARC4($registry); + } + + + $res = "\n$id 0 obj\n"; + + $res .= '<</Registry (' . $registry . ")\n"; // A string identifying an issuer of character collections + $res .= '/Ordering (' . $ordering . ")\n"; // A string that uniquely names a character collection issued by a specific registry + $res .= "/Supplement 0\n"; // The supplement number of the character collection. + $res .= ">>"; + + $res .= "\nendobj"; + + return $res; + } + + return null; + } + + /** + * a font glyph to character map, needed for unicode fonts + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_fontGIDtoCIDMap($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontGIDtoCIDMap', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n"; + $fontFileName = $o['info']['fontFileName']; + $tmp = $this->fonts[$fontFileName]['CIDtoGID'] = base64_decode($this->fonts[$fontFileName]['CIDtoGID']); + + $compressed = isset($this->fonts[$fontFileName]['CIDtoGID_Compressed']) && + $this->fonts[$fontFileName]['CIDtoGID_Compressed']; + + if (!$compressed && isset($o['raw'])) { + $res .= $tmp; + } else { + $res .= "<<"; + + if (!$compressed && $this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $compressed = true; + $tmp = gzcompress($tmp, 6); + } + if ($compressed) { + $res .= "\n/Filter /FlateDecode"; + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream"; + } + + $res .= "\nendobj"; + + return $res; + } + + return null; + } + + /** + * the document procset, solves some problems with printing to old PS printers + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_procset($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'procset', 'info' => ['PDF' => 1, 'Text' => 1]]; + $this->o_pages($this->currentNode, 'procset', $id); + $this->procsetObjectId = $id; + break; + + case 'add': + // this is to add new items to the procset list, despite the fact that this is considered + // obsolete, the items are required for printing to some postscript printers + switch ($options) { + case 'ImageB': + case 'ImageC': + case 'ImageI': + $o['info'][$options] = 1; + break; + } + break; + + case 'out': + $res = "\n$id 0 obj\n["; + foreach ($o['info'] as $label => $val) { + $res .= "/$label "; + } + $res .= "]\nendobj"; + + return $res; + } + + return null; + } + + /** + * define the document information + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_info($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->infoObject = $id; + $date = 'D:' . @date('Ymd'); + $this->objects[$id] = [ + 't' => 'info', + 'info' => [ + 'Producer' => 'CPDF (dompdf)', + 'CreationDate' => $date + ] + ]; + break; + case 'Title': + case 'Author': + case 'Subject': + case 'Keywords': + case 'Creator': + case 'Producer': + case 'CreationDate': + case 'ModDate': + case 'Trapped': + $this->objects[$id]['info'][$action] = $options; + break; + + case 'out': + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + $res = "\n$id 0 obj\n<<\n"; + $o = &$this->objects[$id]; + foreach ($o['info'] as $k => $v) { + $res .= "/$k ("; + + // dates must be outputted as-is, without Unicode transformations + if ($k !== 'CreationDate' && $k !== 'ModDate') { + $v = $this->filterText($v, true, false); + } + + if ($encrypted) { + $v = $this->ARC4($v); + } + + $res .= $v; + $res .= ")\n"; + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an action object, used to link to URLS initially + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_action($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + if (is_array($options)) { + $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => $options['type']]; + } else { + // then assume a URI action + $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => 'URI']; + } + break; + + case 'out': + if ($this->encrypted) { + $this->encryptInit($id); + } + + $res = "\n$id 0 obj\n<< /Type /Action"; + switch ($o['type']) { + case 'ilink': + if (!isset($this->destinations[(string)$o['info']['label']])) { + break; + } + + // there will be an 'label' setting, this is the name of the destination + $res .= "\n/S /GoTo\n/D " . $this->destinations[(string)$o['info']['label']] . " 0 R"; + break; + + case 'URI': + $res .= "\n/S /URI\n/URI ("; + if ($this->encrypted) { + $res .= $this->filterText($this->ARC4($o['info']), false, false); + } else { + $res .= $this->filterText($o['info'], false, false); + } + + $res .= ")"; + break; + } + + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an annotation object, this will add an annotation to the current page. + * initially will support just link annotations + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_annotation($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + // add the annotation to the current page + $pageId = $this->currentPage; + $this->o_page($pageId, 'annot', $id); + + // and add the action object which is going to be required + switch ($options['type']) { + case 'link': + $this->objects[$id] = ['t' => 'annotation', 'info' => $options]; + $this->numObj++; + $this->o_action($this->numObj, 'new', $options['url']); + $this->objects[$id]['info']['actionId'] = $this->numObj; + break; + + case 'ilink': + // this is to a named internal link + $label = $options['label']; + $this->objects[$id] = ['t' => 'annotation', 'info' => $options]; + $this->numObj++; + $this->o_action($this->numObj, 'new', ['type' => 'ilink', 'label' => $label]); + $this->objects[$id]['info']['actionId'] = $this->numObj; + break; + } + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Annot"; + switch ($o['info']['type']) { + case 'link': + case 'ilink': + $res .= "\n/Subtype /Link"; + break; + } + $res .= "\n/A " . $o['info']['actionId'] . " 0 R"; + $res .= "\n/Border [0 0 0]"; + $res .= "\n/H /I"; + $res .= "\n/Rect [ "; + + foreach ($o['info']['rect'] as $v) { + $res .= sprintf("%.4F ", $v); + } + + $res .= "]"; + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * a page object, it also creates a contents object to hold its contents + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_page($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->numPages++; + $this->objects[$id] = [ + 't' => 'page', + 'info' => [ + 'parent' => $this->currentNode, + 'pageNum' => $this->numPages, + 'mediaBox' => $this->objects[$this->currentNode]['info']['mediaBox'] + ] + ]; + + if (is_array($options)) { + // then this must be a page insertion, array should contain 'rid','pos'=[before|after] + $options['id'] = $id; + $this->o_pages($this->currentNode, 'page', $options); + } else { + $this->o_pages($this->currentNode, 'page', $id); + } + + $this->currentPage = $id; + //make a contents object to go with this page + $this->numObj++; + $this->o_contents($this->numObj, 'new', $id); + $this->currentContents = $this->numObj; + $this->objects[$id]['info']['contents'] = []; + $this->objects[$id]['info']['contents'][] = $this->numObj; + + $match = ($this->numPages % 2 ? 'odd' : 'even'); + foreach ($this->addLooseObjects as $oId => $target) { + if ($target === 'all' || $match === $target) { + $this->objects[$id]['info']['contents'][] = $oId; + } + } + break; + + case 'content': + $o['info']['contents'][] = $options; + break; + + case 'annot': + // add an annotation to this page + if (!isset($o['info']['annot'])) { + $o['info']['annot'] = []; + } + + // $options should contain the id of the annotation dictionary + $o['info']['annot'][] = $options; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Page"; + if (isset($o['info']['mediaBox'])) { + $tmp = $o['info']['mediaBox']; + $res .= "\n/MediaBox [" . sprintf( + '%.3F %.3F %.3F %.3F', + $tmp[0], + $tmp[1], + $tmp[2], + $tmp[3] + ) . ']'; + } + $res .= "\n/Parent " . $o['info']['parent'] . " 0 R"; + + if (isset($o['info']['annot'])) { + $res .= "\n/Annots ["; + foreach ($o['info']['annot'] as $aId) { + $res .= " $aId 0 R"; + } + $res .= " ]"; + } + + $count = count($o['info']['contents']); + if ($count == 1) { + $res .= "\n/Contents " . $o['info']['contents'][0] . " 0 R"; + } else { + if ($count > 1) { + $res .= "\n/Contents [\n"; + + // reverse the page contents so added objects are below normal content + //foreach (array_reverse($o['info']['contents']) as $cId) { + // Back to normal now that I've got transparency working --Benj + foreach ($o['info']['contents'] as $cId) { + $res .= "$cId 0 R\n"; + } + $res .= "]"; + } + } + + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * the contents objects hold all of the content which appears on pages + * + * @param $id + * @param $action + * @param string|array $options + * @return null|string + */ + protected function o_contents($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'contents', 'c' => '', 'info' => []]; + if (mb_strlen($options, '8bit') && intval($options)) { + // then this contents is the primary for a page + $this->objects[$id]['onPage'] = $options; + } else { + if ($options === 'raw') { + // then this page contains some other type of system object + $this->objects[$id]['raw'] = 1; + } + } + break; + + case 'add': + // add more options to the declaration + foreach ($options as $k => $v) { + $o['info'][$k] = $v; + } + + case 'out': + $tmp = $o['c']; + $res = "\n$id 0 obj\n"; + + if (isset($this->objects[$id]['raw'])) { + $res .= $tmp; + } else { + $res .= "<<"; + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $res .= " /Filter /FlateDecode"; + $tmp = gzcompress($tmp, 6); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . " >>\nstream\n$tmp\nendstream"; + } + + $res .= "\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @return string|null + */ + protected function o_embedjs($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'embedjs', + 'info' => [ + 'Names' => '[(EmbeddedJS) ' . ($id + 1) . ' 0 R]' + ] + ]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param string $code + * @return null|string + */ + protected function o_javascript($id, $action, $code = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'javascript', + 'info' => [ + 'S' => '/JavaScript', + 'JS' => '(' . $this->filterText($code, true, false) . ')', + ] + ]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an image object, will be an XObject in the document, includes description and data + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_image($id, $action, $options = '') + { + switch ($action) { + case 'new': + // make the new object + $this->objects[$id] = ['t' => 'image', 'data' => &$options['data'], 'info' => []]; + + $info =& $this->objects[$id]['info']; + + $info['Type'] = '/XObject'; + $info['Subtype'] = '/Image'; + $info['Width'] = $options['iw']; + $info['Height'] = $options['ih']; + + if (isset($options['masked']) && $options['masked']) { + $info['SMask'] = ($this->numObj - 1) . ' 0 R'; + } + + if (!isset($options['type']) || $options['type'] === 'jpg') { + if (!isset($options['channels'])) { + $options['channels'] = 3; + } + + switch ($options['channels']) { + case 1: + $info['ColorSpace'] = '/DeviceGray'; + break; + case 4: + $info['ColorSpace'] = '/DeviceCMYK'; + break; + default: + $info['ColorSpace'] = '/DeviceRGB'; + break; + } + + if ($info['ColorSpace'] === '/DeviceCMYK') { + $info['Decode'] = '[1 0 1 0 1 0 1 0]'; + } + + $info['Filter'] = '/DCTDecode'; + $info['BitsPerComponent'] = 8; + } else { + if ($options['type'] === 'png') { + $info['Filter'] = '/FlateDecode'; + $info['DecodeParms'] = '<< /Predictor 15 /Colors ' . $options['ncolor'] . ' /Columns ' . $options['iw'] . ' /BitsPerComponent ' . $options['bitsPerComponent'] . '>>'; + + if ($options['isMask']) { + $info['ColorSpace'] = '/DeviceGray'; + } else { + if (mb_strlen($options['pdata'], '8bit')) { + $tmp = ' [ /Indexed /DeviceRGB ' . (mb_strlen($options['pdata'], '8bit') / 3 - 1) . ' '; + $this->numObj++; + $this->o_contents($this->numObj, 'new'); + $this->objects[$this->numObj]['c'] = $options['pdata']; + $tmp .= $this->numObj . ' 0 R'; + $tmp .= ' ]'; + $info['ColorSpace'] = $tmp; + + if (isset($options['transparency'])) { + $transparency = $options['transparency']; + switch ($transparency['type']) { + case 'indexed': + $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] '; + $info['Mask'] = $tmp; + break; + + case 'color-key': + $tmp = ' [ ' . + $transparency['r'] . ' ' . $transparency['r'] . + $transparency['g'] . ' ' . $transparency['g'] . + $transparency['b'] . ' ' . $transparency['b'] . + ' ] '; + $info['Mask'] = $tmp; + break; + } + } + } else { + if (isset($options['transparency'])) { + $transparency = $options['transparency']; + + switch ($transparency['type']) { + case 'indexed': + $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] '; + $info['Mask'] = $tmp; + break; + + case 'color-key': + $tmp = ' [ ' . + $transparency['r'] . ' ' . $transparency['r'] . ' ' . + $transparency['g'] . ' ' . $transparency['g'] . ' ' . + $transparency['b'] . ' ' . $transparency['b'] . + ' ] '; + $info['Mask'] = $tmp; + break; + } + } + $info['ColorSpace'] = '/' . $options['color']; + } + } + + $info['BitsPerComponent'] = $options['bitsPerComponent']; + } + } + + // assign it a place in the named resource dictionary as an external object, according to + // the label passed in with it. + $this->o_pages($this->currentNode, 'xObject', ['label' => $options['label'], 'objNum' => $id]); + + // also make sure that we have the right procset object for it. + $this->o_procset($this->procsetObjectId, 'add', 'ImageC'); + break; + + case 'out': + $o = &$this->objects[$id]; + $tmp = &$o['data']; + $res = "\n$id 0 obj\n<<"; + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream\nendobj"; + + return $res; + } + + return null; + } + + /** + * graphics state object + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_extGState($id, $action, $options = "") + { + static $valid_params = [ + "LW", + "LC", + "LC", + "LJ", + "ML", + "D", + "RI", + "OP", + "op", + "OPM", + "Font", + "BG", + "BG2", + "UCR", + "TR", + "TR2", + "HT", + "FL", + "SM", + "SA", + "BM", + "SMask", + "CA", + "ca", + "AIS", + "TK" + ]; + + switch ($action) { + case "new": + $this->objects[$id] = ['t' => 'extGState', 'info' => $options]; + + // Tell the pages about the new resource + $this->numStates++; + $this->o_pages($this->currentNode, 'extGState', ["objNum" => $id, "stateNum" => $this->numStates]); + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /ExtGState\n"; + + foreach ($o["info"] as $k => $v) { + if (!in_array($k, $valid_params)) { + continue; + } + $res .= "/$k $v\n"; + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param integer $id + * @param string $action + * @param mixed $options + * @return string + */ + protected function o_xobject($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'xobject', 'info' => $options, 'c' => '']; + break; + + case 'procset': + $this->objects[$id]['procset'] = $options; + break; + + case 'font': + $this->objects[$id]['fonts'][$options['fontNum']] = [ + 'objNum' => $options['objNum'], + 'fontNum' => $options['fontNum'] + ]; + break; + + case 'xObject': + $this->objects[$id]['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /XObject\n"; + + foreach ($o["info"] as $k => $v) { + switch($k) + { + case 'Subtype': + $res .= "/Subtype /$v\n"; + break; + case 'bbox': + $res .= "/BBox ["; + foreach ($v as $value) { + $res .= sprintf("%.4F ", $value); + } + $res .= "]\n"; + break; + default: + $res .= "/$k $v\n"; + break; + } + } + $res .= "/Matrix[1.0 0.0 0.0 1.0 0.0 0.0]\n"; + + $res .= "/Resources <<"; + if (isset($o['procset'])) { + $res .= "\n/ProcSet " . $o['procset'] . " 0 R"; + } else { + $res .= "\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"; + } + if (isset($o['fonts']) && count($o['fonts'])) { + $res .= "\n/Font << "; + foreach ($o['fonts'] as $finfo) { + $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + if (isset($o['xObjects']) && count($o['xObjects'])) { + $res .= "\n/XObject << "; + foreach ($o['xObjects'] as $finfo) { + $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + $res .= "\n>>\n"; + + $tmp = $o["c"]; + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $res .= " /Filter /FlateDecode\n"; + $tmp = gzcompress($tmp, 6); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "/Length " . mb_strlen($tmp, '8bit') . " >>\n"; + $res .= "stream\n" . $tmp . "\nendstream" . "\nendobj";; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_acroform($id, $action, $options = '') + { + switch ($action) { + case "new": + $this->o_catalog($this->catalogId, 'acroform', $id); + $this->objects[$id] = array('t' => 'acroform', 'info' => $options); + break; + + case 'addfield': + $this->objects[$id]['info']['Fields'][] = $options; + break; + + case 'font': + $this->objects[$id]['fonts'][$options['fontNum']] = [ + 'objNum' => $options['objNum'], + 'fontNum' => $options['fontNum'] + ]; + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<<"; + + foreach ($o["info"] as $k => $v) { + switch($k) { + case 'Fields': + $res .= " /Fields ["; + foreach ($v as $i) { + $res .= "$i 0 R "; + } + $res .= "]\n"; + break; + default: + $res .= "/$k $v\n"; + } + } + + $res .= "/DR <<\n"; + if (isset($o['fonts']) && count($o['fonts'])) { + $res .= "/Font << \n"; + foreach ($o['fonts'] as $finfo) { + $res .= "/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R\n"; + } + $res .= ">>\n"; + } + $res .= ">>\n"; + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param mixed $options + * @return null|string + */ + protected function o_field($id, $action, $options = '') + { + switch ($action) { + case "new": + $this->o_page($options['pageid'], 'annot', $id); + $this->o_acroform($this->acroFormId, 'addfield', $id); + $this->objects[$id] = ['t' => 'field', 'info' => $options]; + break; + + case 'set': + $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options); + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /Annot /Subtype /Widget \n"; + + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + foreach ($o["info"] as $k => $v) { + switch ($k) { + case 'pageid': + $res .= "/P $v 0 R\n"; + break; + case 'value': + if ($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/V ($v)\n"; + break; + case 'refvalue': + $res .= "/V $v 0 R\n"; + break; + case 'da': + if ($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/DA ($v)\n"; + break; + case 'options': + $res .= "/Opt [\n"; + foreach ($v as $opt) { + if ($encrypted) { + $opt = $this->filterText($this->ARC4($opt), false, false); + } + $res .= "($opt)\n"; + } + $res .= "]\n"; + break; + case 'rect': + $res .= "/Rect ["; + foreach ($v as $value) { + $res .= sprintf("%.4F ", $value); + } + $res .= "]\n"; + break; + case 'appearance': + $res .= "/AP << "; + foreach ($v as $a => $ref) { + $res .= "/$a $ref 0 R "; + } + $res .= ">>\n"; + break; + case 'T': + if($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/T ($v)\n"; + break; + default: + $res .= "/$k $v\n"; + } + + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_sig($id, $action, $options = '') + { + $sign_maxlen = $this->signatureMaxLen; + + switch ($action) { + case "new": + $this->objects[$id] = array('t' => 'sig', 'info' => $options); + $this->byteRange[$id] = ['t' => 'sig']; + break; + + case 'byterange': + $o = &$this->objects[$id]; + $content =& $options['content']; + $content_len = strlen($content); + $pos = strpos($content, sprintf("/ByteRange [ %'.010d", $id)); + $len = strlen('/ByteRange [ ********** ********** ********** ********** ]'); + $rangeStartPos = $pos + $len + 1 + 10; // before '<' + $content = substr_replace($content, str_pad(sprintf('/ByteRange [ 0 %u %u %u ]', $rangeStartPos, $rangeStartPos + $sign_maxlen + 2, $content_len - 2 - $sign_maxlen - $rangeStartPos ), $len, ' ', STR_PAD_RIGHT), $pos, $len); + + $fuid = uniqid(); + $tmpInput = $this->tmp . "/pkcs7.tmp." . $fuid . '.in'; + $tmpOutput = $this->tmp . "/pkcs7.tmp." . $fuid . '.out'; + + if (file_put_contents($tmpInput, substr($content, 0, $rangeStartPos)) === false) { + throw new \Exception("Unable to write temporary file for signing."); + } + if (file_put_contents($tmpInput, substr($content, $rangeStartPos + 2 + $sign_maxlen), + FILE_APPEND) === false) { + throw new \Exception("Unable to write temporary file for signing."); + } + + if (openssl_pkcs7_sign($tmpInput, $tmpOutput, + $o['info']['SignCert'], + array($o['info']['PrivKey'], $o['info']['Password']), + array(), PKCS7_BINARY | PKCS7_DETACHED) === false) { + throw new \Exception("Failed to prepare signature."); + } + + $signature = file_get_contents($tmpOutput); + + unlink($tmpInput); + unlink($tmpOutput); + + $sign = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13)); + list($head, $signature) = explode("\n\n", $sign); + + $signature = base64_decode(trim($signature)); + + $signature = current(unpack('H*', $signature)); + $signature = str_pad($signature, $sign_maxlen, '0'); + $siglen = strlen($signature); + if (strlen($signature) > $sign_maxlen) { + throw new \Exception("Signature length ($siglen) exceeds the $sign_maxlen limit."); + } + + $content = substr_replace($content, $signature, $rangeStartPos + 1, $sign_maxlen); + break; + + case "out": + $res = "\n$id 0 obj\n<<\n"; + + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + $res .= "/ByteRange " .sprintf("[ %'.010d ********** ********** ********** ]\n", $id); + $res .= "/Contents <" . str_pad('', $sign_maxlen, '0') . ">\n"; + $res .= "/Filter/Adobe.PPKLite\n"; //PPKMS \n"; + $res .= "/Type/Sig/SubFilter/adbe.pkcs7.detached \n"; + + $date = "D:" . substr_replace(date('YmdHisO'), '\'', -2, 0) . '\''; + if ($encrypted) { + $date = $this->ARC4($date); + } + + $res .= "/M ($date)\n"; + $res .= "/Prop_Build << /App << /Name /DomPDF >> /Filter << /Name /Adobe.PPKLite >> >>\n"; + + $o = &$this->objects[$id]; + foreach ($o['info'] as $k => $v) { + switch($k) { + case 'Name': + case 'Location': + case 'Reason': + case 'ContactInfo': + if ($v !== null && $v !== '') { + $res .= "/$k (" . + ($encrypted ? $this->filterText($this->ARC4($v), false, false) : $v) . ") \n"; + } + break; + } + } + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * encryption object. + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_encryption($id, $action, $options = '') + { + switch ($action) { + case 'new': + // make the new object + $this->objects[$id] = ['t' => 'encryption', 'info' => $options]; + $this->arc4_objnum = $id; + break; + + case 'keys': + // figure out the additional parameters required + $pad = chr(0x28) . chr(0xBF) . chr(0x4E) . chr(0x5E) . chr(0x4E) . chr(0x75) . chr(0x8A) . chr(0x41) + . chr(0x64) . chr(0x00) . chr(0x4E) . chr(0x56) . chr(0xFF) . chr(0xFA) . chr(0x01) . chr(0x08) + . chr(0x2E) . chr(0x2E) . chr(0x00) . chr(0xB6) . chr(0xD0) . chr(0x68) . chr(0x3E) . chr(0x80) + . chr(0x2F) . chr(0x0C) . chr(0xA9) . chr(0xFE) . chr(0x64) . chr(0x53) . chr(0x69) . chr(0x7A); + + $info = $this->objects[$id]['info']; + + $len = mb_strlen($info['owner'], '8bit'); + + if ($len > 32) { + $owner = substr($info['owner'], 0, 32); + } else { + if ($len < 32) { + $owner = $info['owner'] . substr($pad, 0, 32 - $len); + } else { + $owner = $info['owner']; + } + } + + $len = mb_strlen($info['user'], '8bit'); + if ($len > 32) { + $user = substr($info['user'], 0, 32); + } else { + if ($len < 32) { + $user = $info['user'] . substr($pad, 0, 32 - $len); + } else { + $user = $info['user']; + } + } + + $tmp = $this->md5_16($owner); + $okey = substr($tmp, 0, 5); + $this->ARC4_init($okey); + $ovalue = $this->ARC4($user); + $this->objects[$id]['info']['O'] = $ovalue; + + // now make the u value, phew. + $tmp = $this->md5_16( + $user . $ovalue . chr($info['p']) . chr(255) . chr(255) . chr(255) . hex2bin($this->fileIdentifier) + ); + + $ukey = substr($tmp, 0, 5); + $this->ARC4_init($ukey); + $this->encryptionKey = $ukey; + $this->encrypted = true; + $uvalue = $this->ARC4($pad); + $this->objects[$id]['info']['U'] = $uvalue; + // initialize the arc4 array + break; + + case 'out': + $o = &$this->objects[$id]; + + $res = "\n$id 0 obj\n<<"; + $res .= "\n/Filter /Standard"; + $res .= "\n/V 1"; + $res .= "\n/R 2"; + $res .= "\n/O (" . $this->filterText($o['info']['O'], false, false) . ')'; + $res .= "\n/U (" . $this->filterText($o['info']['U'], false, false) . ')'; + // and the p-value needs to be converted to account for the twos-complement approach + $o['info']['p'] = (($o['info']['p'] ^ 255) + 1) * -1; + $res .= "\n/P " . ($o['info']['p']); + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + protected function o_indirect_references($id, $action, $options = null) + { + switch ($action) { + case 'new': + case 'add': + if ($id === 0) { + $id = ++$this->numObj; + $this->o_catalog($this->catalogId, 'names', $id); + $this->objects[$id] = ['t' => 'indirect_references', 'info' => $options]; + $this->indirectReferenceId = $id; + } else { + $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options); + } + break; + case 'out': + $res = "\n$id 0 obj << "; + + foreach($this->objects[$id]['info'] as $referenceObjName => $referenceObjId) { + $res .= "/$referenceObjName $referenceObjId 0 R "; + } + + $res .= ">> endobj"; + return $res; + } + + return null; + } + + protected function o_names($id, $action, $options = null) + { + switch ($action) { + case 'new': + case 'add': + if ($id === 0) { + $id = ++$this->numObj; + $this->objects[$id] = ['t' => 'names', 'info' => [$options]]; + $this->o_indirect_references($this->indirectReferenceId, 'add', ['EmbeddedFiles' => $id]); + $this->embeddedFilesId = $id; + } else { + $this->objects[$id]['info'][] = $options; + } + break; + case 'out': + $info = &$this->objects[$id]['info']; + $res = ''; + if (count($info) > 0) { + $res = "\n$id 0 obj << /Names [ "; + + if ($this->encrypted) { + $this->encryptInit($id); + } + + foreach ($info as $entry) { + if ($this->encrypted) { + $filename = $this->ARC4($entry['filename']); + } else { + $filename = $entry['filename']; + } + + $res .= "($filename) " . $entry['dict_reference'] . " 0 R "; + } + + $res .= "] >> endobj"; + } + return $res; + } + + return null; + } + + protected function o_embedded_file_dictionary($id, $action, $options = null) + { + switch ($action) { + case 'new': + $embeddedFileId = ++$this->numObj; + $options['embedded_reference'] = $embeddedFileId; + $this->objects[$id] = ['t' => 'embedded_file_dictionary', 'info' => $options]; + $this->o_embedded_file($embeddedFileId, 'new', $options); + $options['dict_reference'] = $id; + $this->o_names($this->embeddedFilesId, 'add', $options); + break; + case 'out': + $info = &$this->objects[$id]['info']; + + if ($this->encrypted) { + $this->encryptInit($id); + $filename = $this->ARC4($info['filename']); + $description = $this->ARC4($info['description']); + } else { + $filename = $info['filename']; + $description = $info['description']; + } + + $res = "\n$id 0 obj <</Type /Filespec /EF"; + $res .= " <</F " . $info['embedded_reference'] . " 0 R >>"; + $res .= " /F ($filename) /UF ($filename) /Desc ($description)"; + $res .= " >> endobj"; + return $res; + } + + return null; + } + + protected function o_embedded_file($id, $action, $options = null): ?string + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'embedded_file', 'info' => $options]; + break; + case 'out': + $info = &$this->objects[$id]['info']; + + if ($this->compressionReady) { + $filepath = $info['filepath']; + $checksum = md5_file($filepath); + $f = fopen($filepath, "rb"); + + $file_content_compressed = ''; + $deflateContext = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 6]); + while (($block = fread($f, 8192))) { + $file_content_compressed .= deflate_add($deflateContext, $block, ZLIB_NO_FLUSH); + } + $file_content_compressed .= deflate_add($deflateContext, '', ZLIB_FINISH); + $file_size_uncompressed = ftell($f); + fclose($f); + } else { + $file_content = file_get_contents($info['filepath']); + $file_size_uncompressed = mb_strlen($file_content, '8bit'); + $checksum = md5($file_content); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $checksum = $this->ARC4($checksum); + $file_content_compressed = $this->ARC4($file_content_compressed); + } + $file_size_compressed = mb_strlen($file_content_compressed, '8bit'); + + $res = "\n$id 0 obj <</Params <</Size $file_size_uncompressed /CheckSum ($checksum) >>" . + " /Type/EmbeddedFile /Filter/FlateDecode" . + " /Length $file_size_compressed >> stream\n$file_content_compressed\nendstream\nendobj"; + + return $res; + } + + return null; + } + + /** + * ARC4 functions + * A series of function to implement ARC4 encoding in PHP + */ + + /** + * calculate the 16 byte version of the 128 bit md5 digest of the string + * + * @param $string + * @return string + */ + function md5_16($string) + { + $tmp = md5($string); + $out = ''; + for ($i = 0; $i <= 30; $i = $i + 2) { + $out .= chr(hexdec(substr($tmp, $i, 2))); + } + + return $out; + } + + /** + * initialize the encryption for processing a particular object + * + * @param $id + */ + function encryptInit($id) + { + $tmp = $this->encryptionKey; + $hex = dechex($id); + if (mb_strlen($hex, '8bit') < 6) { + $hex = substr('000000', 0, 6 - mb_strlen($hex, '8bit')) . $hex; + } + $tmp .= chr(hexdec(substr($hex, 4, 2))) + . chr(hexdec(substr($hex, 2, 2))) + . chr(hexdec(substr($hex, 0, 2))) + . chr(0) + . chr(0) + ; + $key = $this->md5_16($tmp); + $this->ARC4_init(substr($key, 0, 10)); + } + + /** + * initialize the ARC4 encryption + * + * @param string $key + */ + function ARC4_init($key = '') + { + $this->arc4 = ''; + + // setup the control array + if (mb_strlen($key, '8bit') == 0) { + return; + } + + $k = ''; + while (mb_strlen($k, '8bit') < 256) { + $k .= $key; + } + + $k = substr($k, 0, 256); + for ($i = 0; $i < 256; $i++) { + $this->arc4 .= chr($i); + } + + $j = 0; + + for ($i = 0; $i < 256; $i++) { + $t = $this->arc4[$i]; + $j = ($j + ord($t) + ord($k[$i])) % 256; + $this->arc4[$i] = $this->arc4[$j]; + $this->arc4[$j] = $t; + } + } + + /** + * ARC4 encrypt a text string + * + * @param $text + * @return string + */ + function ARC4($text) + { + $len = mb_strlen($text, '8bit'); + $a = 0; + $b = 0; + $c = $this->arc4; + $out = ''; + for ($i = 0; $i < $len; $i++) { + $a = ($a + 1) % 256; + $t = $c[$a]; + $b = ($b + ord($t)) % 256; + $c[$a] = $c[$b]; + $c[$b] = $t; + $k = ord($c[(ord($c[$a]) + ord($c[$b])) % 256]); + $out .= chr(ord($text[$i]) ^ $k); + } + + return $out; + } + + /** + * functions which can be called to adjust or add to the document + */ + + /** + * add a link in the document to an external URL + * + * @param $url + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + */ + function addLink($url, $x0, $y0, $x1, $y1) + { + $this->numObj++; + $info = ['type' => 'link', 'url' => $url, 'rect' => [$x0, $y0, $x1, $y1]]; + $this->o_annotation($this->numObj, 'new', $info); + } + + /** + * add a link in the document to an internal destination (ie. within the document) + * + * @param $label + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + */ + function addInternalLink($label, $x0, $y0, $x1, $y1) + { + $this->numObj++; + $info = ['type' => 'ilink', 'label' => $label, 'rect' => [$x0, $y0, $x1, $y1]]; + $this->o_annotation($this->numObj, 'new', $info); + } + + /** + * set the encryption of the document + * can be used to turn it on and/or set the passwords which it will have. + * also the functions that the user will have are set here, such as print, modify, add + * + * @param string $userPass + * @param string $ownerPass + * @param array $pc + */ + function setEncryption($userPass = '', $ownerPass = '', $pc = []) + { + $p = bindec("11000000"); + + $options = ['print' => 4, 'modify' => 8, 'copy' => 16, 'add' => 32]; + + foreach ($pc as $k => $v) { + if ($v && isset($options[$k])) { + $p += $options[$k]; + } else { + if (isset($options[$v])) { + $p += $options[$v]; + } + } + } + + // implement encryption on the document + if ($this->arc4_objnum == 0) { + // then the block does not exist already, add it. + $this->numObj++; + if (mb_strlen($ownerPass) == 0) { + $ownerPass = $userPass; + } + + $this->o_encryption($this->numObj, 'new', ['user' => $userPass, 'owner' => $ownerPass, 'p' => $p]); + } + } + + /** + * should be used for internal checks, not implemented as yet + */ + function checkAllHere() + { + } + + /** + * return the pdf stream as a string returned from the function + * + * @param bool $debug + * @return string + */ + function output($debug = false) + { + if ($debug) { + // turn compression off + $this->options['compression'] = false; + } + + if ($this->javascript) { + $this->numObj++; + + $js_id = $this->numObj; + $this->o_embedjs($js_id, 'new'); + $this->o_javascript(++$this->numObj, 'new', $this->javascript); + + $id = $this->catalogId; + + $this->o_indirect_references($this->indirectReferenceId, 'add', ['JavaScript' => $js_id]); + } + + if ($this->fileIdentifier === '') { + $tmp = implode('', $this->objects[$this->infoObject]['info']); + $this->fileIdentifier = md5('DOMPDF' . __FILE__ . $tmp . microtime() . mt_rand()); + } + + if ($this->arc4_objnum) { + $this->o_encryption($this->arc4_objnum, 'keys'); + $this->ARC4_init($this->encryptionKey); + } + + $this->checkAllHere(); + + $xref = []; + $content = '%PDF-' . self::PDF_VERSION; + $pos = mb_strlen($content, '8bit'); + + // pre-process o_font objects before output of all objects + foreach ($this->objects as $k => $v) { + if ($v['t'] === 'font') { + $this->o_font($k, 'add'); + } + } + + foreach ($this->objects as $k => $v) { + $tmp = 'o_' . $v['t']; + $cont = $this->$tmp($k, 'out'); + $content .= $cont; + $xref[] = $pos + 1; //+1 to account for \n at the start of each object + $pos += mb_strlen($cont, '8bit'); + } + + $content .= "\nxref\n0 " . (count($xref) + 1) . "\n0000000000 65535 f \n"; + + foreach ($xref as $p) { + $content .= str_pad($p, 10, "0", STR_PAD_LEFT) . " 00000 n \n"; + } + + $content .= "trailer\n<<\n" . + '/Size ' . (count($xref) + 1) . "\n" . + '/Root 1 0 R' . "\n" . + '/Info ' . $this->infoObject . " 0 R\n" + ; + + // if encryption has been applied to this document then add the marker for this dictionary + if ($this->arc4_objnum > 0) { + $content .= '/Encrypt ' . $this->arc4_objnum . " 0 R\n"; + } + + $content .= '/ID[<' . $this->fileIdentifier . '><' . $this->fileIdentifier . ">]\n"; + + // account for \n added at start of xref table + $pos++; + + $content .= ">>\nstartxref\n$pos\n%%EOF\n"; + + if (count($this->byteRange) > 0) { + foreach ($this->byteRange as $k => $v) { + $tmp = 'o_' . $v['t']; + $this->$tmp($k, 'byterange', ['content' => &$content]); + } + } + + return $content; + } + + /** + * initialize a new document + * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum + * this function is called automatically by the constructor function + * + * @param array $pageSize + */ + private function newDocument($pageSize = [0, 0, 612, 792]) + { + $this->numObj = 0; + $this->objects = []; + + $this->numObj++; + $this->o_catalog($this->numObj, 'new'); + + $this->numObj++; + $this->o_outlines($this->numObj, 'new'); + + $this->numObj++; + $this->o_pages($this->numObj, 'new'); + + $this->o_pages($this->numObj, 'mediaBox', $pageSize); + $this->currentNode = 3; + + $this->numObj++; + $this->o_procset($this->numObj, 'new'); + + $this->numObj++; + $this->o_info($this->numObj, 'new'); + + $this->numObj++; + $this->o_page($this->numObj, 'new'); + + // need to store the first page id as there is no way to get it to the user during + // startup + $this->firstPageId = $this->currentContents; + } + + /** + * open the font file and return a php structure containing it. + * first check if this one has been done before and saved in a form more suited to php + * note that if a php serialized version does not exist it will try and make one, but will + * require write access to the directory to do it... it is MUCH faster to have these serialized + * files. + * + * @param $font + */ + private function openFont($font) + { + // assume that $font contains the path and file but not the extension + $name = basename($font); + $dir = dirname($font) . '/'; + + $fontcache = $this->fontcache; + if ($fontcache == '') { + $fontcache = rtrim($dir, DIRECTORY_SEPARATOR."/\\"); + } + + //$name filename without folder and extension of font metrics + //$dir folder of font metrics + //$fontcache folder of runtime created php serialized version of font metrics. + // If this is not given, the same folder as the font metrics will be used. + // Storing and reusing serialized versions improves speed much + + $this->addMessage("openFont: $font - $name"); + + if (!$this->isUnicode || in_array(mb_strtolower(basename($name)), self::$coreFonts)) { + $metrics_name = "$name.afm"; + } else { + $metrics_name = "$name.ufm"; + } + + $cache_name = "$metrics_name.php"; + $this->addMessage("metrics: $metrics_name, cache: $cache_name"); + + if (file_exists($fontcache . '/' . $cache_name)) { + $this->addMessage("openFont: php file exists $fontcache/$cache_name"); + $this->fonts[$font] = require($fontcache . '/' . $cache_name); + + if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_'] != $this->fontcacheVersion) { + // if the font file is old, then clear it out and prepare for re-creation + $this->addMessage('openFont: clear out, make way for new version.'); + $this->fonts[$font] = null; + unset($this->fonts[$font]); + } + } else { + $old_cache_name = "php_$metrics_name"; + if (file_exists($fontcache . '/' . $old_cache_name)) { + $this->addMessage( + "openFont: php file doesn't exist $fontcache/$cache_name, creating it from the old format" + ); + $old_cache = file_get_contents($fontcache . '/' . $old_cache_name); + file_put_contents($fontcache . '/' . $cache_name, '<?php return ' . $old_cache . ';'); + + $this->openFont($font); + return; + } + } + + if (!isset($this->fonts[$font]) && file_exists($dir . $metrics_name)) { + // then rebuild the php_<font>.afm file from the <font>.afm file + $this->addMessage("openFont: build php file from $dir$metrics_name"); + $data = []; + + // 20 => 'space' + $data['codeToName'] = []; + + // Since we're not going to enable Unicode for the core fonts we need to use a font-based + // setting for Unicode support rather than a global setting. + $data['isUnicode'] = (strtolower(substr($metrics_name, -3)) !== 'afm'); + + $cidtogid = ''; + if ($data['isUnicode']) { + $cidtogid = str_pad('', 256 * 256 * 2, "\x00"); + } + + $file = file($dir . $metrics_name); + + foreach ($file as $rowA) { + $row = trim($rowA); + $pos = strpos($row, ' '); + + if ($pos) { + // then there must be some keyword + $key = substr($row, 0, $pos); + switch ($key) { + case 'FontName': + case 'FullName': + case 'FamilyName': + case 'PostScriptName': + case 'Weight': + case 'ItalicAngle': + case 'IsFixedPitch': + case 'CharacterSet': + case 'UnderlinePosition': + case 'UnderlineThickness': + case 'Version': + case 'EncodingScheme': + case 'CapHeight': + case 'XHeight': + case 'Ascender': + case 'Descender': + case 'StdHW': + case 'StdVW': + case 'StartCharMetrics': + case 'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font. Otherwise it's too big. + $data[$key] = trim(substr($row, $pos)); + break; + + case 'FontBBox': + $data[$key] = explode(' ', trim(substr($row, $pos))); + break; + + //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; + case 'C': // Found in AFM files + $bits = explode(';', trim($row)); + $dtmp = ['C' => null, 'N' => null, 'WX' => null, 'B' => []]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') == 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['C']; + $n = $dtmp['N']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + if (!ctype_xdigit($n) || $c != hexdec($n)) { + $data['codeToName'][$c] = $n; + } + $data['C'][$c] = $width; + } elseif (isset($n)) { + $data['C'][$n] = $width; + } + + if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') { + $data['MissingWidth'] = $width; + } + + break; + + // U 827 ; WX 0 ; N squaresubnosp ; G 675 ; + case 'U': // Found in UFM files + if (!$data['isUnicode']) { + break; + } + + $bits = explode(';', trim($row)); + $dtmp = ['G' => null, 'N' => null, 'U' => null, 'WX' => null]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') === 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['U']; + $n = $dtmp['N']; + $glyph = $dtmp['G']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + // Set values in CID to GID map + if ($c >= 0 && $c < 0xFFFF && $glyph) { + $cidtogid[$c * 2] = chr($glyph >> 8); + $cidtogid[$c * 2 + 1] = chr($glyph & 0xFF); + } + + if (!ctype_xdigit($n) || $c != hexdec($n)) { + $data['codeToName'][$c] = $n; + } + $data['C'][$c] = $width; + } elseif (isset($n)) { + $data['C'][$n] = $width; + } + + if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') { + $data['MissingWidth'] = $width; + } + + break; + + case 'KPX': + break; // don't include them as they are not used yet + //KPX Adieresis yacute -40 + /*$bits = explode(' ', trim($row)); + $data['KPX'][$bits[1]][$bits[2]] = $bits[3]; + break;*/ + } + } + } + + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on CIDtoGID string + $data['CIDtoGID_Compressed'] = true; + $cidtogid = gzcompress($cidtogid, 6); + } + $data['CIDtoGID'] = base64_encode($cidtogid); + $data['_version_'] = $this->fontcacheVersion; + $this->fonts[$font] = $data; + + //Because of potential trouble with php safe mode, expect that the folder already exists. + //If not existing, this will hit performance because of missing cached results. + if (is_dir($fontcache) && is_writable($fontcache)) { + file_put_contents($fontcache . '/' . $cache_name, '<?php return ' . var_export($data, true) . ';'); + } + $data = null; + } + + if (!isset($this->fonts[$font])) { + $this->addMessage("openFont: no font file found for $font. Do you need to run load_font.php?"); + } + + //pre_r($this->messages); + } + + /** + * if the font is not loaded then load it and make the required object + * else just make it the current font + * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding' + * note that encoding='none' will need to be used for symbolic fonts + * and 'differences' => an array of mappings between numbers 0->255 and character names. + * + * @param $fontName + * @param string $encoding + * @param bool $set + * @param bool $isSubsetting + * @return int + * @throws FontNotFoundException + */ + function selectFont($fontName, $encoding = '', $set = true, $isSubsetting = true) + { + if ($fontName === null || $fontName === '') { + return $this->currentFontNum; + } + + $ext = substr($fontName, -4); + if ($ext === '.afm' || $ext === '.ufm') { + $fontName = substr($fontName, 0, mb_strlen($fontName) - 4); + } + + if (!isset($this->fonts[$fontName])) { + $this->addMessage("selectFont: selecting - $fontName - $encoding, $set"); + + // load the file + $this->openFont($fontName); + + if (isset($this->fonts[$fontName])) { + $this->numObj++; + $this->numFonts++; + + $font = &$this->fonts[$fontName]; + + $name = basename($fontName); + $options = ['name' => $name, 'fontFileName' => $fontName, 'isSubsetting' => $isSubsetting]; + + if (is_array($encoding)) { + // then encoding and differences might be set + if (isset($encoding['encoding'])) { + $options['encoding'] = $encoding['encoding']; + } + + if (isset($encoding['differences'])) { + $options['differences'] = $encoding['differences']; + } + } else { + if (mb_strlen($encoding, '8bit')) { + // then perhaps only the encoding has been set + $options['encoding'] = $encoding; + } + } + + $this->o_font($this->numObj, 'new', $options); + + if (file_exists("$fontName.ttf")) { + $fileSuffix = 'ttf'; + } elseif (file_exists("$fontName.TTF")) { + $fileSuffix = 'TTF'; + } elseif (file_exists("$fontName.pfb")) { + $fileSuffix = 'pfb'; + } elseif (file_exists("$fontName.PFB")) { + $fileSuffix = 'PFB'; + } else { + $fileSuffix = ''; + } + + $font['fileSuffix'] = $fileSuffix; + + $font['fontNum'] = $this->numFonts; + $font['isSubsetting'] = $isSubsetting && $font['isUnicode'] && strtolower($fileSuffix) === 'ttf'; + + // also set the differences here, note that this means that these will take effect only the + //first time that a font is selected, else they are ignored + if (isset($options['differences'])) { + $font['differences'] = $options['differences']; + } + } + } + + if ($set && isset($this->fonts[$fontName])) { + // so if for some reason the font was not set in the last one then it will not be selected + $this->currentBaseFont = $fontName; + + // the next lines mean that if a new font is selected, then the current text state will be + // applied to it as well. + $this->currentFont = $this->currentBaseFont; + $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; + } + + return $this->currentFontNum; + } + + /** + * sets up the current font, based on the font families, and the current text state + * note that this system is quite flexible, a bold-italic font can be completely different to a + * italic-bold font, and even bold-bold will have to be defined within the family to have meaning + * This function is to be called whenever the currentTextState is changed, it will update + * the currentFont setting to whatever the appropriate family one is. + * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont + * This function will change the currentFont to whatever it should be, but will not change the + * currentBaseFont. + */ + private function setCurrentFont() + { + // if (strlen($this->currentBaseFont) == 0){ + // // then assume an initial font + // $this->selectFont($this->defaultFont); + // } + // $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1); + // if (strlen($this->currentTextState) + // && isset($this->fontFamilies[$cf]) + // && isset($this->fontFamilies[$cf][$this->currentTextState])){ + // // then we are in some state or another + // // and this font has a family, and the current setting exists within it + // // select the font, then return it + // $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState]; + // $this->selectFont($nf,'',0); + // $this->currentFont = $nf; + // $this->currentFontNum = $this->fonts[$nf]['fontNum']; + // } else { + // // the this font must not have the right family member for the current state + // // simply assume the base font + $this->currentFont = $this->currentBaseFont; + $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; + // } + } + + /** + * function for the user to find out what the ID is of the first page that was created during + * startup - useful if they wish to add something to it later. + * + * @return int + */ + function getFirstPageId() + { + return $this->firstPageId; + } + + /** + * add content to the currently active object + * + * @param $content + */ + private function addContent($content) + { + $this->objects[$this->currentContents]['c'] .= $content; + } + + /** + * sets the color for fill operations + * + * @param $color + * @param bool $force + */ + function setColor($color, $force = false) + { + $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null]; + + if (!$force && $this->currentColor == $new_color) { + return; + } + + if (isset($new_color[3])) { + $this->currentColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F k", $this->currentColor)); + } else { + if (isset($new_color[2])) { + $this->currentColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F rg", $this->currentColor)); + } + } + } + + /** + * sets the color for fill operations + * + * @param $fillRule + */ + function setFillRule($fillRule) + { + if (!in_array($fillRule, ["nonzero", "evenodd"])) { + return; + } + + $this->fillRule = $fillRule; + } + + /** + * sets the color for stroke operations + * + * @param $color + * @param bool $force + */ + function setStrokeColor($color, $force = false) + { + $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null]; + + if (!$force && $this->currentStrokeColor == $new_color) { + return; + } + + if (isset($new_color[3])) { + $this->currentStrokeColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F K", $this->currentStrokeColor)); + } else { + if (isset($new_color[2])) { + $this->currentStrokeColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F RG", $this->currentStrokeColor)); + } + } + } + + /** + * Set the graphics state for compositions + * + * @param $parameters + */ + function setGraphicsState($parameters) + { + // Create a new graphics state object if necessary + if (($gstate = array_search($parameters, $this->gstates)) === false) { + $this->numObj++; + $this->o_extGState($this->numObj, 'new', $parameters); + $gstate = $this->numStates; + $this->gstates[$gstate] = $parameters; + } + $this->addContent("\n/GS$gstate gs"); + } + + /** + * Set current blend mode & opacity for lines. + * + * Valid blend modes are: + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blend mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + function setLineTransparency($mode, $opacity) + { + static $blend_modes = [ + "Normal", + "Multiply", + "Screen", + "Overlay", + "Darken", + "Lighten", + "ColorDogde", + "ColorBurn", + "HardLight", + "SoftLight", + "Difference", + "Exclusion" + ]; + + if (!in_array($mode, $blend_modes)) { + $mode = "Normal"; + } + + if (is_null($this->currentLineTransparency)) { + $this->currentLineTransparency = []; + } + + if ($mode === (key_exists('mode', $this->currentLineTransparency) ? + $this->currentLineTransparency['mode'] : '') && + $opacity === (key_exists('opacity', $this->currentLineTransparency) ? + $this->currentLineTransparency["opacity"] : '')) { + return; + } + + $this->currentLineTransparency["mode"] = $mode; + $this->currentLineTransparency["opacity"] = $opacity; + + $options = [ + "BM" => "/$mode", + "CA" => (float)$opacity + ]; + + $this->setGraphicsState($options); + } + + /** + * Set current blend mode & opacity for filled objects. + * + * Valid blend modes are: + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blend mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + function setFillTransparency($mode, $opacity) + { + static $blend_modes = [ + "Normal", + "Multiply", + "Screen", + "Overlay", + "Darken", + "Lighten", + "ColorDogde", + "ColorBurn", + "HardLight", + "SoftLight", + "Difference", + "Exclusion" + ]; + + if (!in_array($mode, $blend_modes)) { + $mode = "Normal"; + } + + if (is_null($this->currentFillTransparency)) { + $this->currentFillTransparency = []; + } + + if ($mode === (key_exists('mode', $this->currentFillTransparency) ? + $this->currentFillTransparency['mode'] : '') && + $opacity === (key_exists('opacity', $this->currentFillTransparency) ? + $this->currentFillTransparency["opacity"] : '')) { + return; + } + + $this->currentFillTransparency["mode"] = $mode; + $this->currentFillTransparency["opacity"] = $opacity; + + $options = [ + "BM" => "/$mode", + "ca" => (float)$opacity, + ]; + + $this->setGraphicsState($options); + } + + /** + * draw a line from one set of coordinates to another + * + * @param $x1 + * @param $y1 + * @param $x2 + * @param $y2 + * @param bool $stroke + */ + function line($x1, $y1, $x2, $y2, $stroke = true) + { + $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F l", $x1, $y1, $x2, $y2)); + + if ($stroke) { + $this->addContent(' S'); + } + } + + /** + * draw a bezier curve based on 4 control points + * + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + * @param $x2 + * @param $y2 + * @param $x3 + * @param $y3 + */ + function curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + { + // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points + // as the control points for the curve. + $this->addContent( + sprintf("\n%.3F %.3F m %.3F %.3F %.3F %.3F %.3F %.3F c S", $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + ); + } + + /** + * draw a part of an ellipse + * + * @param $x0 + * @param $y0 + * @param $astart + * @param $afinish + * @param $r1 + * @param int $r2 + * @param int $angle + * @param int $nSeg + */ + function partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 = 0, $angle = 0, $nSeg = 8) + { + $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, false); + } + + /** + * draw a filled ellipse + * + * @param $x0 + * @param $y0 + * @param $r1 + * @param int $r2 + * @param int $angle + * @param int $nSeg + * @param int $astart + * @param int $afinish + */ + function filledEllipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360) + { + $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, true, true); + } + + /** + * @param $x + * @param $y + */ + function lineTo($x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F l", $x, $y)); + } + + /** + * @param $x + * @param $y + */ + function moveTo($x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F m", $x, $y)); + } + + /** + * draw a bezier curve based on 4 control points + * + * @param $x1 + * @param $y1 + * @param $x2 + * @param $y2 + * @param $x3 + * @param $y3 + */ + function curveTo($x1, $y1, $x2, $y2, $x3, $y3) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F %.3F %.3F c", $x1, $y1, $x2, $y2, $x3, $y3)); + } + + /** + * draw a bezier curve based on 4 control points + */ + function quadTo($cpx, $cpy, $x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F v", $cpx, $cpy, $x, $y)); + } + + function closePath() + { + $this->addContent(' h'); + } + + function endPath() + { + $this->addContent(' n'); + } + + /** + * draw an ellipse + * note that the part and filled ellipse are just special cases of this function + * + * draws an ellipse in the current line style + * centered at $x0,$y0, radii $r1,$r2 + * if $r2 is not set, then a circle is drawn + * from $astart to $afinish, measured in degrees, running anti-clockwise from the right hand side of the ellipse. + * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a + * pretty crappy shape at 2, as we are approximating with bezier curves. + * + * @param $x0 + * @param $y0 + * @param $r1 + * @param int $r2 + * @param int $angle + * @param int $nSeg + * @param int $astart + * @param int $afinish + * @param bool $close + * @param bool $fill + * @param bool $stroke + * @param bool $incomplete + */ + function ellipse( + $x0, + $y0, + $r1, + $r2 = 0, + $angle = 0, + $nSeg = 8, + $astart = 0, + $afinish = 360, + $close = true, + $fill = false, + $stroke = true, + $incomplete = false + ) { + if ($r1 == 0) { + return; + } + + if ($r2 == 0) { + $r2 = $r1; + } + + if ($nSeg < 2) { + $nSeg = 2; + } + + $astart = deg2rad((float)$astart); + $afinish = deg2rad((float)$afinish); + $totalAngle = $afinish - $astart; + + $dt = $totalAngle / $nSeg; + $dtm = $dt / 3; + + if ($angle != 0) { + $a = -1 * deg2rad((float)$angle); + + $this->addContent( + sprintf("\n q %.3F %.3F %.3F %.3F %.3F %.3F cm", cos($a), -sin($a), sin($a), cos($a), $x0, $y0) + ); + + $x0 = 0; + $y0 = 0; + } + + $t1 = $astart; + $a0 = $x0 + $r1 * cos($t1); + $b0 = $y0 + $r2 * sin($t1); + $c0 = -$r1 * sin($t1); + $d0 = $r2 * cos($t1); + + if (!$incomplete) { + $this->addContent(sprintf("\n%.3F %.3F m ", $a0, $b0)); + } + + for ($i = 1; $i <= $nSeg; $i++) { + // draw this bit of the total curve + $t1 = $i * $dt + $astart; + $a1 = $x0 + $r1 * cos($t1); + $b1 = $y0 + $r2 * sin($t1); + $c1 = -$r1 * sin($t1); + $d1 = $r2 * cos($t1); + + $this->addContent( + sprintf( + "\n%.3F %.3F %.3F %.3F %.3F %.3F c", + ($a0 + $c0 * $dtm), + ($b0 + $d0 * $dtm), + ($a1 - $c1 * $dtm), + ($b1 - $d1 * $dtm), + $a1, + $b1 + ) + ); + + $a0 = $a1; + $b0 = $b1; + $c0 = $c1; + $d0 = $d1; + } + + if (!$incomplete) { + if ($fill) { + $this->addContent(' f'); + } + + if ($stroke) { + if ($close) { + $this->addContent(' s'); // small 's' signifies closing the path as well + } else { + $this->addContent(' S'); + } + } + } + + if ($angle != 0) { + $this->addContent(' Q'); + } + } + + /** + * this sets the line drawing style. + * width, is the thickness of the line in user units + * cap is the type of cap to put on the line, values can be 'butt','round','square' + * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the + * end of the line. + * join can be 'miter', 'round', 'bevel' + * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the + * on and off dashes. + * (2) represents 2 on, 2 off, 2 on , 2 off ... + * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc + * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. + * + * @param int $width + * @param string $cap + * @param string $join + * @param string $dash + * @param int $phase + */ + function setLineStyle($width = 1, $cap = '', $join = '', $dash = '', $phase = 0) + { + // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day + $string = ''; + + if ($width > 0) { + $string .= "$width w"; + } + + $ca = ['butt' => 0, 'round' => 1, 'square' => 2]; + + if (isset($ca[$cap])) { + $string .= " $ca[$cap] J"; + } + + $ja = ['miter' => 0, 'round' => 1, 'bevel' => 2]; + + if (isset($ja[$join])) { + $string .= " $ja[$join] j"; + } + + if (is_array($dash)) { + $string .= ' [ ' . implode(' ', $dash) . " ] $phase d"; + } + + $this->currentLineStyle = $string; + $this->addContent("\n$string"); + } + + /** + * draw a polygon, the syntax for this is similar to the GD polygon command + * + * @param $p + * @param $np + * @param bool $f + */ + function polygon($p, $np, $f = false) + { + $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1])); + + for ($i = 2; $i < $np * 2; $i = $i + 2) { + $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1])); + } + + if ($f) { + $this->addContent(' f'); + } else { + $this->addContent(' S'); + } + } + + /** + * a filled rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param $x1 + * @param $y1 + * @param $width + * @param $height + */ + function filledRectangle($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re f", $x1, $y1, $width, $height)); + } + + /** + * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param $x1 + * @param $y1 + * @param $width + * @param $height + */ + function rectangle($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re S", $x1, $y1, $width, $height)); + } + + /** + * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param $x1 + * @param $y1 + * @param $width + * @param $height + */ + function rect($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re", $x1, $y1, $width, $height)); + } + + function stroke(bool $close = false) + { + $this->addContent("\n" . ($close ? "s" : "S")); + } + + function fill() + { + $this->addContent("\nf" . ($this->fillRule === "evenodd" ? "*" : "")); + } + + function fillStroke(bool $close = false) + { + $this->addContent("\n" . ($close ? "b" : "B") . ($this->fillRule === "evenodd" ? "*" : "")); + } + + /** + * @param string $subtype + * @param integer $x + * @param integer $y + * @param integer $w + * @param integer $h + * @return int + */ + function addXObject($subtype, $x, $y, $w, $h) + { + $id = ++$this->numObj; + $this->o_xobject($id, 'new', ['Subtype' => $subtype, 'bbox' => [$x, $y, $w, $h]]); + return $id; + } + + /** + * @param integer $numXObject + * @param string $type + * @param array $options + */ + function setXObjectResource($numXObject, $type, $options) + { + if (in_array($type, ['procset', 'font', 'xObject'])) { + $this->o_xobject($numXObject, $type, $options); + } + } + + /** + * add signature + * + * $fieldSigId = $cpdf->addFormField(Cpdf::ACROFORM_FIELD_SIG, 'Signature1', 0, 0, 0, 0, 0); + * + * $signatureId = $cpdf->addSignature([ + * 'signcert' => file_get_contents('dompdf.crt'), + * 'privkey' => file_get_contents('dompdf.key'), + * 'password' => 'password', + * 'name' => 'DomPDF DEMO', + * 'location' => 'Home', + * 'reason' => 'First Form', + * 'contactinfo' => 'info' + * ]); + * $cpdf->setFormFieldValue($fieldSigId, "$signatureId 0 R"); + * + * @param string $signcert + * @param string $privkey + * @param string $password + * @param string|null $name + * @param string|null $location + * @param string|null $reason + * @param string|null $contactinfo + * @return int + */ + function addSignature($signcert, $privkey, $password = '', $name = null, $location = null, $reason = null, $contactinfo = null) { + $sigId = ++$this->numObj; + $this->o_sig($sigId, 'new', [ + 'SignCert' => $signcert, + 'PrivKey' => $privkey, + 'Password' => $password, + 'Name' => $name, + 'Location' => $location, + 'Reason' => $reason, + 'ContactInfo' => $contactinfo + ]); + + return $sigId; + } + + /** + * add field to form + * + * @param string $type ACROFORM_FIELD_* + * @param string $name + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + * @param integer $ff Field Flag ACROFORM_FIELD_*_* + * @param float $size + * @param array $color + * @return int + */ + public function addFormField($type, $name, $x0, $y0, $x1, $y1, $ff = 0, $size = 10.0, $color = [0, 0, 0]) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $color = implode(' ', $color) . ' rg'; + + $currentFontNum = $this->currentFontNum; + $font = array_filter($this->objects[$this->currentNode]['info']['fonts'], + function($item) use ($currentFontNum) { return $item['fontNum'] == $currentFontNum; }); + + $this->o_acroform($this->acroFormId, 'font', + ['objNum' => $font[0]['objNum'], 'fontNum' => $font[0]['fontNum']]); + + $fieldId = ++$this->numObj; + $this->o_field($fieldId, 'new', [ + 'rect' => [$x0, $y0, $x1, $y1], + 'F' => 4, + 'FT' => "/$type", + 'T' => $name, + 'Ff' => $ff, + 'pageid' => $this->currentPage, + 'da' => "$color /F$this->currentFontNum " . sprintf('%.1F Tf ', $size) + ]); + + return $fieldId; + } + + /** + * set Field value + * + * @param integer $numFieldObj + * @param string $value + */ + public function setFormFieldValue($numFieldObj, $value) + { + $this->o_field($numFieldObj, 'set', ['value' => $value]); + } + + /** + * set Field value (reference) + * + * @param integer $numFieldObj + * @param integer $numObj Object number + */ + public function setFormFieldRefValue($numFieldObj, $numObj) + { + $this->o_field($numFieldObj, 'set', ['refvalue' => $numObj]); + } + + /** + * set Field Appearanc (reference) + * + * @param integer $numFieldObj + * @param integer $normalNumObj + * @param integer|null $rolloverNumObj + * @param integer|null $downNumObj + */ + public function setFormFieldAppearance($numFieldObj, $normalNumObj, $rolloverNumObj = null, $downNumObj = null) + { + $appearance['N'] = $normalNumObj; + + if ($rolloverNumObj !== null) { + $appearance['R'] = $rolloverNumObj; + } + + if ($downNumObj !== null) { + $appearance['D'] = $downNumObj; + } + + $this->o_field($numFieldObj, 'set', ['appearance' => $appearance]); + } + + /** + * set Choice Field option values + * + * @param integer $numFieldObj + * @param array $value + */ + public function setFormFieldOpt($numFieldObj, $value) + { + $this->o_field($numFieldObj, 'set', ['options' => $value]); + } + + /** + * add form to document + * + * @param integer $sigFlags + * @param boolean $needAppearances + */ + public function addForm($sigFlags = 0, $needAppearances = false) + { + $this->acroFormId = ++$this->numObj; + $this->o_acroform($this->acroFormId, 'new', [ + 'NeedAppearances' => $needAppearances ? 'true' : 'false', + 'SigFlags' => $sigFlags + ]); + } + + /** + * save the current graphic state + */ + function save() + { + // we must reset the color cache or it will keep bad colors after clipping + $this->currentColor = null; + $this->currentStrokeColor = null; + $this->addContent("\nq"); + } + + /** + * restore the last graphic state + */ + function restore() + { + // we must reset the color cache or it will keep bad colors after clipping + $this->currentColor = null; + $this->currentStrokeColor = null; + $this->addContent("\nQ"); + } + + /** + * draw a clipping rectangle, all the elements added after this will be clipped + * + * @param $x1 + * @param $y1 + * @param $width + * @param $height + */ + function clippingRectangle($x1, $y1, $width, $height) + { + $this->save(); + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re W n", $x1, $y1, $width, $height)); + } + + /** + * draw a clipping rounded rectangle, all the elements added after this will be clipped + * + * @param $x1 + * @param $y1 + * @param $w + * @param $h + * @param $rTL + * @param $rTR + * @param $rBR + * @param $rBL + */ + function clippingRectangleRounded($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + $this->save(); + + // start: top edge, left end + $this->addContent(sprintf("\n%.3F %.3F m ", $x1, $y1 - $rTL + $h)); + + // line: bottom edge, left end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1, $y1 + $rBL)); + + // curve: bottom-left corner + $this->ellipse($x1 + $rBL, $y1 + $rBL, $rBL, 0, 0, 8, 180, 270, false, false, false, true); + + // line: right edge, bottom end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w - $rBR, $y1)); + + // curve: bottom-right corner + $this->ellipse($x1 + $w - $rBR, $y1 + $rBR, $rBR, 0, 0, 8, 270, 360, false, false, false, true); + + // line: right edge, top end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w, $y1 + $h - $rTR)); + + // curve: bottom-right corner + $this->ellipse($x1 + $w - $rTR, $y1 + $h - $rTR, $rTR, 0, 0, 8, 0, 90, false, false, false, true); + + // line: bottom edge, right end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rTL, $y1 + $h)); + + // curve: top-right corner + $this->ellipse($x1 + $rTL, $y1 + $h - $rTL, $rTL, 0, 0, 8, 90, 180, false, false, false, true); + + // line: top edge, left end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rBL, $y1)); + + // Close & clip + $this->addContent(" W n"); + } + + /** + * ends the last clipping shape + */ + function clippingEnd() + { + $this->restore(); + } + + /** + * scale + * + * @param float $s_x scaling factor for width as percent + * @param float $s_y scaling factor for height as percent + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function scale($s_x, $s_y, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $tm = [ + $s_x, + 0, + 0, + $s_y, + $x * (1 - $s_x), + $y * (1 - $s_y) + ]; + + $this->transform($tm); + } + + /** + * translate + * + * @param float $t_x movement to the right + * @param float $t_y movement to the bottom + */ + function translate($t_x, $t_y) + { + $tm = [ + 1, + 0, + 0, + 1, + $t_x, + -$t_y + ]; + + $this->transform($tm); + } + + /** + * rotate + * + * @param float $angle angle in degrees for counter-clockwise rotation + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function rotate($angle, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $a = deg2rad($angle); + $cos_a = cos($a); + $sin_a = sin($a); + + $tm = [ + $cos_a, + -$sin_a, + $sin_a, + $cos_a, + $x - $sin_a * $y - $cos_a * $x, + $y - $cos_a * $y + $sin_a * $x, + ]; + + $this->transform($tm); + } + + /** + * skew + * + * @param float $angle_x + * @param float $angle_y + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function skew($angle_x, $angle_y, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $tan_x = tan(deg2rad($angle_x)); + $tan_y = tan(deg2rad($angle_y)); + + $tm = [ + 1, + -$tan_y, + -$tan_x, + 1, + $tan_x * $y, + $tan_y * $x, + ]; + + $this->transform($tm); + } + + /** + * apply graphic transformations + * + * @param array $tm transformation matrix + */ + function transform($tm) + { + $this->addContent(vsprintf("\n %.3F %.3F %.3F %.3F %.3F %.3F cm", $tm)); + } + + /** + * add a new page to the document + * this also makes the new page the current active object + * + * @param int $insert + * @param int $id + * @param string $pos + * @return int + */ + function newPage($insert = 0, $id = 0, $pos = 'after') + { + // if there is a state saved, then go up the stack closing them + // then on the new page, re-open them with the right setings + + if ($this->nStateStack) { + for ($i = $this->nStateStack; $i >= 1; $i--) { + $this->restoreState($i); + } + } + + $this->numObj++; + + if ($insert) { + // the id from the ezPdf class is the id of the contents of the page, not the page object itself + // query that object to find the parent + $rid = $this->objects[$id]['onPage']; + $opt = ['rid' => $rid, 'pos' => $pos]; + $this->o_page($this->numObj, 'new', $opt); + } else { + $this->o_page($this->numObj, 'new'); + } + + // if there is a stack saved, then put that onto the page + if ($this->nStateStack) { + for ($i = 1; $i <= $this->nStateStack; $i++) { + $this->saveState($i); + } + } + + // and if there has been a stroke or fill color set, then transfer them + if (isset($this->currentColor)) { + $this->setColor($this->currentColor, true); + } + + if (isset($this->currentStrokeColor)) { + $this->setStrokeColor($this->currentStrokeColor, true); + } + + // if there is a line style set, then put this in too + if (mb_strlen($this->currentLineStyle, '8bit')) { + $this->addContent("\n$this->currentLineStyle"); + } + + // the call to the o_page object set currentContents to the present page, so this can be returned as the page id + return $this->currentContents; + } + + /** + * Streams the PDF to the client. + * + * @param string $filename The filename to present to the client. + * @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1). + */ + function stream($filename = "document.pdf", $options = []) + { + if (headers_sent()) { + die("Unable to stream pdf: headers already sent"); + } + + if (!isset($options["compress"])) $options["compress"] = true; + if (!isset($options["Attachment"])) $options["Attachment"] = true; + + $debug = !$options['compress']; + $tmp = ltrim($this->output($debug)); + + header("Cache-Control: private"); + header("Content-Type: application/pdf"); + header("Content-Length: " . mb_strlen($tmp, "8bit")); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf"; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + + $encoding = mb_detect_encoding($filename); + $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); + $fallbackfilename = str_replace("\"", "", $fallbackfilename); + $encodedfilename = rawurlencode($filename); + + $contentDisposition = "Content-Disposition: $attachment; filename=\"$fallbackfilename\""; + if ($fallbackfilename !== $filename) { + $contentDisposition .= "; filename*=UTF-8''$encodedfilename"; + } + header($contentDisposition); + + echo $tmp; + flush(); + } + + /** + * return the height in units of the current font in the given size + * + * @param $size + * @return float|int + */ + function getFontHeight($size) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $font = $this->fonts[$this->currentFont]; + + // for the current font, and the given size, what is the height of the font in user units + if (isset($font['Ascender']) && isset($font['Descender'])) { + $h = $font['Ascender'] - $font['Descender']; + } else { + $h = $font['FontBBox'][3] - $font['FontBBox'][1]; + } + + // have to adjust by a font offset for Windows fonts. unfortunately it looks like + // the bounding box calculations are wrong and I don't know why. + if (isset($font['FontHeightOffset'])) { + // For CourierNew from Windows this needs to be -646 to match the + // Adobe native Courier font. + // + // For FreeMono from GNU this needs to be -337 to match the + // Courier font. + // + // Both have been added manually to the .afm and .ufm files. + $h += (int)$font['FontHeightOffset']; + } + + return $size * $h / 1000; + } + + /** + * @param $size + * @return float|int + */ + function getFontXHeight($size) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $font = $this->fonts[$this->currentFont]; + + // for the current font, and the given size, what is the height of the font in user units + if (isset($font['XHeight'])) { + $xh = $font['Ascender'] - $font['Descender']; + } else { + $xh = $this->getFontHeight($size) / 2; + } + + return $size * $xh / 1000; + } + + /** + * return the font descender, this will normally return a negative number + * if you add this number to the baseline, you get the level of the bottom of the font + * it is in the pdf user units + * + * @param $size + * @return float|int + */ + function getFontDescender($size) + { + // note that this will most likely return a negative value + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + //$h = $this->fonts[$this->currentFont]['FontBBox'][1]; + $h = $this->fonts[$this->currentFont]['Descender']; + + return $size * $h / 1000; + } + + /** + * filter the text, this is applied to all text just before being inserted into the pdf document + * it escapes the various things that need to be escaped, and so on + * + * @access private + * + * @param $text + * @param bool $bom + * @param bool $convert_encoding + * @return string + */ + function filterText($text, $bom = true, $convert_encoding = true) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + if ($convert_encoding) { + $cf = $this->currentFont; + if (isset($this->fonts[$cf]) && $this->fonts[$cf]['isUnicode']) { + $text = $this->utf8toUtf16BE($text, $bom); + } else { + //$text = html_entity_decode($text, ENT_QUOTES); + $text = mb_convert_encoding($text, self::$targetEncoding, 'UTF-8'); + } + } else if ($bom) { + $text = $this->utf8toUtf16BE($text, $bom); + } + + // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290) + return strtr($text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r']); + } + + /** + * return array containing codepoints (UTF-8 character values) for the + * string passed in. + * + * based on the excellent TCPDF code by Nicola Asuni and the + * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html + * + * @access private + * @author Orion Richardson + * @since January 5, 2008 + * + * @param string $text UTF-8 string to process + * + * @return array UTF-8 codepoints array for the string + */ + function utf8toCodePointsArray(&$text) + { + $length = mb_strlen($text, '8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040 + $unicode = []; // array containing unicode values + $bytes = []; // array containing single character byte sequences + $numbytes = 1; // number of octets needed to represent the UTF-8 character + + for ($i = 0; $i < $length; $i++) { + $c = ord($text[$i]); // get one string character at time + if (count($bytes) === 0) { // get starting octect + if ($c <= 0x7F) { + $unicode[] = $c; // use the character "as is" because is ASCII + $numbytes = 1; + } elseif (($c >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN) + $bytes[] = ($c - 0xC0) << 0x06; + $numbytes = 2; + } elseif (($c >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN) + $bytes[] = ($c - 0xE0) << 0x0C; + $numbytes = 3; + } elseif (($c >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN) + $bytes[] = ($c - 0xF0) << 0x12; + $numbytes = 4; + } else { + // use replacement character for other invalid sequences + $unicode[] = 0xFFFD; + $bytes = []; + $numbytes = 1; + } + } elseif (($c >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN + $bytes[] = $c - 0x80; + if (count($bytes) === $numbytes) { + // compose UTF-8 bytes to a single unicode value + $c = $bytes[0]; + for ($j = 1; $j < $numbytes; $j++) { + $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06)); + } + if ((($c >= 0xD800) and ($c <= 0xDFFF)) or ($c >= 0x10FFFF)) { + // The definition of UTF-8 prohibits encoding character numbers between + // U+D800 and U+DFFF, which are reserved for use with the UTF-16 + // encoding form (as surrogate pairs) and do not directly represent + // characters. + $unicode[] = 0xFFFD; // use replacement character + } else { + $unicode[] = $c; // add char to array + } + // reset data for next char + $bytes = []; + $numbytes = 1; + } + } else { + // use replacement character for other invalid sequences + $unicode[] = 0xFFFD; + $bytes = []; + $numbytes = 1; + } + } + + return $unicode; + } + + /** + * convert UTF-8 to UTF-16 with an additional byte order marker + * at the front if required. + * + * based on the excellent TCPDF code by Nicola Asuni and the + * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html + * + * @access private + * @author Orion Richardson + * @since January 5, 2008 + * + * @param string $text UTF-8 string to process + * @param boolean $bom whether to add the byte order marker + * + * @return string UTF-16 result string + */ + function utf8toUtf16BE(&$text, $bom = true) + { + $out = $bom ? "\xFE\xFF" : ''; + + $unicode = $this->utf8toCodePointsArray($text); + foreach ($unicode as $c) { + if ($c === 0xFFFD) { + $out .= "\xFF\xFD"; // replacement character + } elseif ($c < 0x10000) { + $out .= chr($c >> 0x08) . chr($c & 0xFF); + } else { + $c -= 0x10000; + $w1 = 0xD800 | ($c >> 0x10); + $w2 = 0xDC00 | ($c & 0x3FF); + $out .= chr($w1 >> 0x08) . chr($w1 & 0xFF) . chr($w2 >> 0x08) . chr($w2 & 0xFF); + } + } + + return $out; + } + + /** + * given a start position and information about how text is to be laid out, calculate where + * on the page the text will end + * + * @param $x + * @param $y + * @param $angle + * @param $size + * @param $wa + * @param $text + * @return array + */ + private function getTextPosition($x, $y, $angle, $size, $wa, $text) + { + // given this information return an array containing x and y for the end position as elements 0 and 1 + $w = $this->getTextWidth($size, $text); + + // need to adjust for the number of spaces in this text + $words = explode(' ', $text); + $nspaces = count($words) - 1; + $w += $wa * $nspaces; + $a = deg2rad((float)$angle); + + return [cos($a) * $w + $x, -sin($a) * $w + $y]; + } + + /** + * Callback method used by smallCaps + * + * @param array $matches + * + * @return string + */ + function toUpper($matches) + { + return mb_strtoupper($matches[0]); + } + + function concatMatches($matches) + { + $str = ""; + foreach ($matches as $match) { + $str .= $match[0]; + } + + return $str; + } + + /** + * register text for font subsetting + * + * @param $font + * @param $text + */ + function registerText($font, $text) + { + if (!$this->isUnicode || in_array(mb_strtolower(basename($font)), self::$coreFonts)) { + return; + } + + if (!isset($this->stringSubsets[$font])) { + $this->stringSubsets[$font] = []; + } + + $this->stringSubsets[$font] = array_unique( + array_merge($this->stringSubsets[$font], $this->utf8toCodePointsArray($text)) + ); + } + + /** + * add text to the document, at a specified location, size and angle on the page + * + * @param $x + * @param $y + * @param $size + * @param $text + * @param int $angle + * @param int $wordSpaceAdjust + * @param int $charSpaceAdjust + * @param bool $smallCaps + */ + function addText($x, $y, $size, $text, $angle = 0, $wordSpaceAdjust = 0, $charSpaceAdjust = 0, $smallCaps = false) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $text = str_replace(["\r", "\n"], "", $text); + + // if ($smallCaps) { + // preg_match_all("/(\P{Ll}+)/u", $text, $matches, PREG_SET_ORDER); + // $lower = $this->concatMatches($matches); + // d($lower); + + // preg_match_all("/(\p{Ll}+)/u", $text, $matches, PREG_SET_ORDER); + // $other = $this->concatMatches($matches); + // d($other); + + // $text = preg_replace_callback("/\p{Ll}/u", array($this, "toUpper"), $text); + // } + + // if there are any open callbacks, then they should be called, to show the start of the line + if ($this->nCallback > 0) { + for ($i = $this->nCallback; $i > 0; $i--) { + // call each function + $info = [ + 'x' => $x, + 'y' => $y, + 'angle' => $angle, + 'status' => 'sol', + 'p' => $this->callback[$i]['p'], + 'nCallback' => $this->callback[$i]['nCallback'], + 'height' => $this->callback[$i]['height'], + 'descender' => $this->callback[$i]['descender'] + ]; + + $func = $this->callback[$i]['f']; + $this->$func($info); + } + } + + if ($angle == 0) { + $this->addContent(sprintf("\nBT %.3F %.3F Td", $x, $y)); + } else { + $a = deg2rad((float)$angle); + $this->addContent( + sprintf("\nBT %.3F %.3F %.3F %.3F %.3F %.3F Tm", cos($a), -sin($a), sin($a), cos($a), $x, $y) + ); + } + + if ($wordSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tw", $wordSpaceAdjust)); + } + + if ($charSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tc", $charSpaceAdjust)); + } + + $len = mb_strlen($text); + $start = 0; + + if ($start < $len) { + $part = $text; // OAR - Don't need this anymore, given that $start always equals zero. substr($text, $start); + $place_text = $this->filterText($part, false); + // modify unicode text so that extra word spacing is manually implemented (bug #) + if ($this->fonts[$this->currentFont]['isUnicode'] && $wordSpaceAdjust != 0) { + $space_scale = 1000 / $size; + $place_text = str_replace("\x00\x20", "\x00\x20)\x00\x20" . (-round($space_scale * $wordSpaceAdjust)) . "\x00\x20(", $place_text); + } + $this->addContent(" /F$this->currentFontNum " . sprintf('%.1F Tf ', $size)); + $this->addContent(" [($place_text)] TJ"); + } + + if ($wordSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tw", 0)); + } + + if ($charSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tc", 0)); + } + + $this->addContent(' ET'); + + // if there are any open callbacks, then they should be called, to show the end of the line + if ($this->nCallback > 0) { + for ($i = $this->nCallback; $i > 0; $i--) { + // call each function + $tmp = $this->getTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text); + $info = [ + 'x' => $tmp[0], + 'y' => $tmp[1], + 'angle' => $angle, + 'status' => 'eol', + 'p' => $this->callback[$i]['p'], + 'nCallback' => $this->callback[$i]['nCallback'], + 'height' => $this->callback[$i]['height'], + 'descender' => $this->callback[$i]['descender'] + ]; + $func = $this->callback[$i]['f']; + $this->$func($info); + } + } + + if ($this->fonts[$this->currentFont]['isSubsetting']) { + $this->registerText($this->currentFont, $text); + } + } + + /** + * calculate how wide a given text string will be on a page, at a given size. + * this can be called externally, but is also used by the other class functions + * + * @param float $size + * @param string $text + * @param float $word_spacing + * @param float $char_spacing + * @return float + */ + function getTextWidth($size, $text, $word_spacing = 0, $char_spacing = 0) + { + static $ord_cache = []; + + // this function should not change any of the settings, though it will need to + // track any directives which change during calculation, so copy them at the start + // and put them back at the end. + $store_currentTextState = $this->currentTextState; + + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $text = str_replace(["\r", "\n"], "", $text); + + // converts a number or a float to a string so it can get the width + $text = "$text"; + + // hmm, this is where it all starts to get tricky - use the font information to + // calculate the width of each character, add them up and convert to user units + $w = 0; + $cf = $this->currentFont; + $current_font = $this->fonts[$cf]; + $space_scale = 1000 / ($size > 0 ? $size : 1); + + if ($current_font['isUnicode']) { + // for Unicode, use the code points array to calculate width rather + // than just the string itself + $unicode = $this->utf8toCodePointsArray($text); + + foreach ($unicode as $char) { + // check if we have to replace character + if (isset($current_font['differences'][$char])) { + $char = $current_font['differences'][$char]; + } + + if (isset($current_font['C'][$char])) { + $char_width = $current_font['C'][$char]; + + // add the character width + $w += $char_width; + + // add additional padding for space + if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space + $w += $word_spacing * $space_scale; + } + } + } + + // add additional char spacing + if ($char_spacing != 0) { + $w += $char_spacing * $space_scale * count($unicode); + } + + } else { + // If CPDF is in Unicode mode but the current font does not support Unicode we need to convert the character set to Windows-1252 + if ($this->isUnicode) { + $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8'); + } + + $len = mb_strlen($text, 'Windows-1252'); + + for ($i = 0; $i < $len; $i++) { + $c = $text[$i]; + $char = isset($ord_cache[$c]) ? $ord_cache[$c] : ($ord_cache[$c] = ord($c)); + + // check if we have to replace character + if (isset($current_font['differences'][$char])) { + $char = $current_font['differences'][$char]; + } + + if (isset($current_font['C'][$char])) { + $char_width = $current_font['C'][$char]; + + // add the character width + $w += $char_width; + + // add additional padding for space + if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space + $w += $word_spacing * $space_scale; + } + } + } + + // add additional char spacing + if ($char_spacing != 0) { + $w += $char_spacing * $space_scale * $len; + } + } + + $this->currentTextState = $store_currentTextState; + $this->setCurrentFont(); + + return $w * $size / 1000; + } + + /** + * this will be called at a new page to return the state to what it was on the + * end of the previous page, before the stack was closed down + * This is to get around not being able to have open 'q' across pages + * + * @param int $pageEnd + */ + function saveState($pageEnd = 0) + { + if ($pageEnd) { + // this will be called at a new page to return the state to what it was on the + // end of the previous page, before the stack was closed down + // This is to get around not being able to have open 'q' across pages + $opt = $this->stateStack[$pageEnd]; + // ok to use this as stack starts numbering at 1 + $this->setColor($opt['col'], true); + $this->setStrokeColor($opt['str'], true); + $this->addContent("\n" . $opt['lin']); + // $this->currentLineStyle = $opt['lin']; + } else { + $this->nStateStack++; + $this->stateStack[$this->nStateStack] = [ + 'col' => $this->currentColor, + 'str' => $this->currentStrokeColor, + 'lin' => $this->currentLineStyle + ]; + } + + $this->save(); + } + + /** + * restore a previously saved state + * + * @param int $pageEnd + */ + function restoreState($pageEnd = 0) + { + if (!$pageEnd) { + $n = $this->nStateStack; + $this->currentColor = $this->stateStack[$n]['col']; + $this->currentStrokeColor = $this->stateStack[$n]['str']; + $this->addContent("\n" . $this->stateStack[$n]['lin']); + $this->currentLineStyle = $this->stateStack[$n]['lin']; + $this->stateStack[$n] = null; + unset($this->stateStack[$n]); + $this->nStateStack--; + } + + $this->restore(); + } + + /** + * make a loose object, the output will go into this object, until it is closed, then will revert to + * the current one. + * this object will not appear until it is included within a page. + * the function will return the object number + * + * @return int + */ + function openObject() + { + $this->nStack++; + $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage]; + // add a new object of the content type, to hold the data flow + $this->numObj++; + $this->o_contents($this->numObj, 'new'); + $this->currentContents = $this->numObj; + $this->looseObjects[$this->numObj] = 1; + + return $this->numObj; + } + + /** + * open an existing object for editing + * + * @param $id + */ + function reopenObject($id) + { + $this->nStack++; + $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage]; + $this->currentContents = $id; + + // also if this object is the primary contents for a page, then set the current page to its parent + if (isset($this->objects[$id]['onPage'])) { + $this->currentPage = $this->objects[$id]['onPage']; + } + } + + /** + * close an object + */ + function closeObject() + { + // close the object, as long as there was one open in the first place, which will be indicated by + // an objectId on the stack. + if ($this->nStack > 0) { + $this->currentContents = $this->stack[$this->nStack]['c']; + $this->currentPage = $this->stack[$this->nStack]['p']; + $this->nStack--; + // easier to probably not worry about removing the old entries, they will be overwritten + // if there are new ones. + } + } + + /** + * stop an object from appearing on pages from this point on + * + * @param $id + */ + function stopObject($id) + { + // if an object has been appearing on pages up to now, then stop it, this page will + // be the last one that could contain it. + if (isset($this->addLooseObjects[$id])) { + $this->addLooseObjects[$id] = ''; + } + } + + /** + * after an object has been created, it wil only show if it has been added, using this function. + * + * @param $id + * @param string $options + */ + function addObject($id, $options = 'add') + { + // add the specified object to the page + if (isset($this->looseObjects[$id]) && $this->currentContents != $id) { + // then it is a valid object, and it is not being added to itself + switch ($options) { + case 'all': + // then this object is to be added to this page (done in the next block) and + // all future new pages. + $this->addLooseObjects[$id] = 'all'; + + case 'add': + if (isset($this->objects[$this->currentContents]['onPage'])) { + // then the destination contents is the primary for the page + // (though this object is actually added to that page) + $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id); + } + break; + + case 'even': + $this->addLooseObjects[$id] = 'even'; + $pageObjectId = $this->objects[$this->currentContents]['onPage']; + if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 0) { + $this->addObject($id); + // hacky huh :) + } + break; + + case 'odd': + $this->addLooseObjects[$id] = 'odd'; + $pageObjectId = $this->objects[$this->currentContents]['onPage']; + if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 1) { + $this->addObject($id); + // hacky huh :) + } + break; + + case 'next': + $this->addLooseObjects[$id] = 'all'; + break; + + case 'nexteven': + $this->addLooseObjects[$id] = 'even'; + break; + + case 'nextodd': + $this->addLooseObjects[$id] = 'odd'; + break; + } + } + } + + /** + * return a storable representation of a specific object + * + * @param $id + * @return string|null + */ + function serializeObject($id) + { + if (array_key_exists($id, $this->objects)) { + return serialize($this->objects[$id]); + } + + return null; + } + + /** + * restore an object from its stored representation. Returns its new object id. + * + * @param $obj + * @return int + */ + function restoreSerializedObject($obj) + { + $obj_id = $this->openObject(); + $this->objects[$obj_id] = unserialize($obj); + $this->closeObject(); + + return $obj_id; + } + + /** + * Embeds a file inside the PDF + * + * @param string $filepath path to the file to store inside the PDF + * @param string $embeddedFilename the filename displayed in the list of embedded files + * @param string $description a description in the list of embedded files + */ + public function addEmbeddedFile(string $filepath, string $embeddedFilename, string $description): void + { + $this->numObj++; + $this->o_embedded_file_dictionary( + $this->numObj, + 'new', + [ + 'filepath' => $filepath, + 'filename' => $embeddedFilename, + 'description' => $description + ] + ); + } + + /** + * add content to the documents info object + * + * @param $label + * @param int $value + */ + function addInfo($label, $value = 0) + { + // this will only work if the label is one of the valid ones. + // modify this so that arrays can be passed as well. + // if $label is an array then assume that it is key => value pairs + // else assume that they are both scalar, anything else will probably error + if (is_array($label)) { + foreach ($label as $l => $v) { + $this->o_info($this->infoObject, $l, $v); + } + } else { + $this->o_info($this->infoObject, $label, $value); + } + } + + /** + * set the viewer preferences of the document, it is up to the browser to obey these. + * + * @param $label + * @param int $value + */ + function setPreferences($label, $value = 0) + { + // this will only work if the label is one of the valid ones. + if (is_array($label)) { + foreach ($label as $l => $v) { + $this->o_catalog($this->catalogId, 'viewerPreferences', [$l => $v]); + } + } else { + $this->o_catalog($this->catalogId, 'viewerPreferences', [$label => $value]); + } + } + + /** + * extract an integer from a position in a byte stream + * + * @param $data + * @param $pos + * @param $num + * @return int + */ + private function getBytes(&$data, $pos, $num) + { + // return the integer represented by $num bytes from $pos within $data + $ret = 0; + for ($i = 0; $i < $num; $i++) { + $ret *= 256; + $ret += ord($data[$pos + $i]); + } + + return $ret; + } + + /** + * Check if image already added to pdf image directory. + * If yes, need not to create again (pass empty data) + * + * @param string $imgname + * @return bool + */ + function image_iscached($imgname) + { + return isset($this->imagelist[$imgname]); + } + + /** + * add a PNG image into the document, from a GD object + * this should work with remote files + * + * @param \GdImage|resource $img A GD resource + * @param string $file The PNG file + * @param float $x X position + * @param float $y Y position + * @param float $w Width + * @param float $h Height + * @param bool $is_mask true if the image is a mask + * @param bool $mask true if the image is masked + * @throws Exception + */ + function addImagePng(&$img, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null) + { + if (!function_exists("imagepng")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + //if already cached, need not to read again + if (isset($this->imagelist[$file])) { + $data = null; + } else { + // Example for transparency handling on new image. Retain for current image + // $tIndex = imagecolortransparent($img); + // if ($tIndex > 0) { + // $tColor = imagecolorsforindex($img, $tIndex); + // $new_tIndex = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']); + // imagefill($new_img, 0, 0, $new_tIndex); + // imagecolortransparent($new_img, $new_tIndex); + // } + // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn + //imagealphablending($img, true); + + //default, but explicitely set to ensure pdf compatibility + imagesavealpha($img, false/*!$is_mask && !$mask*/); + + $error = 0; + //DEBUG_IMG_TEMP + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addImagePng ' . $file . ']'; + } + + ob_start(); + @imagepng($img); + $data = ob_get_clean(); + + if ($data == '') { + $error = 1; + $errormsg = 'trouble writing file from GD'; + //DEBUG_IMG_TEMP + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print 'trouble writing file from GD'; + } + } + + if ($error) { + $this->addMessage('PNG error - (' . $file . ') ' . $errormsg); + + return; + } + } //End isset($this->imagelist[$file]) (png Duplicate removal) + + $this->addPngFromBuf($data, $file, $x, $y, $w, $h, $is_mask, $mask); + } + + /** + * @param $file + * @param $x + * @param $y + * @param $w + * @param $h + * @param $byte + */ + protected function addImagePngAlpha($file, $x, $y, $w, $h, $byte) + { + // generate images + $img = imagecreatefrompng($file); + + if ($img === false) { + return; + } + + // FIXME The pixel transformation doesn't work well with 8bit PNGs + $eight_bit = ($byte & 4) !== 4; + + $wpx = imagesx($img); + $hpx = imagesy($img); + + imagesavealpha($img, false); + + // create temp alpha file + $tempfile_alpha = @tempnam($this->tmp, "cpdf_img_"); + @unlink($tempfile_alpha); + $tempfile_alpha = "$tempfile_alpha.png"; + + // create temp plain file + $tempfile_plain = @tempnam($this->tmp, "cpdf_img_"); + @unlink($tempfile_plain); + $tempfile_plain = "$tempfile_plain.png"; + + $imgalpha = imagecreate($wpx, $hpx); + imagesavealpha($imgalpha, false); + + // generate gray scale palette (0 -> 255) + for ($c = 0; $c < 256; ++$c) { + imagecolorallocate($imgalpha, $c, $c, $c); + } + + // Use PECL gmagick + Graphics Magic to process transparent PNG images + if (extension_loaded("gmagick")) { + $gmagick = new \Gmagick($file); + $gmagick->setimageformat('png'); + + // Get opacity channel (negative of alpha channel) + $alpha_channel_neg = clone $gmagick; + $alpha_channel_neg->separateimagechannel(\Gmagick::CHANNEL_OPACITY); + + // Negate opacity channel + $alpha_channel = new \Gmagick(); + $alpha_channel->newimage($wpx, $hpx, "#FFFFFF", "png"); + $alpha_channel->compositeimage($alpha_channel_neg, \Gmagick::COMPOSITE_DIFFERENCE, 0, 0); + $alpha_channel->separateimagechannel(\Gmagick::CHANNEL_RED); + $alpha_channel->writeimage($tempfile_alpha); + + // Cast to 8bit+palette + $imgalpha_ = imagecreatefrompng($tempfile_alpha); + imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($imgalpha_); + imagepng($imgalpha, $tempfile_alpha); + + // Make opaque image + $color_channels = new \Gmagick(); + $color_channels->newimage($wpx, $hpx, "#FFFFFF", "png"); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYRED, 0, 0); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYGREEN, 0, 0); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYBLUE, 0, 0); + $color_channels->writeimage($tempfile_plain); + + $imgplain = imagecreatefrompng($tempfile_plain); + } + // Use PECL imagick + ImageMagic to process transparent PNG images + elseif (extension_loaded("imagick")) { + // Native cloning was added to pecl-imagick in svn commit 263814 + // the first version containing it was 3.0.1RC1 + static $imagickClonable = null; + if ($imagickClonable === null) { + $imagickClonable = true; + if (defined('Imagick::IMAGICK_EXTVER')) { + $imagickVersion = \Imagick::IMAGICK_EXTVER; + } else { + $imagickVersion = '0'; + } + if (version_compare($imagickVersion, '0.0.1', '>=')) { + $imagickClonable = version_compare($imagickVersion, '3.0.1rc1', '>='); + } + } + + $imagick = new \Imagick($file); + $imagick->setFormat('png'); + + // Get opacity channel (negative of alpha channel) + if ($imagick->getImageAlphaChannel() !== 0) { + $alpha_channel = $imagickClonable ? clone $imagick : $imagick->clone(); + $alpha_channel->separateImageChannel(\Imagick::CHANNEL_ALPHA); + // Since ImageMagick7 negate invert transparency as default + if (\Imagick::getVersion()['versionNumber'] < 1800) { + $alpha_channel->negateImage(true); + } + $alpha_channel->writeImage($tempfile_alpha); + + // Cast to 8bit+palette + $imgalpha_ = imagecreatefrompng($tempfile_alpha); + imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($imgalpha_); + imagepng($imgalpha, $tempfile_alpha); + } else { + $tempfile_alpha = null; + } + + // Make opaque image + $color_channels = new \Imagick(); + $color_channels->newImage($wpx, $hpx, "#FFFFFF", "png"); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYRED, 0, 0); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYGREEN, 0, 0); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYBLUE, 0, 0); + $color_channels->writeImage($tempfile_plain); + + $imgplain = imagecreatefrompng($tempfile_plain); + } else { + // allocated colors cache + $allocated_colors = []; + + // extract alpha channel + for ($xpx = 0; $xpx < $wpx; ++$xpx) { + for ($ypx = 0; $ypx < $hpx; ++$ypx) { + $color = imagecolorat($img, $xpx, $ypx); + $col = imagecolorsforindex($img, $color); + $alpha = $col['alpha']; + + if ($eight_bit) { + // with gamma correction + $gammacorr = 2.2; + $pixel = round(pow((((127 - $alpha) * 255 / 127) / 255), $gammacorr) * 255); + } else { + // without gamma correction + $pixel = (127 - $alpha) * 2; + + $key = $col['red'] . $col['green'] . $col['blue']; + + if (!isset($allocated_colors[$key])) { + $pixel_img = imagecolorallocate($img, $col['red'], $col['green'], $col['blue']); + $allocated_colors[$key] = $pixel_img; + } else { + $pixel_img = $allocated_colors[$key]; + } + + imagesetpixel($img, $xpx, $ypx, $pixel_img); + } + + imagesetpixel($imgalpha, $xpx, $ypx, $pixel); + } + } + + // extract image without alpha channel + $imgplain = imagecreatetruecolor($wpx, $hpx); + imagecopy($imgplain, $img, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($img); + + imagepng($imgalpha, $tempfile_alpha); + imagepng($imgplain, $tempfile_plain); + } + + $this->imageAlphaList[$file] = [$tempfile_alpha, $tempfile_plain]; + + // embed mask image + if ($tempfile_alpha) { + $this->addImagePng($imgalpha, $tempfile_alpha, $x, $y, $w, $h, true); + imagedestroy($imgalpha); + $this->imageCache[] = $tempfile_alpha; + } + + // embed image, masked with previously embedded mask + $this->addImagePng($imgplain, $tempfile_plain, $x, $y, $w, $h, false, ($tempfile_alpha !== null)); + imagedestroy($imgplain); + $this->imageCache[] = $tempfile_plain; + } + + /** + * add a PNG image into the document, from a file + * this should work with remote files + * + * @param $file + * @param $x + * @param $y + * @param int $w + * @param int $h + * @throws Exception + */ + function addPngFromFile($file, $x, $y, $w = 0, $h = 0) + { + if (!function_exists("imagecreatefrompng")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + if (isset($this->imageAlphaList[$file])) { + [$alphaFile, $plainFile] = $this->imageAlphaList[$file]; + + if ($alphaFile) { + $img = null; + $this->addImagePng($img, $alphaFile, $x, $y, $w, $h, true); + } + + $img = null; + $this->addImagePng($img, $plainFile, $x, $y, $w, $h, false, ($plainFile !== null)); + return; + } + + //if already cached, need not to read again + if (isset($this->imagelist[$file])) { + $img = null; + } else { + $info = file_get_contents($file, false, null, 24, 5); + $meta = unpack("CbitDepth/CcolorType/CcompressionMethod/CfilterMethod/CinterlaceMethod", $info); + $bit_depth = $meta["bitDepth"]; + $color_type = $meta["colorType"]; + + // http://www.w3.org/TR/PNG/#11IHDR + // 3 => indexed + // 4 => greyscale with alpha + // 6 => fullcolor with alpha + $is_alpha = in_array($color_type, [4, 6]) || ($color_type == 3 && $bit_depth != 4); + + if ($is_alpha) { // exclude grayscale alpha + $this->addImagePngAlpha($file, $x, $y, $w, $h, $color_type); + return; + } + + //png files typically contain an alpha channel. + //pdf file format or class.pdf does not support alpha blending. + //on alpha blended images, more transparent areas have a color near black. + //This appears in the result on not storing the alpha channel. + //Correct would be the box background image or its parent when transparent. + //But this would make the image dependent on the background. + //Therefore create an image with white background and copy in + //A more natural background than black is white. + //Therefore create an empty image with white background and merge the + //image in with alpha blending. + $imgtmp = @imagecreatefrompng($file); + if (!$imgtmp) { + return; + } + $sx = imagesx($imgtmp); + $sy = imagesy($imgtmp); + $img = imagecreatetruecolor($sx, $sy); + imagealphablending($img, true); + + // @todo is it still needed ?? + $ti = imagecolortransparent($imgtmp); + if ($ti >= 0) { + $tc = imagecolorsforindex($imgtmp, $ti); + $ti = imagecolorallocate($img, $tc['red'], $tc['green'], $tc['blue']); + imagefill($img, 0, 0, $ti); + imagecolortransparent($img, $ti); + } else { + imagefill($img, 1, 1, imagecolorallocate($img, 255, 255, 255)); + } + + imagecopy($img, $imgtmp, 0, 0, 0, 0, $sx, $sy); + imagedestroy($imgtmp); + } + $this->addImagePng($img, $file, $x, $y, $w, $h); + + if ($img) { + imagedestroy($img); + } + } + + /** + * add a PNG image into the document, from a memory buffer of the file + * + * @param $data + * @param $file + * @param $x + * @param $y + * @param float $w + * @param float $h + * @param bool $is_mask + * @param null $mask + */ + function addPngFromBuf(&$data, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null) + { + if (isset($this->imagelist[$file])) { + $data = null; + $info['width'] = $this->imagelist[$file]['w']; + $info['height'] = $this->imagelist[$file]['h']; + $label = $this->imagelist[$file]['label']; + } else { + if ($data == null) { + $this->addMessage('addPngFromBuf error - data not present!'); + + return; + } + + $error = 0; + + if (!$error) { + $header = chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10); + + if (mb_substr($data, 0, 8, '8bit') != $header) { + $error = 1; + + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile this file does not have a valid header ' . $file . ']'; + } + + $errormsg = 'this file does not have a valid header'; + } + } + + if (!$error) { + // set pointer + $p = 8; + $len = mb_strlen($data, '8bit'); + + // cycle through the file, identifying chunks + $haveHeader = 0; + $info = []; + $idata = ''; + $pdata = ''; + + while ($p < $len) { + $chunkLen = $this->getBytes($data, $p, 4); + $chunkType = mb_substr($data, $p + 4, 4, '8bit'); + + switch ($chunkType) { + case 'IHDR': + // this is where all the file information comes from + $info['width'] = $this->getBytes($data, $p + 8, 4); + $info['height'] = $this->getBytes($data, $p + 12, 4); + $info['bitDepth'] = ord($data[$p + 16]); + $info['colorType'] = ord($data[$p + 17]); + $info['compressionMethod'] = ord($data[$p + 18]); + $info['filterMethod'] = ord($data[$p + 19]); + $info['interlaceMethod'] = ord($data[$p + 20]); + + //print_r($info); + $haveHeader = 1; + if ($info['compressionMethod'] != 0) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported compression method ' . $file . ']'; + } + + $errormsg = 'unsupported compression method'; + } + + if ($info['filterMethod'] != 0) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported filter method ' . $file . ']'; + } + + $errormsg = 'unsupported filter method'; + } + break; + + case 'PLTE': + $pdata .= mb_substr($data, $p + 8, $chunkLen, '8bit'); + break; + + case 'IDAT': + $idata .= mb_substr($data, $p + 8, $chunkLen, '8bit'); + break; + + case 'tRNS': + //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk + //print "tRNS found, color type = ".$info['colorType']."\n"; + $transparency = []; + + switch ($info['colorType']) { + // indexed color, rbg + case 3: + /* corresponding to entries in the plte chunk + Alpha for palette index 0: 1 byte + Alpha for palette index 1: 1 byte + ...etc... + */ + // there will be one entry for each palette entry. up until the last non-opaque entry. + // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent) + $transparency['type'] = 'indexed'; + $trans = 0; + + for ($i = $chunkLen; $i >= 0; $i--) { + if (ord($data[$p + 8 + $i]) == 0) { + $trans = $i; + } + } + + $transparency['data'] = $trans; + break; + + // grayscale + case 0: + /* corresponding to entries in the plte chunk + Gray: 2 bytes, range 0 .. (2^bitdepth)-1 + */ + // $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale + $transparency['type'] = 'indexed'; + $transparency['data'] = ord($data[$p + 8 + 1]); + break; + + // truecolor + case 2: + /* corresponding to entries in the plte chunk + Red: 2 bytes, range 0 .. (2^bitdepth)-1 + Green: 2 bytes, range 0 .. (2^bitdepth)-1 + Blue: 2 bytes, range 0 .. (2^bitdepth)-1 + */ + $transparency['r'] = $this->getBytes($data, $p + 8, 2); + // r from truecolor + $transparency['g'] = $this->getBytes($data, $p + 10, 2); + // g from truecolor + $transparency['b'] = $this->getBytes($data, $p + 12, 2); + // b from truecolor + + $transparency['type'] = 'color-key'; + break; + + //unsupported transparency type + default: + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported transparency type ' . $file . ']'; + } + break; + } + + // KS End new code + break; + + default: + break; + } + + $p += $chunkLen + 12; + } + + if (!$haveHeader) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile information header is missing ' . $file . ']'; + } + + $errormsg = 'information header is missing'; + } + + if (isset($info['interlaceMethod']) && $info['interlaceMethod']) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile no support for interlaced images in pdf ' . $file . ']'; + } + + $errormsg = 'There appears to be no support for interlaced images in pdf.'; + } + } + + if (!$error && $info['bitDepth'] > 8) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile bit depth of 8 or less is supported ' . $file . ']'; + } + + $errormsg = 'only bit depth of 8 or less is supported'; + } + + if (!$error) { + switch ($info['colorType']) { + case 3: + $color = 'DeviceRGB'; + $ncolor = 1; + break; + + case 2: + $color = 'DeviceRGB'; + $ncolor = 3; + break; + + case 0: + $color = 'DeviceGray'; + $ncolor = 1; + break; + + default: + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile alpha channel not supported: ' . $info['colorType'] . ' ' . $file . ']'; + } + + $errormsg = 'transparency alpha channel not supported, transparency only supported for palette images.'; + } + } + + if ($error) { + $this->addMessage('PNG error - (' . $file . ') ' . $errormsg); + + return; + } + + //print_r($info); + // so this image is ok... add it in. + $this->numImages++; + $im = $this->numImages; + $label = "I$im"; + $this->numObj++; + + // $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width'])); + $options = [ + 'label' => $label, + 'data' => $idata, + 'bitsPerComponent' => $info['bitDepth'], + 'pdata' => $pdata, + 'iw' => $info['width'], + 'ih' => $info['height'], + 'type' => 'png', + 'color' => $color, + 'ncolor' => $ncolor, + 'masked' => $mask, + 'isMask' => $is_mask + ]; + + if (isset($transparency)) { + $options['transparency'] = $transparency; + } + + $this->o_image($this->numObj, 'new', $options); + $this->imagelist[$file] = ['label' => $label, 'w' => $info['width'], 'h' => $info['height']]; + } + + if ($is_mask) { + return; + } + + if ($w <= 0 && $h <= 0) { + $w = $info['width']; + $h = $info['height']; + } + + if ($w <= 0) { + $w = $h / $info['height'] * $info['width']; + } + + if ($h <= 0) { + $h = $w * $info['height'] / $info['width']; + } + + $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ", $w, $h, $x, $y, $label)); + } + + /** + * add a JPEG image into the document, from a file + * + * @param $img + * @param $x + * @param $y + * @param int $w + * @param int $h + */ + function addJpegFromFile($img, $x, $y, $w = 0, $h = 0) + { + // attempt to add a jpeg image straight from a file, using no GD commands + // note that this function is unable to operate on a remote file. + + if (!file_exists($img)) { + return; + } + + if ($this->image_iscached($img)) { + $data = null; + $imageWidth = $this->imagelist[$img]['w']; + $imageHeight = $this->imagelist[$img]['h']; + $channels = $this->imagelist[$img]['c']; + } else { + $tmp = getimagesize($img); + $imageWidth = $tmp[0]; + $imageHeight = $tmp[1]; + + if (isset($tmp['channels'])) { + $channels = $tmp['channels']; + } else { + $channels = 3; + } + + $data = file_get_contents($img); + } + + if ($w <= 0 && $h <= 0) { + $w = $imageWidth; + } + + if ($w == 0) { + $w = $h / $imageHeight * $imageWidth; + } + + if ($h == 0) { + $h = $w * $imageHeight / $imageWidth; + } + + $this->addJpegImage_common($data, $img, $imageWidth, $imageHeight, $x, $y, $w, $h, $channels); + } + + /** + * common code used by the two JPEG adding functions + * @param $data + * @param $imgname + * @param $imageWidth + * @param $imageHeight + * @param $x + * @param $y + * @param int $w + * @param int $h + * @param int $channels + */ + private function addJpegImage_common( + &$data, + $imgname, + $imageWidth, + $imageHeight, + $x, + $y, + $w = 0, + $h = 0, + $channels = 3 + ) { + if ($this->image_iscached($imgname)) { + $label = $this->imagelist[$imgname]['label']; + //debugpng + //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']'; + + } else { + if ($data == null) { + $this->addMessage('addJpegImage_common error - (' . $imgname . ') data not present!'); + + return; + } + + // note that this function is not to be called externally + // it is just the common code between the GD and the file options + $this->numImages++; + $im = $this->numImages; + $label = "I$im"; + $this->numObj++; + + $this->o_image( + $this->numObj, + 'new', + [ + 'label' => $label, + 'data' => &$data, + 'iw' => $imageWidth, + 'ih' => $imageHeight, + 'channels' => $channels + ] + ); + + $this->imagelist[$imgname] = [ + 'label' => $label, + 'w' => $imageWidth, + 'h' => $imageHeight, + 'c' => $channels + ]; + } + + $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ ", $w, $h, $x, $y, $label)); + } + + /** + * specify where the document should open when it first starts + * + * @param $style + * @param int $a + * @param int $b + * @param int $c + */ + function openHere($style, $a = 0, $b = 0, $c = 0) + { + // this function will open the document at a specified page, in a specified style + // the values for style, and the required parameters are: + // 'XYZ' left, top, zoom + // 'Fit' + // 'FitH' top + // 'FitV' left + // 'FitR' left,bottom,right + // 'FitB' + // 'FitBH' top + // 'FitBV' left + $this->numObj++; + $this->o_destination( + $this->numObj, + 'new', + ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c] + ); + $id = $this->catalogId; + $this->o_catalog($id, 'openHere', $this->numObj); + } + + /** + * Add JavaScript code to the PDF document + * + * @param string $code + */ + function addJavascript($code) + { + $this->javascript .= $code; + } + + /** + * create a labelled destination within the document + * + * @param $label + * @param $style + * @param int $a + * @param int $b + * @param int $c + */ + function addDestination($label, $style, $a = 0, $b = 0, $c = 0) + { + // associates the given label with the destination, it is done this way so that a destination can be specified after + // it has been linked to + // styles are the same as the 'openHere' function + $this->numObj++; + $this->o_destination( + $this->numObj, + 'new', + ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c] + ); + $id = $this->numObj; + + // store the label->idf relationship, note that this means that labels can be used only once + $this->destinations["$label"] = $id; + } + + /** + * define font families, this is used to initialize the font families for the default fonts + * and for the user to add new ones for their fonts. The default bahavious can be overridden should + * that be desired. + * + * @param $family + * @param string $options + */ + function setFontFamily($family, $options = '') + { + if (!is_array($options)) { + if ($family === 'init') { + // set the known family groups + // these font families will be used to enable bold and italic markers to be included + // within text streams. html forms will be used... <b></b> <i></i> + $this->fontFamilies['Helvetica.afm'] = + [ + 'b' => 'Helvetica-Bold.afm', + 'i' => 'Helvetica-Oblique.afm', + 'bi' => 'Helvetica-BoldOblique.afm', + 'ib' => 'Helvetica-BoldOblique.afm' + ]; + + $this->fontFamilies['Courier.afm'] = + [ + 'b' => 'Courier-Bold.afm', + 'i' => 'Courier-Oblique.afm', + 'bi' => 'Courier-BoldOblique.afm', + 'ib' => 'Courier-BoldOblique.afm' + ]; + + $this->fontFamilies['Times-Roman.afm'] = + [ + 'b' => 'Times-Bold.afm', + 'i' => 'Times-Italic.afm', + 'bi' => 'Times-BoldItalic.afm', + 'ib' => 'Times-BoldItalic.afm' + ]; + } + } else { + + // the user is trying to set a font family + // note that this can also be used to set the base ones to something else + if (mb_strlen($family)) { + $this->fontFamilies[$family] = $options; + } + } + } + + /** + * used to add messages for use in debugging + * + * @param $message + */ + function addMessage($message) + { + $this->messages .= $message . "\n"; + } + + /** + * a few functions which should allow the document to be treated transactionally. + * + * @param $action + */ + function transaction($action) + { + switch ($action) { + case 'start': + // store all the data away into the checkpoint variable + $data = get_object_vars($this); + $this->checkpoint = $data; + unset($data); + break; + + case 'commit': + if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])) { + $tmp = $this->checkpoint['checkpoint']; + $this->checkpoint = $tmp; + unset($tmp); + } else { + $this->checkpoint = ''; + } + break; + + case 'rewind': + // do not destroy the current checkpoint, but move us back to the state then, so that we can try again + if (is_array($this->checkpoint)) { + // can only abort if were inside a checkpoint + $tmp = $this->checkpoint; + + foreach ($tmp as $k => $v) { + if ($k !== 'checkpoint') { + $this->$k = $v; + } + } + unset($tmp); + } + break; + + case 'abort': + if (is_array($this->checkpoint)) { + // can only abort if were inside a checkpoint + $tmp = $this->checkpoint; + foreach ($tmp as $k => $v) { + $this->$k = $v; + } + unset($tmp); + } + break; + } + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php new file mode 100644 index 0000000..62cc74a --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php @@ -0,0 +1,495 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Surface; + +use Svg\Document; +use Svg\Style; + +class SurfaceCpdf implements SurfaceInterface +{ + const DEBUG = false; + + /** @var \Svg\Surface\CPdf */ + private $canvas; + + private $width; + private $height; + + /** @var Style */ + private $style; + + public function __construct(Document $doc, $canvas = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $dimensions = $doc->getDimensions(); + $w = $dimensions["width"]; + $h = $dimensions["height"]; + + if (!$canvas) { + $canvas = new \Svg\Surface\CPdf(array(0, 0, $w, $h)); + $refl = new \ReflectionClass($canvas); + $canvas->fontcache = realpath(dirname($refl->getFileName()) . "/../../fonts/")."/"; + } + + // Flip PDF coordinate system so that the origin is in + // the top left rather than the bottom left + $canvas->transform(array( + 1, 0, + 0, -1, + 0, $h + )); + + $this->width = $w; + $this->height = $h; + + $this->canvas = $canvas; + } + + function out() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->canvas->output(); + } + + public function save() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->save(); + } + + public function restore() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->restore(); + } + + public function scale($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->transform($x, 0, 0, $y, 0, 0); + } + + public function rotate($angle) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $a = deg2rad($angle); + $cos_a = cos($a); + $sin_a = sin($a); + + $this->transform( + $cos_a, $sin_a, + -$sin_a, $cos_a, + 0, 0 + ); + } + + public function translate($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->transform( + 1, 0, + 0, 1, + $x, $y + ); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->canvas->transform(array($a, $b, $c, $d, $e, $f)); + } + + public function beginPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement beginPath() method. + } + + public function closePath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->closePath(); + } + + public function fillStroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fillStroke($close); + } + + public function clip() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->clip(); + } + + public function fillText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->addText($x, $y, $this->style->fontSize, $text); + } + + public function strokeText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->addText($x, $y, $this->style->fontSize, $text); + } + + public function drawImage($image, $sx, $sy, $sw = null, $sh = null, $dx = null, $dy = null, $dw = null, $dh = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + if (strpos($image, "data:") === 0) { + $parts = explode(',', $image, 2); + + $data = $parts[1]; + $base64 = false; + + $token = strtok($parts[0], ';'); + while ($token !== false) { + if ($token == 'base64') { + $base64 = true; + } + + $token = strtok(';'); + } + + if ($base64) { + $data = base64_decode($data); + } + } + else { + $data = file_get_contents($image); + } + + $image = tempnam(sys_get_temp_dir(), "svg"); + file_put_contents($image, $data); + + $img = $this->image($image, $sx, $sy, $sw, $sh, "normal"); + + + unlink($image); + } + + public static function getimagesize($filename) + { + static $cache = array(); + + if (isset($cache[$filename])) { + return $cache[$filename]; + } + + list($width, $height, $type) = getimagesize($filename); + + if ($width == null || $height == null) { + $data = file_get_contents($filename, null, null, 0, 26); + + if (substr($data, 0, 2) === "BM") { + $meta = unpack('vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight', $data); + $width = (int)$meta['width']; + $height = (int)$meta['height']; + $type = IMAGETYPE_BMP; + } + } + + return $cache[$filename] = array($width, $height, $type); + } + + function image($img, $x, $y, $w, $h, $resolution = "normal") + { + list($width, $height, $type) = $this->getimagesize($img); + + switch ($type) { + case IMAGETYPE_JPEG: + $this->canvas->addJpegFromFile($img, $x, $y - $h, $w, $h); + break; + + case IMAGETYPE_GIF: + case IMAGETYPE_BMP: + // @todo use cache for BMP and GIF + $img = $this->_convert_gif_bmp_to_png($img, $type); + + case IMAGETYPE_PNG: + $this->canvas->addPngFromFile($img, $x, $y - $h, $w, $h); + break; + + default: + } + } + + public function lineTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->lineTo($x, $y); + } + + public function moveTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->moveTo($x, $y); + } + + public function quadraticCurveTo($cpx, $cpy, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + // FIXME not accurate + $this->canvas->quadTo($cpx, $cpy, $x, $y); + } + + public function bezierCurveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->curveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y); + } + + public function arcTo($x1, $y1, $x2, $y2, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + } + + public function arc($x, $y, $radius, $startAngle, $endAngle, $anticlockwise = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radius, $radius, 0, 8, $startAngle, $endAngle, false, false, false, true); + } + + public function circle($x, $y, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radius, $radius, 0, 8, 0, 360, true, false, false, false); + } + + public function ellipse($x, $y, $radiusX, $radiusY, $rotation, $startAngle, $endAngle, $anticlockwise) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radiusX, $radiusY, 0, 8, 0, 360, false, false, false, false); + } + + public function fillRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->fill(); + } + + public function rect($x, $y, $w, $h, $rx = 0, $ry = 0) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $canvas = $this->canvas; + + if ($rx <= 0.000001/* && $ry <= 0.000001*/) { + $canvas->rect($x, $y, $w, $h); + + return; + } + + $rx = min($rx, $w / 2); + $rx = min($rx, $h / 2); + + /* Define a path for a rectangle with corners rounded by a given radius. + * Start from the lower left corner and proceed counterclockwise. + */ + $this->moveTo($x + $rx, $y); + + /* Start of the arc segment in the lower right corner */ + $this->lineTo($x + $w - $rx, $y); + + /* Arc segment in the lower right corner */ + $this->arc($x + $w - $rx, $y + $rx, $rx, 270, 360); + + /* Start of the arc segment in the upper right corner */ + $this->lineTo($x + $w, $y + $h - $rx ); + + /* Arc segment in the upper right corner */ + $this->arc($x + $w - $rx, $y + $h - $rx, $rx, 0, 90); + + /* Start of the arc segment in the upper left corner */ + $this->lineTo($x + $rx, $y + $h); + + /* Arc segment in the upper left corner */ + $this->arc($x + $rx, $y + $h - $rx, $rx, 90, 180); + + /* Start of the arc segment in the lower left corner */ + $this->lineTo($x , $y + $rx); + + /* Arc segment in the lower left corner */ + $this->arc($x + $rx, $y + $rx, $rx, 180, 270); + } + + public function fill() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fill(); + } + + public function strokeRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->stroke(); + } + + public function stroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->stroke($close); + } + + public function endPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->endPath(); + } + + public function measureText($text) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $style = $this->getStyle(); + $this->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + + return $this->canvas->getTextWidth($this->getStyle()->fontSize, $text); + } + + public function getStyle() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->style; + } + + public function setStyle(Style $style) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->style = $style; + $canvas = $this->canvas; + + if (is_array($style->stroke) && $stroke = $style->stroke) { + $canvas->setStrokeColor(array((float)$stroke[0]/255, (float)$stroke[1]/255, (float)$stroke[2]/255), true); + } + + if (is_array($style->fill) && $fill = $style->fill) { + $canvas->setColor(array((float)$fill[0]/255, (float)$fill[1]/255, (float)$fill[2]/255), true); + } + + if ($fillRule = strtolower($style->fillRule)) { + $canvas->setFillRule($fillRule); + } + + $opacity = $style->opacity; + if ($opacity !== null && $opacity < 1.0) { + $canvas->setLineTransparency("Normal", $opacity); + $canvas->currentLineTransparency = null; + + $canvas->setFillTransparency("Normal", $opacity); + $canvas->currentFillTransparency = null; + } + else { + $fillOpacity = $style->fillOpacity; + if ($fillOpacity !== null && $fillOpacity < 1.0) { + $canvas->setFillTransparency("Normal", $fillOpacity); + $canvas->currentFillTransparency = null; + } + + $strokeOpacity = $style->strokeOpacity; + if ($strokeOpacity !== null && $strokeOpacity < 1.0) { + $canvas->setLineTransparency("Normal", $strokeOpacity); + $canvas->currentLineTransparency = null; + } + } + + $dashArray = null; + if ($style->strokeDasharray) { + $dashArray = preg_split('/\s*,\s*/', $style->strokeDasharray); + } + + + $phase=0; + if ($style->strokeDashoffset) { + $phase = $style->strokeDashoffset; + } + + + $canvas->setLineStyle( + $style->strokeWidth, + $style->strokeLinecap, + $style->strokeLinejoin, + $dashArray, + $phase + ); + + $this->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + } + + public function setFont($family, $style, $weight) + { + $map = [ + "serif" => "times", + "sans-serif" => "helvetica", + "fantasy" => "symbol", + "cursive" => "times", + "monospace" => "courier" + ]; + + $styleMap = [ + "courier" => [ + "" => "Courier", + "b" => "Courier-Bold", + "i" => "Courier-Oblique", + "bi" => "Courier-BoldOblique", + ], + "helvetica" => [ + "" => "Helvetica", + "b" => "Helvetica-Bold", + "i" => "Helvetica-Oblique", + "bi" => "Helvetica-BoldOblique", + ], + "symbol" => [ + "" => "Symbol" + ], + "times" => [ + "" => "Times-Roman", + "b" => "Times-Bold", + "i" => "Times-Italic", + "bi" => "Times-BoldItalic", + ], + ]; + + $family_lc = strtolower($family); + if (isset($map[$family_lc])) { + $family = $map[$family_lc]; + } + + if (isset($styleMap[$family])) { + $key = ""; + + $weight = strtolower($weight); + if ($weight === "bold" || $weight === "bolder" || (is_numeric($weight) && $weight >= 600)) { + $key .= "b"; + } + + $style = strtolower($style); + if ($style === "italic" || $style === "oblique") { + $key .= "i"; + } + + if (isset($styleMap[$family][$key])) { + $family = $styleMap[$family][$key]; + } + } + + $this->canvas->selectFont("$family.afm"); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php new file mode 100644 index 0000000..25b3001 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php @@ -0,0 +1,90 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Surface; + +use Svg\Style; + +/** + * Interface Surface, like CanvasRenderingContext2D + * + * @package Svg + */ +interface SurfaceInterface +{ + public function save(); + + public function restore(); + + // transformations (default transform is the identity matrix) + public function scale($x, $y); + + public function rotate($angle); + + public function translate($x, $y); + + public function transform($a, $b, $c, $d, $e, $f); + + // path ends + public function beginPath(); + + public function closePath(); + + public function fill(); + + public function stroke(bool $close = false); + + public function endPath(); + + public function fillStroke(bool $close = false); + + public function clip(); + + // text (see also the CanvasDrawingStyles interface) + public function fillText($text, $x, $y, $maxWidth = null); + + public function strokeText($text, $x, $y, $maxWidth = null); + + public function measureText($text); + + // drawing images + public function drawImage($image, $sx, $sy, $sw = null, $sh = null, $dx = null, $dy = null, $dw = null, $dh = null); + + // paths + public function lineTo($x, $y); + + public function moveTo($x, $y); + + public function quadraticCurveTo($cpx, $cpy, $x, $y); + + public function bezierCurveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y); + + public function arcTo($x1, $y1, $x2, $y2, $radius); + + public function circle($x, $y, $radius); + + public function arc($x, $y, $radius, $startAngle, $endAngle, $anticlockwise = false); + + public function ellipse($x, $y, $radiusX, $radiusY, $rotation, $startAngle, $endAngle, $anticlockwise); + + // Rectangle + public function rect($x, $y, $w, $h, $rx = 0, $ry = 0); + + public function fillRect($x, $y, $w, $h); + + public function strokeRect($x, $y, $w, $h); + + public function setStyle(Style $style); + + /** + * @return Style + */ + public function getStyle(); + + public function setFont($family, $style, $weight); +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php new file mode 100644 index 0000000..3d25aef --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php @@ -0,0 +1,430 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Surface; + +use Svg\Style; +use Svg\Document; + +class SurfacePDFLib implements SurfaceInterface +{ + const DEBUG = false; + + private $canvas; + + private $width; + private $height; + + /** @var Style */ + private $style; + + public function __construct(Document $doc, $canvas = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $dimensions = $doc->getDimensions(); + $w = $dimensions["width"]; + $h = $dimensions["height"]; + + if (!$canvas) { + $canvas = new \PDFlib(); + + /* all strings are expected as utf8 */ + $canvas->set_option("stringformat=utf8"); + $canvas->set_option("errorpolicy=return"); + + /* open new PDF file; insert a file name to create the PDF on disk */ + if ($canvas->begin_document("", "") == 0) { + die("Error: " . $canvas->get_errmsg()); + } + $canvas->set_info("Creator", "PDFlib starter sample"); + $canvas->set_info("Title", "starter_graphics"); + + $canvas->begin_page_ext($w, $h, ""); + } + + // Flip PDF coordinate system so that the origin is in + // the top left rather than the bottom left + $canvas->setmatrix( + 1, 0, + 0, -1, + 0, $h + ); + + $this->width = $w; + $this->height = $h; + + $this->canvas = $canvas; + } + + function out() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->canvas->end_page_ext(""); + $this->canvas->end_document(""); + + return $this->canvas->get_buffer(); + } + + public function save() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->save(); + } + + public function restore() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->restore(); + } + + public function scale($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->scale($x, $y); + } + + public function rotate($angle) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->rotate($angle); + } + + public function translate($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->translate($x, $y); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->concat($a, $b, $c, $d, $e, $f); + } + + public function beginPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement beginPath() method. + } + + public function closePath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->closepath(); + } + + public function fillStroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + if ($close) { + $this->canvas->closepath_fill_stroke(); + } else { + $this->canvas->fill_stroke(); + } + } + + public function clip() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->clip(); + } + + public function fillText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->set_text_pos($x, $y); + $this->canvas->show($text); + } + + public function strokeText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement drawImage() method. + } + + public function drawImage($image, $sx, $sy, $sw = null, $sh = null, $dx = null, $dy = null, $dw = null, $dh = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + if (strpos($image, "data:") === 0) { + $data = substr($image, strpos($image, ";") + 1); + if (strpos($data, "base64") === 0) { + $data = base64_decode(substr($data, 7)); + } + } + else { + $data = file_get_contents($image); + } + + $image = tempnam(sys_get_temp_dir(), "svg"); + file_put_contents($image, $data); + + $img = $this->canvas->load_image("auto", $image, ""); + + $sy = $sy - $sh; + $this->canvas->fit_image($img, $sx, $sy, 'boxsize={' . "$sw $sh" . '} fitmethod=entire'); + + unlink($image); + } + + public function lineTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->lineto($x, $y); + } + + public function moveTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->moveto($x, $y); + } + + public function quadraticCurveTo($cpx, $cpy, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + // FIXME not accurate + $this->canvas->curveTo($cpx, $cpy, $cpx, $cpy, $x, $y); + } + + public function bezierCurveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->curveto($cp1x, $cp1y, $cp2x, $cp2y, $x, $y); + } + + public function arcTo($x1, $y1, $x2, $y2, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + } + + public function arc($x, $y, $radius, $startAngle, $endAngle, $anticlockwise = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->arc($x, $y, $radius, $startAngle, $endAngle); + } + + public function circle($x, $y, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->circle($x, $y, $radius); + } + + public function ellipse($x, $y, $radiusX, $radiusY, $rotation, $startAngle, $endAngle, $anticlockwise) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radiusX, $radiusY); + } + + public function fillRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->fill(); + } + + public function rect($x, $y, $w, $h, $rx = 0, $ry = 0) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $canvas = $this->canvas; + + if ($rx <= 0.000001/* && $ry <= 0.000001*/) { + $canvas->rect($x, $y, $w, $h); + + return; + } + + /* Define a path for a rectangle with corners rounded by a given radius. + * Start from the lower left corner and proceed counterclockwise. + */ + $canvas->moveto($x + $rx, $y); + + /* Start of the arc segment in the lower right corner */ + $canvas->lineto($x + $w - $rx, $y); + + /* Arc segment in the lower right corner */ + $canvas->arc($x + $w - $rx, $y + $rx, $rx, 270, 360); + + /* Start of the arc segment in the upper right corner */ + $canvas->lineto($x + $w, $y + $h - $rx ); + + /* Arc segment in the upper right corner */ + $canvas->arc($x + $w - $rx, $y + $h - $rx, $rx, 0, 90); + + /* Start of the arc segment in the upper left corner */ + $canvas->lineto($x + $rx, $y + $h); + + /* Arc segment in the upper left corner */ + $canvas->arc($x + $rx, $y + $h - $rx, $rx, 90, 180); + + /* Start of the arc segment in the lower left corner */ + $canvas->lineto($x , $y + $rx); + + /* Arc segment in the lower left corner */ + $canvas->arc($x + $rx, $y + $rx, $rx, 180, 270); + } + + public function fill() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fill(); + } + + public function strokeRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->stroke(); + } + + public function stroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + if ($close) { + $this->canvas->closepath_stroke(); + } else { + $this->canvas->stroke(); + } + } + + public function endPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->endPath(); + } + + public function measureText($text) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $style = $this->getStyle(); + $font = $this->getFont($style->fontFamily, $style->fontStyle); + + return $this->canvas->stringwidth($text, $font, $this->getStyle()->fontSize); + } + + public function getStyle() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->style; + } + + public function setStyle(Style $style) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->style = $style; + $canvas = $this->canvas; + + if (is_array($style->stroke) && $stroke = $style->stroke) { + $canvas->setcolor( + "stroke", + "rgb", + $stroke[0] / 255, + $stroke[1] / 255, + $stroke[2] / 255, + null + ); + } + + if (is_array($style->fill) && $fill = $style->fill) { + $canvas->setcolor( + "fill", + "rgb", + $fill[0] / 255, + $fill[1] / 255, + $fill[2] / 255, + null + ); + } + + if ($fillRule = strtolower($style->fillRule)) { + $map = array( + "nonzero" => "winding", + "evenodd" => "evenodd", + ); + + if (isset($map[$fillRule])) { + $fillRule = $map[$fillRule]; + + $canvas->set_parameter("fillrule", $fillRule); + } + } + + $opts = array(); + if ($style->strokeWidth > 0.000001) { + $opts[] = "linewidth=$style->strokeWidth"; + } + + if (in_array($style->strokeLinecap, array("butt", "round", "projecting"))) { + $opts[] = "linecap=$style->strokeLinecap"; + } + + if (in_array($style->strokeLinejoin, array("miter", "round", "bevel"))) { + $opts[] = "linejoin=$style->strokeLinejoin"; + } + + $canvas->set_graphics_option(implode(" ", $opts)); + + $opts = array(); + $opacity = $style->opacity; + if ($opacity !== null && $opacity < 1.0) { + $opts[] = "opacityfill=$opacity"; + $opts[] = "opacitystroke=$opacity"; + } + else { + $fillOpacity = $style->fillOpacity; + if ($fillOpacity !== null && $fillOpacity < 1.0) { + $opts[] = "opacityfill=$fillOpacity"; + } + + $strokeOpacity = $style->strokeOpacity; + if ($strokeOpacity !== null && $strokeOpacity < 1.0) { + $opts[] = "opacitystroke=$strokeOpacity"; + } + } + + if (count($opts)) { + $gs = $canvas->create_gstate(implode(" ", $opts)); + $canvas->set_gstate($gs); + } + + $font = $this->getFont($style->fontFamily, $style->fontStyle); + if ($font) { + $canvas->setfont($font, $style->fontSize); + } + } + + private function getFont($family, $style) + { + $map = array( + "serif" => "Times", + "sans-serif" => "Helvetica", + "fantasy" => "Symbol", + "cursive" => "Times", + "monospace" => "Courier", + + "arial" => "Helvetica", + "verdana" => "Helvetica", + ); + + $family = strtolower($family); + if (isset($map[$family])) { + $family = $map[$family]; + } + + return $this->canvas->load_font($family, "unicode", "fontstyle=$style"); + } + + public function setFont($family, $style, $weight) + { + // TODO: Implement setFont() method. + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php new file mode 100644 index 0000000..9fa6793 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php @@ -0,0 +1,236 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\CssLength; +use Svg\Document; +use Svg\Style; + +abstract class AbstractTag +{ + /** @var Document */ + protected $document; + + public $tagName; + + /** @var Style */ + protected $style; + + protected $attributes = array(); + + protected $hasShape = true; + + /** @var self[] */ + protected $children = array(); + + public function __construct(Document $document, $tagName) + { + $this->document = $document; + $this->tagName = $tagName; + } + + public function getDocument(){ + return $this->document; + } + + /** + * @return Group|null + */ + public function getParentGroup() { + $stack = $this->getDocument()->getStack(); + for ($i = count($stack)-2; $i >= 0; $i--) { + $tag = $stack[$i]; + + if ($tag instanceof Group || $tag instanceof Document) { + return $tag; + } + } + + return null; + } + + public function handle($attributes) + { + $this->attributes = $attributes; + + if (!$this->getDocument()->inDefs) { + $this->before($attributes); + $this->start($attributes); + } + } + + public function handleEnd() + { + if (!$this->getDocument()->inDefs) { + $this->end(); + $this->after(); + } + } + + protected function before($attributes) + { + } + + protected function start($attributes) + { + } + + protected function end() + { + } + + protected function after() + { + } + + public function getAttributes() + { + return $this->attributes; + } + + protected function setStyle(Style $style) + { + $this->style = $style; + + if ($style->display === "none") { + $this->hasShape = false; + } + } + + /** + * @return Style + */ + public function getStyle() + { + return $this->style; + } + + /** + * Make a style object from the tag and its attributes + * + * @param array $attributes + * + * @return Style + */ + protected function makeStyle($attributes) { + $style = new Style(); + $style->inherit($this); + $style->fromStyleSheets($this, $attributes); + $style->fromAttributes($attributes); + + return $style; + } + + protected function applyTransform($attributes) + { + + if (isset($attributes["transform"])) { + $surface = $this->document->getSurface(); + + $transform = $attributes["transform"]; + + $matches = array(); + preg_match_all( + '/(matrix|translate|scale|rotate|skew|skewX|skewY)\((.*?)\)/is', + $transform, + $matches, + PREG_SET_ORDER + ); + + $transformations = array(); + foreach ($matches as $match) { + $arguments = preg_split('/[ ,]+/', $match[2]); + array_unshift($arguments, $match[1]); + $transformations[] = $arguments; + } + + foreach ($transformations as $t) { + switch ($t[0]) { + case "matrix": + $surface->transform($t[1], $t[2], $t[3], $t[4], $t[5], $t[6]); + break; + + case "translate": + $surface->translate($t[1], isset($t[2]) ? $t[2] : 0); + break; + + case "scale": + $surface->scale($t[1], isset($t[2]) ? $t[2] : $t[1]); + break; + + case "rotate": + if (isset($t[2])) { + $t[3] = isset($t[3]) ? $t[3] : 0; + $surface->translate($t[2], $t[3]); + $surface->rotate($t[1]); + $surface->translate(-$t[2], -$t[3]); + } else { + $surface->rotate($t[1]); + } + break; + + case "skewX": + $tan_x = tan(deg2rad($t[1])); + $surface->transform(1, 0, $tan_x, 1, 0, 0); + break; + + case "skewY": + $tan_y = tan(deg2rad($t[1])); + $surface->transform(1, $tan_y, 0, 1, 0, 0); + break; + } + } + } + } + + /** + * Convert the given size for the context of this current tag. + * Takes a pixel-based reference, which is usually specific to the context of the size, + * but the actual reference size will be decided based upon the unit used. + * + * @param string $size + * @param float $pxReference + * + * @return float + */ + protected function convertSize(string $size, float $pxReference): float + { + $length = new CssLength($size); + $reference = $pxReference; + $defaultFontSize = 12; + + switch ($length->getUnit()) { + case "em": + $reference = $this->style->fontSize ?? $defaultFontSize; + break; + case "rem": + $reference = $this->document->style->fontSize ?? $defaultFontSize; + break; + case "ex": + case "ch": + $emRef = $this->style->fontSize ?? $defaultFontSize; + $reference = $emRef * 0.5; + break; + case "vw": + $reference = $this->getDocument()->getWidth(); + break; + case "vh": + $reference = $this->getDocument()->getHeight(); + break; + case "vmin": + $reference = min($this->getDocument()->getHeight(), $this->getDocument()->getWidth()); + break; + case "vmax": + $reference = max($this->getDocument()->getHeight(), $this->getDocument()->getWidth()); + break; + } + + return (new CssLength($size))->toPixels($reference); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Anchor.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Anchor.php new file mode 100644 index 0000000..6979495 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Anchor.php @@ -0,0 +1,14 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class Anchor extends Group +{ + +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Circle.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Circle.php new file mode 100644 index 0000000..e504ffe --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Circle.php @@ -0,0 +1,36 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Circle extends Shape +{ + protected $cx = 0; + protected $cy = 0; + protected $r; + + public function start($attributes) + { + if (isset($attributes['cx'])) { + $width = $this->document->getWidth(); + $this->cx = $this->convertSize($attributes['cx'], $width); + } + if (isset($attributes['cy'])) { + $height = $this->document->getHeight(); + $this->cy = $this->convertSize($attributes['cy'], $height); + } + if (isset($attributes['r'])) { + $diagonal = $this->document->getDiagonal(); + $this->r = $this->convertSize($attributes['r'], $diagonal); + } + + $this->document->getSurface()->circle($this->cx, $this->cy, $this->r); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php new file mode 100644 index 0000000..46722f9 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php @@ -0,0 +1,33 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class ClipPath extends AbstractTag +{ + protected function before($attributes) + { + $surface = $this->document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php new file mode 100644 index 0000000..42891e0 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php @@ -0,0 +1,42 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Ellipse extends Shape +{ + protected $cx = 0; + protected $cy = 0; + protected $rx = 0; + protected $ry = 0; + + public function start($attributes) + { + parent::start($attributes); + + $width = $this->document->getWidth(); + $height = $this->document->getHeight(); + + if (isset($attributes['cx'])) { + $this->cx = $this->convertSize($attributes['cx'], $width); + } + if (isset($attributes['cy'])) { + $this->cy = $this->convertSize($attributes['cy'], $height); + } + if (isset($attributes['rx'])) { + $this->rx = $this->convertSize($attributes['rx'], $width); + } + if (isset($attributes['ry'])) { + $this->ry = $this->convertSize($attributes['ry'], $height); + } + + $this->document->getSurface()->ellipse($this->cx, $this->cy, $this->rx, $this->ry, 0, 0, 360, false); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Group.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Group.php new file mode 100644 index 0000000..bacb385 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Group.php @@ -0,0 +1,33 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Group extends AbstractTag +{ + protected function before($attributes) + { + $surface = $this->document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Image.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Image.php new file mode 100644 index 0000000..bda17ea --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Image.php @@ -0,0 +1,68 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Image extends AbstractTag +{ + protected $x = 0; + protected $y = 0; + protected $width = 0; + protected $height = 0; + protected $href = null; + + protected function before($attributes) + { + parent::before($attributes); + + $surface = $this->document->getSurface(); + $surface->save(); + + $this->applyTransform($attributes); + } + + public function start($attributes) + { + $height = $this->document->getHeight(); + $width = $this->document->getWidth(); + $this->y = $height; + + if (isset($attributes['x'])) { + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $height - $this->convertSize($attributes['y'], $height); + } + + if (isset($attributes['width'])) { + $this->width = $this->convertSize($attributes['width'], $width); + } + if (isset($attributes['height'])) { + $this->height = $this->convertSize($attributes['height'], $height); + } + + if (isset($attributes['xlink:href'])) { + $this->href = $attributes['xlink:href']; + } + + if (isset($attributes['href'])) { + $this->href = $attributes['href']; + } + + $this->document->getSurface()->transform(1, 0, 0, -1, 0, $height); + + $this->document->getSurface()->drawImage($this->href, $this->x, $this->y, $this->width, $this->height); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Line.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Line.php new file mode 100644 index 0000000..fb3b64c --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Line.php @@ -0,0 +1,43 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Line extends Shape +{ + protected $x1 = 0; + protected $y1 = 0; + + protected $x2 = 0; + protected $y2 = 0; + + public function start($attributes) + { + $height = $this->document->getHeight(); + $width = $this->document->getWidth(); + + if (isset($attributes['x1'])) { + $this->x1 = $this->convertSize($attributes['x1'], $width); + } + if (isset($attributes['y1'])) { + $this->y1 = $this->convertSize($attributes['y1'], $height); + } + if (isset($attributes['x2'])) { + $this->x2 = $this->convertSize($attributes['x2'], $width); + } + if (isset($attributes['y2'])) { + $this->y2 = $this->convertSize($attributes['y2'], $height); + } + + $surface = $this->document->getSurface(); + $surface->moveTo($this->x1, $this->y1); + $surface->lineTo($this->x2, $this->y2); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php new file mode 100644 index 0000000..c5e6397 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php @@ -0,0 +1,83 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + + +use Svg\Gradient; +use Svg\Style; + +class LinearGradient extends AbstractTag +{ + protected $x1; + protected $y1; + protected $x2; + protected $y2; + + /** @var Gradient\Stop[] */ + protected $stops = array(); + + public function start($attributes) + { + parent::start($attributes); + + if (isset($attributes['x1'])) { + $this->x1 = $attributes['x1']; + } + if (isset($attributes['y1'])) { + $this->y1 = $attributes['y1']; + } + if (isset($attributes['x2'])) { + $this->x2 = $attributes['x2']; + } + if (isset($attributes['y2'])) { + $this->y2 = $attributes['y2']; + } + } + + public function getStops() { + if (empty($this->stops)) { + foreach ($this->children as $_child) { + if ($_child->tagName != "stop") { + continue; + } + + $_stop = new Gradient\Stop(); + $_attributes = $_child->attributes; + + // Style + if (isset($_attributes["style"])) { + $_style = Style::parseCssStyle($_attributes["style"]); + + if (isset($_style["stop-color"])) { + $_stop->color = Style::parseColor($_style["stop-color"]); + } + + if (isset($_style["stop-opacity"])) { + $_stop->opacity = max(0, min(1.0, $_style["stop-opacity"])); + } + } + + // Attributes + if (isset($_attributes["offset"])) { + $_stop->offset = $_attributes["offset"]; + } + if (isset($_attributes["stop-color"])) { + $_stop->color = Style::parseColor($_attributes["stop-color"]); + } + if (isset($_attributes["stop-opacity"])) { + $_stop->opacity = max(0, min(1.0, $_attributes["stop-opacity"])); + } + + $this->stops[] = $_stop; + } + } + + return $this->stops; + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Path.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Path.php new file mode 100644 index 0000000..3dce7a6 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Path.php @@ -0,0 +1,576 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Surface\SurfaceInterface; + +class Path extends Shape +{ + // kindly borrowed from fabric.util.parsePath. + /* @see https://github.com/fabricjs/fabric.js/blob/master/src/util/path.js#L664 */ + const NUMBER_PATTERN = '([-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)\s*'; + const COMMA_PATTERN = '(?:\s+,?\s*|,\s*)?'; + const FLAG_PATTERN = '([01])'; + const ARC_REGEXP = '/' + . self::NUMBER_PATTERN + . self::COMMA_PATTERN + . self::NUMBER_PATTERN + . self::COMMA_PATTERN + . self::NUMBER_PATTERN + . self::COMMA_PATTERN + . self::FLAG_PATTERN + . self::COMMA_PATTERN + . self::FLAG_PATTERN + . self::COMMA_PATTERN + . self::NUMBER_PATTERN + . self::COMMA_PATTERN + . self::NUMBER_PATTERN + . '/'; + + static $commandLengths = array( + 'm' => 2, + 'l' => 2, + 'h' => 1, + 'v' => 1, + 'c' => 6, + 's' => 4, + 'q' => 4, + 't' => 2, + 'a' => 7, + ); + + static $repeatedCommands = array( + 'm' => 'l', + 'M' => 'L', + ); + + public static function parse(string $commandSequence): array + { + $commands = array(); + preg_match_all('/([MZLHVCSQTAmzlhvcsqta])([eE ,\-.\d]+)*/', $commandSequence, $commands, PREG_SET_ORDER); + + $path = array(); + foreach ($commands as $c) { + if (count($c) == 3) { + $commandLower = strtolower($c[1]); + + // arcs have special flags that apparently don't require spaces. + if ($commandLower === 'a' && preg_match_all(static::ARC_REGEXP, $c[2], $matches, PREG_PATTERN_ORDER)) { + $numberOfMatches = count($matches[0]); + for ($k = 0; $k < $numberOfMatches; ++$k) { + $path[] = [ + $c[1], + $matches[1][$k], + $matches[2][$k], + $matches[3][$k], + $matches[4][$k], + $matches[5][$k], + $matches[6][$k], + $matches[7][$k], + ]; + } + continue; + } + + $arguments = array(); + preg_match_all('/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/i', $c[2], $arguments, PREG_PATTERN_ORDER); + $item = $arguments[0]; + + if ( + isset(self::$commandLengths[$commandLower]) && + ($commandLength = self::$commandLengths[$commandLower]) && + count($item) > $commandLength + ) { + $repeatedCommand = isset(self::$repeatedCommands[$c[1]]) ? self::$repeatedCommands[$c[1]] : $c[1]; + $command = $c[1]; + + for ($k = 0, $klen = count($item); $k < $klen; $k += $commandLength) { + $_item = array_slice($item, $k, $k + $commandLength); + array_unshift($_item, $command); + $path[] = $_item; + + $command = $repeatedCommand; + } + } else { + array_unshift($item, $c[1]); + $path[] = $item; + } + + } else { + $item = array($c[1]); + + $path[] = $item; + } + } + + return $path; + } + + public function start($attributes) + { + if (!isset($attributes['d'])) { + $this->hasShape = false; + + return; + } + + $path = static::parse($attributes['d']); + $surface = $this->document->getSurface(); + + // From https://github.com/kangax/fabric.js/blob/master/src/shapes/path.class.js + $current = null; // current instruction + $previous = null; + $subpathStartX = 0; + $subpathStartY = 0; + $x = 0; // current x + $y = 0; // current y + $controlX = 0; // current control point x + $controlY = 0; // current control point y + $tempX = null; + $tempY = null; + $tempControlX = null; + $tempControlY = null; + $l = 0; //-((this.width / 2) + $this.pathOffset.x), + $t = 0; //-((this.height / 2) + $this.pathOffset.y), + + foreach ($path as $current) { + switch ($current[0]) { // first letter + case 'l': // lineto, relative + $x += $current[1]; + $y += $current[2]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'L': // lineto, absolute + $x = $current[1]; + $y = $current[2]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'h': // horizontal lineto, relative + $x += $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'H': // horizontal lineto, absolute + $x = $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'v': // vertical lineto, relative + $y += $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'V': // verical lineto, absolute + $y = $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'm': // moveTo, relative + $x += $current[1]; + $y += $current[2]; + $subpathStartX = $x; + $subpathStartY = $y; + $surface->moveTo($x + $l, $y + $t); + break; + + case 'M': // moveTo, absolute + $x = $current[1]; + $y = $current[2]; + $subpathStartX = $x; + $subpathStartY = $y; + $surface->moveTo($x + $l, $y + $t); + break; + + case 'c': // bezierCurveTo, relative + $tempX = $x + $current[5]; + $tempY = $y + $current[6]; + $controlX = $x + $current[3]; + $controlY = $y + $current[4]; + $surface->bezierCurveTo( + $x + $current[1] + $l, // x1 + $y + $current[2] + $t, // y1 + $controlX + $l, // x2 + $controlY + $t, // y2 + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'C': // bezierCurveTo, absolute + $x = $current[5]; + $y = $current[6]; + $controlX = $current[3]; + $controlY = $current[4]; + $surface->bezierCurveTo( + $current[1] + $l, + $current[2] + $t, + $controlX + $l, + $controlY + $t, + $x + $l, + $y + $t + ); + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + $tempX = $x + $current[3]; + $tempY = $y + $current[4]; + + if (!preg_match('/[CcSs]/', $previous[0])) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + $controlX = $x; + $controlY = $y; + } else { + // calculate reflection of previous control points + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } + + $surface->bezierCurveTo( + $controlX + $l, + $controlY + $t, + $x + $current[1] + $l, + $y + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + $controlX = $x + $current[1]; + $controlY = $y + $current[2]; + + $x = $tempX; + $y = $tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + $tempX = $current[3]; + $tempY = $current[4]; + + if (!preg_match('/[CcSs]/', $previous[0])) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + $controlX = $x; + $controlY = $y; + } else { + // calculate reflection of previous control points + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } + + $surface->bezierCurveTo( + $controlX + $l, + $controlY + $t, + $current[1] + $l, + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + $controlX = $current[1]; + $controlY = $current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + $tempX = $x + $current[3]; + $tempY = $y + $current[4]; + + $controlX = $x + $current[1]; + $controlY = $y + $current[2]; + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + $tempX = $current[3]; + $tempY = $current[4]; + + $surface->quadraticCurveTo( + $current[1] + $l, + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + $controlX = $current[1]; + $controlY = $current[2]; + break; + + case 't': // shorthand quadraticCurveTo, relative + + // transform to absolute x,y + $tempX = $x + $current[1]; + $tempY = $y + $current[2]; + + // calculate reflection of previous control points + if (preg_match('/[QqT]/', $previous[0])) { + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } elseif ($previous[0] === 't') { + $controlX = 2 * $x - $tempControlX; + $controlY = 2 * $y - $tempControlY; + } else { + $controlX = $x; + $controlY = $y; + } + + $tempControlX = $controlX; + $tempControlY = $controlY; + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'T': + $tempX = $current[1]; + $tempY = $current[2]; + + // calculate reflection of previous control points + if (preg_match('/[QqTt]/', $previous[0])) { + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } else { + $controlX = $x; + $controlY = $y; + } + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'a': + $this->drawArc( + $surface, + $x + $l, + $y + $t, + array( + $current[1], + $current[2], + $current[3], + $current[4], + $current[5], + $current[6] + $x + $l, + $current[7] + $y + $t + ) + ); + $x += $current[6]; + $y += $current[7]; + break; + + case 'A': + // TODO: optimize this + $this->drawArc( + $surface, + $x + $l, + $y + $t, + array( + $current[1], + $current[2], + $current[3], + $current[4], + $current[5], + $current[6] + $l, + $current[7] + $t + ) + ); + $x = $current[6]; + $y = $current[7]; + break; + + case 'z': + case 'Z': + $x = $subpathStartX; + $y = $subpathStartY; + $surface->closePath(); + break; + } + $previous = $current; + } + } + + function drawArc(SurfaceInterface $surface, $fx, $fy, $coords) + { + $rx = $coords[0]; + $ry = $coords[1]; + $rot = $coords[2]; + $large = $coords[3]; + $sweep = $coords[4]; + $tx = $coords[5]; + $ty = $coords[6]; + $segs = array( + array(), + array(), + array(), + array(), + ); + + $toX = $tx - $fx; + $toY = $ty - $fy; + + if ($toX + $toY === 0) { + return; + } + + $segsNorm = $this->arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rot); + + for ($i = 0, $len = count($segsNorm); $i < $len; $i++) { + $segs[$i][0] = $segsNorm[$i][0] + $fx; + $segs[$i][1] = $segsNorm[$i][1] + $fy; + $segs[$i][2] = $segsNorm[$i][2] + $fx; + $segs[$i][3] = $segsNorm[$i][3] + $fy; + $segs[$i][4] = $segsNorm[$i][4] + $fx; + $segs[$i][5] = $segsNorm[$i][5] + $fy; + + call_user_func_array(array($surface, "bezierCurveTo"), $segs[$i]); + } + } + + function arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rotateX) + { + $th = $rotateX * M_PI / 180; + $sinTh = sin($th); + $cosTh = cos($th); + $fromX = 0; + $fromY = 0; + + $rx = abs($rx); + $ry = abs($ry); + + $px = -$cosTh * $toX * 0.5 - $sinTh * $toY * 0.5; + $py = -$cosTh * $toY * 0.5 + $sinTh * $toX * 0.5; + $rx2 = $rx * $rx; + $ry2 = $ry * $ry; + $py2 = $py * $py; + $px2 = $px * $px; + $pl = $rx2 * $ry2 - $rx2 * $py2 - $ry2 * $px2; + $root = 0; + + if ($pl < 0) { + $s = sqrt(1 - $pl / ($rx2 * $ry2)); + $rx *= $s; + $ry *= $s; + } else { + $root = ($large == $sweep ? -1.0 : 1.0) * sqrt($pl / ($rx2 * $py2 + $ry2 * $px2)); + } + + $cx = $root * $rx * $py / $ry; + $cy = -$root * $ry * $px / $rx; + $cx1 = $cosTh * $cx - $sinTh * $cy + $toX * 0.5; + $cy1 = $sinTh * $cx + $cosTh * $cy + $toY * 0.5; + $mTheta = $this->calcVectorAngle(1, 0, ($px - $cx) / $rx, ($py - $cy) / $ry); + $dtheta = $this->calcVectorAngle(($px - $cx) / $rx, ($py - $cy) / $ry, (-$px - $cx) / $rx, (-$py - $cy) / $ry); + + if ($sweep == 0 && $dtheta > 0) { + $dtheta -= 2 * M_PI; + } else { + if ($sweep == 1 && $dtheta < 0) { + $dtheta += 2 * M_PI; + } + } + + // $Convert $into $cubic $bezier $segments <= 90deg + $segments = ceil(abs($dtheta / M_PI * 2)); + $result = array(); + $mDelta = $dtheta / $segments; + $mT = 8 / 3 * sin($mDelta / 4) * sin($mDelta / 4) / sin($mDelta / 2); + $th3 = $mTheta + $mDelta; + + for ($i = 0; $i < $segments; $i++) { + $result[$i] = $this->segmentToBezier( + $mTheta, + $th3, + $cosTh, + $sinTh, + $rx, + $ry, + $cx1, + $cy1, + $mT, + $fromX, + $fromY + ); + $fromX = $result[$i][4]; + $fromY = $result[$i][5]; + $mTheta = $th3; + $th3 += $mDelta; + } + + return $result; + } + + function segmentToBezier($th2, $th3, $cosTh, $sinTh, $rx, $ry, $cx1, $cy1, $mT, $fromX, $fromY) + { + $costh2 = cos($th2); + $sinth2 = sin($th2); + $costh3 = cos($th3); + $sinth3 = sin($th3); + $toX = $cosTh * $rx * $costh3 - $sinTh * $ry * $sinth3 + $cx1; + $toY = $sinTh * $rx * $costh3 + $cosTh * $ry * $sinth3 + $cy1; + $cp1X = $fromX + $mT * (-$cosTh * $rx * $sinth2 - $sinTh * $ry * $costh2); + $cp1Y = $fromY + $mT * (-$sinTh * $rx * $sinth2 + $cosTh * $ry * $costh2); + $cp2X = $toX + $mT * ($cosTh * $rx * $sinth3 + $sinTh * $ry * $costh3); + $cp2Y = $toY + $mT * ($sinTh * $rx * $sinth3 - $cosTh * $ry * $costh3); + + return array( + $cp1X, + $cp1Y, + $cp2X, + $cp2Y, + $toX, + $toY + ); + } + + function calcVectorAngle($ux, $uy, $vx, $vy) + { + $ta = atan2($uy, $ux); + $tb = atan2($vy, $vx); + if ($tb >= $ta) { + return $tb - $ta; + } else { + return 2 * M_PI - ($ta - $tb); + } + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Polygon.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Polygon.php new file mode 100644 index 0000000..e7ca92a --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Polygon.php @@ -0,0 +1,42 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class Polygon extends Shape +{ + public function start($attributes) + { + $tmp = array(); + preg_match_all('/([\-]*[0-9\.]+)/', $attributes['points'], $tmp, PREG_PATTERN_ORDER); + + $points = $tmp[0]; + $count = count($points); + + if ($count < 4) { + // nothing to draw + return; + } + + $surface = $this->document->getSurface(); + list($x, $y) = $points; + $surface->moveTo($x, $y); + + for ($i = 2; $i < $count; $i += 2) { + if ($i + 1 === $count) { + // invalid trailing point + continue; + } + $x = $points[$i]; + $y = $points[$i + 1]; + $surface->lineTo($x, $y); + } + + $surface->closePath(); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Polyline.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Polyline.php new file mode 100644 index 0000000..45e2131 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Polyline.php @@ -0,0 +1,40 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class Polyline extends Shape +{ + public function start($attributes) + { + $tmp = array(); + preg_match_all('/([\-]*[0-9\.]+)/', $attributes['points'], $tmp, PREG_PATTERN_ORDER); + + $points = $tmp[0]; + $count = count($points); + + if ($count < 4) { + // nothing to draw + return; + } + + $surface = $this->document->getSurface(); + list($x, $y) = $points; + $surface->moveTo($x, $y); + + for ($i = 2; $i < $count; $i += 2) { + if ($i + 1 === $count) { + // invalid trailing point + continue; + } + $x = $points[$i]; + $y = $points[$i + 1]; + $surface->lineTo($x, $y); + } + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php new file mode 100644 index 0000000..a9de62f --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php @@ -0,0 +1,17 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class RadialGradient extends AbstractTag +{ + public function start($attributes) + { + + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Rect.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Rect.php new file mode 100644 index 0000000..b5f3f77 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Rect.php @@ -0,0 +1,50 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Rect extends Shape +{ + protected $x = 0; + protected $y = 0; + protected $width = 0; + protected $height = 0; + protected $rx = 0; + protected $ry = 0; + + public function start($attributes) + { + $width = $this->document->getWidth(); + $height = $this->document->getHeight(); + + if (isset($attributes['x'])) { + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $this->convertSize($attributes['y'], $height); + } + + if (isset($attributes['width'])) { + $this->width = $this->convertSize($attributes['width'], $width); + } + if (isset($attributes['height'])) { + $this->height = $this->convertSize($attributes['height'], $height); + } + + if (isset($attributes['rx'])) { + $this->rx = $attributes['rx']; + } + if (isset($attributes['ry'])) { + $this->ry = $attributes['ry']; + } + + $this->document->getSurface()->rect($this->x, $this->y, $this->width, $this->height, $this->rx, $this->ry); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Shape.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Shape.php new file mode 100644 index 0000000..767e81d --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Shape.php @@ -0,0 +1,63 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Shape extends AbstractTag +{ + protected function before($attributes) + { + $surface = $this->document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $surface = $this->document->getSurface(); + + if ($this->hasShape) { + $style = $surface->getStyle(); + + $fill = $style->fill && is_array($style->fill); + $stroke = $style->stroke && is_array($style->stroke); + + if ($fill) { + if ($stroke) { + $surface->fillStroke(false); + } else { +// if (is_string($style->fill)) { +// /** @var LinearGradient|RadialGradient $gradient */ +// $gradient = $this->getDocument()->getDef($style->fill); +// +// var_dump($gradient->getStops()); +// } + + $surface->fill(); + } + } + elseif ($stroke) { + $surface->stroke(false); + } + else { + $surface->endPath(); + } + } + + $surface->restore(); + } +}
\ No newline at end of file diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Stop.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Stop.php new file mode 100644 index 0000000..22c9a98 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Stop.php @@ -0,0 +1,17 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class Stop extends AbstractTag +{ + public function start($attributes) + { + + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php new file mode 100644 index 0000000..309de01 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php @@ -0,0 +1,27 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Sabberworm\CSS; + +class StyleTag extends AbstractTag +{ + protected $text = ""; + + public function end() + { + $parser = new CSS\Parser($this->text); + $this->document->appendStyleSheet($parser->parse()); + } + + public function appendText($text) + { + $this->text .= $text; + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/Text.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/Text.php new file mode 100644 index 0000000..80e08a6 --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/Text.php @@ -0,0 +1,72 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +use Svg\Style; + +class Text extends Shape +{ + protected $x = 0; + protected $y = 0; + protected $text = ""; + + public function start($attributes) + { + $height = $this->document->getHeight(); + $this->y = $height; + + if (isset($attributes['x'])) { + $width = $this->document->getWidth(); + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $height - $this->convertSize($attributes['y'], $height); + } + + $this->document->getSurface()->transform(1, 0, 0, -1, 0, $height); + } + + public function end() + { + $surface = $this->document->getSurface(); + $x = $this->x; + $y = $this->y; + $style = $surface->getStyle(); + $surface->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + + switch ($style->textAnchor) { + case "middle": + $width = $surface->measureText($this->text); + $x -= $width / 2; + break; + + case "end": + $width = $surface->measureText($this->text); + $x -= $width; + break; + } + + $surface->fillText($this->getText(), $x, $y); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } + + public function appendText($text) + { + $this->text .= $text; + } + + public function getText() + { + return trim($this->text); + } +} diff --git a/vendor/phenx/php-svg-lib/src/Svg/Tag/UseTag.php b/vendor/phenx/php-svg-lib/src/Svg/Tag/UseTag.php new file mode 100644 index 0000000..c5f00ea --- /dev/null +++ b/vendor/phenx/php-svg-lib/src/Svg/Tag/UseTag.php @@ -0,0 +1,102 @@ +<?php +/** + * @package php-svg-lib + * @link http://github.com/PhenX/php-svg-lib + * @author Fabien Ménager <fabien.menager@gmail.com> + * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html + */ + +namespace Svg\Tag; + +class UseTag extends AbstractTag +{ + protected $x = 0; + protected $y = 0; + protected $width; + protected $height; + + /** @var AbstractTag */ + protected $reference; + + protected function before($attributes) + { + if (isset($attributes['x'])) { + $this->x = $attributes['x']; + } + if (isset($attributes['y'])) { + $this->y = $attributes['y']; + } + + if (isset($attributes['width'])) { + $this->width = $attributes['width']; + } + if (isset($attributes['height'])) { + $this->height = $attributes['height']; + } + + parent::before($attributes); + + $document = $this->getDocument(); + + $link = $attributes["href"] ?? $attributes["xlink:href"]; + $this->reference = $document->getDef($link); + + if ($this->reference) { + $this->reference->before($attributes); + } + + $surface = $document->getSurface(); + $surface->save(); + + $surface->translate($this->x, $this->y); + } + + protected function after() { + parent::after(); + + if ($this->reference) { + $this->reference->after(); + } + + $this->getDocument()->getSurface()->restore(); + } + + public function handle($attributes) + { + parent::handle($attributes); + + if (!$this->reference) { + return; + } + + $mergedAttributes = $this->reference->attributes; + $attributesToNotMerge = ['x', 'y', 'width', 'height']; + foreach ($attributes as $attrKey => $attrVal) { + if (!in_array($attrKey, $attributesToNotMerge) && !isset($mergedAttributes[$attrKey])) { + $mergedAttributes[$attrKey] = $attrVal; + } + } + + $this->reference->handle($mergedAttributes); + + foreach ($this->reference->children as $_child) { + $_attributes = array_merge($_child->attributes, $mergedAttributes); + $_child->handle($_attributes); + } + } + + public function handleEnd() + { + parent::handleEnd(); + + if (!$this->reference) { + return; + } + + $this->reference->handleEnd(); + + foreach ($this->reference->children as $_child) { + $_child->handleEnd(); + } + } +} |