* @package cleversvg * @subpackage document */ class csDocument { /** * Document attributes container * @var array */ protected $attributes = array(); /** * Definitions and elements container * @var csElementsContainer */ protected $container; /** * Scripts container * @var array */ protected $scripts = array(); /** * Embeded styles container * @var array */ protected $embed_styles = array(); /** * External styles references container * @var array */ protected $external_styles = array(); /** * Document description * @var string */ protected $description = NULL; /** * DOMDocument instance * @var DOMDocument */ protected $dom_document = NULL; /** * Max depth * @var int */ protected $maxdepth = 0; /** * Document title * @var string */ protected $title = 'Untitled SVG Document'; /** * Embeded SVG flag * @var boolean */ public static $embedded = false; /** * Strict mode flag * @var boolean */ protected $strict_mode = false; /** * Class constructor * * @param mixed $width Width * @param mixed $height Height * @param string $title Document title * @param array $attrs Attributes array */ public function __construct($width=null, $height=null, $title=null, $attrs = array()) { $this->setWidth($width); $this->setHeight($height); $this->setTitle($title); $this->container = new csElementsContainer(); $this->attributes = array_merge($attrs, $this->attributes); $this->initDefaults(); } /** * Adds a SVG shape or group to current SVG Document at a certain depth * * @param mixed $element SVG element to add * @throws csException */ public function addElement($element) { return $this->container->addElement($element); } /** * Create an svg element and returns its instance * * @param string $name * @return csBaseElement */ public function createElement($name) { return $this->container->createElement($name); } /** * Drop an element from document by its DOM id * * @param string $id * @throws csException */ public function dropElementById($id) { return $this->container->dropElementById($id); } /** * Retrieve an element instance by its DOM id * * @param string $id * @return csBaseElement * @throw csException */ public function getElementById($id) { return $this->container->getElementById($id); } /** * Returns an array containing SVG Document Elements * * @return array */ protected function getElements() { return $this->container->getElements(); } /** * Checks if SVG output should be in embed mode * * @return boolean */ public static function getIsEmbedded() { return self::$embedded; } /** * Gets the namespace prefix if this is an embedded document * * @return string */ public static function getNodeNS() { $prefix = ''; if (self::$embedded === true) { $prefix = 'svg:'; } return $prefix; } /** * Adds a script tag in global document array of scripts * * @param string $content * @param string $content_type */ public function addScript($content, $type = 'text/ecmascript') { $this->scripts[] = array($content, $type); } /** * Adds a style tag in global document array of styles * * @param string $content Style declaration content * @param string $content_type * @param mixed $media * @param mixed $title */ public function addStyleDeclaration($content, $type = 'text/css', $media = null, $title = null) { $this->embed_styles[] = array('content' => $content, 'type' => $type, 'media' => $media, 'title' => $title); } /** * Adds a linked stylesheet global document * * @param string $href * @param string $content_type * @param mixed $media * @param mixed $title */ public function addStylesheetLink($href, $type = 'text/css', $alternate = 'no', $media = null, $title = null) { $this->external_styles[] = array('href' => $href, 'type' => $type, 'alternate' => $alternate, 'media' => $media, 'title' => $title); } /** * Adds a definition to current SVG Document * * @param mixed $definition SVG definition to add * @param string $id Override element DOM id * TODO: implement definitions and references */ public function addAsDefinition($definition, $id = null) { return $this->container->addAsDefinition($definition, $id); } /** * Returns an array containing SVG Document definitions * * @return array */ protected function getDefinitions() { return $this->container->getDefinitions(); } /** * Returns an array containing used definitions for current document * * @return array */ protected function getUsedDefinitions() { return $this->container->getUsedDefinitions(); } /** * Returns a defined element alias (a <use/> tag) * * @param string $id * @return array * @throws csException */ protected function getDefinition($id) { return $this->container->getDefinition($id); } /** * Returns DOMDocument instance for SVG current document * * @param boolean $new Shall we force creating a new empty DOMDocument ? * @return DOMDocument */ public function getDomDocument($new = false) { if (!$new && $this->dom_document instanceof DOMDocument) { return $this->dom_document; } else { // DOMDocument creation $imp = new DOMImplementation; switch ($this->attributes['version']) { default: case '1.1': $doctype = $imp->createDocumentType("svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"); break; case '1.0': $doctype = $imp->createDocumentType("svg", "-//W3C//DTD SVG 1.0//EN", "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"); break; } if (true === self::$embedded) { $dom = new DOMDocument('1.0', 'UTF-8'); } else { $dom = $imp->createDocument(null, null, $doctype); } $dom->encoding = 'UTF-8'; $dom->standalone = false; $dom->formatOutput = true; $this->dom_document = $dom; return $dom; } } /** * Uses a definition in current SVG Document * * @param mixed $element * @param array $attrs More atributes to add to <use/> tag */ public function useDefinition($id, $attrs = array()) { return $this->container->useDefinition($id, $attrs); } /** * Swap respective depths of two elements. Both elements are retrieved by * their DOM id. * * @param string $id1 * @param string $id2 * @throws csException */ public function swapDepths($id1, $id2) { return $this->container->swapDepths($id1, $id2); } /** * Checks if a DOM id already exists in current SVG Document * * @param string $id * @return boolean */ public function hasId($id) { return $this->container->hasId($id); } /** * Initialize object with default required values if they're not present in * the attributes array * */ protected function initDefaults() { if (!array_key_exists('xmlns', $this->attributes)) { $this->attributes['xmlns'] = 'http://www.w3.org/2000/svg'; } if (!array_key_exists('version', $this->attributes)) { $this->attributes['version'] = '1.1'; } if (!array_key_exists('xmlns:xlink', $this->attributes)) { $this->attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'; } } /** * Sets a description describing the SVG Document * * @param string $description */ public function setDescription($description) { $this->description = $description; } /** * Sets if the SVG document will be embedded or not * * @param boolean $flag */ public function setEmbedded($flag) { self::$embedded = (boolean) $flag; } /** * Sets if the SVG document will be stricly validated * * @param boolean $flag */ public function setStrictMode($flag) { $this->strict_mode = (boolean) $flag; } /** * Sets if the SVG document title * * @param string $title */ public function setTitle($title) { $this->title = trim($title) != '' ? $title : 'Untitled SVG Document'; } /** * Standard XML attribute for identifying an XML namespace. * * @param string $xmlns XML namespace url */ public function setXmlns($xmlns = 'http://www.w3.org/2000/svg') { $this->attributes['xmlns'] = $xmlns; } /** * The x-axis coordinate of one corner of the rectangular region into which an * embedded 'svg' element is placed. * */ public function setX($x) { $this->attributes['x'] = (string)$x; } /** * The y-axis coordinate of one corner of the rectangular region into which an * embedded 'svg' element is placed. * */ public function setY($y) { $this->attributes['y'] = (string)$y; } /** * For outermost 'svg' elements, the intrinsic width of the SVG document * fragment. For embedded 'svg' elements, the width of the rectangular * region into which the 'svg' element is placed. * */ public function setWidth($width) { $this->attributes['width'] = (string)$width; } /** * For outermost 'svg' elements, the intrinsic height of the SVG document * fragment. For embedded 'svg' elements, the height of the rectangular * region into which the 'svg' element is placed. * */ public function setHeight($height) { $this->attributes['height'] = (string)$height; } /** * Indicates the SVG language version to which this document fragment * conforms. In SVG 1.0, this attribute was fixed to the value "1.0". For * SVG 1.1, the attribute should have the value "1.1". * */ public function setVersion($version = '1.1') { $this->attributes['version'] = (string)$version; } /** * This attribute provides a convenient way to design SVG documents to * scale-to-fit into an arbitrary viewport. * */ public function setViewBox($x, $y, $width, $height) { $this->attributes['viewBox'] = sprintf('%d %d %d %d', $x, $y, $width, $height); } /** * Describes the minimum SVG language profile that the author believes is * necessary to correctly render the content. This can be considered metadata. * * @param string $base_profile */ public function setBaseProfile($base_profile) { $this->attributes['baseProfile'] = $base_profile; } /** * Gets SVG XML rendering output. Never forget to send image/svg+xml headers * before outputing. * * @param boolean $embedded Is SVG Document embedded ? * @return string */ public function toXML($embedded = null) { if (!is_null($embedded) && is_bool($embedded)) { $this->setEmbedded($embedded); } $xml_prepend = ''; $dom = $this->getDomDocument(true); // External stylesheets $this->processExternalStyleSheets(); // Root node creation $this->processRootNode(); $this->processMetas(); $this->processDefinitions(); $this->processScripts(); $this->processEmbeddedStyles(); $this->processElements(); $this->processUsedDefinitions(); // Document normalization $dom->normalizeDocument(); // Validation if strict mode if (true === $this->strict_mode) { $this->validate(); } // Output if (self::$embedded === true) { return $dom->saveXml($dom->documentElement, LIBXML_NOXMLDECL); } else { return $dom->saveXML(); } } /** * Process definitions if any * */ protected function processDefinitions() { return $this->container->processDefinitions($this->getDomDocument()); } /** * Process Document elements * */ protected function processElements() { return $this->container->processElements($this->getDomDocument()); } /** * Process embedded styles, if any * */ protected function processEmbeddedStyles() { $dom = $this->getDomDocument(); if (count($this->embed_styles) > 0) { // Styles declaration foreach ($this->embed_styles as $style) { $style_node = $dom->createElement(self::getNodeNS().'style'); $style_node->setAttribute('type', $style['type']); $style_node->setAttribute('media', $style['media']); $style_node->setAttribute('title', $style['title']); $style_content = $dom->createCDATASection($style['content']); $style_node->appendChild($style_content); $dom->documentElement->appendChild($style_node); } } } /** * Process externals stylesheets, if any * */ protected function processExternalStyleSheets() { $dom = $this->getDomDocument(); if (count($this->external_styles) > 0) { foreach ($this->external_styles as $stylesheet) { $style_declaration = $dom->createProcessingInstruction( 'xml-stylesheet', sprintf('href="%s" type="%s"', $stylesheet['href'], $stylesheet['type'])); $dom->appendChild($style_declaration); } } } /** * Process meta description * */ protected function processMetas() { $dom = $this->getDomDocument(); // Title node if (!is_null($this->title)) { $title_node = $dom->createElement(self::getNodeNS().'title', strip_tags($this->title)); $dom->documentElement->appendChild($title_node); } // Description node if (!is_null($this->description)) { $desc_node = $dom->createElement(self::getNodeNS().'desc', strip_tags($this->description)); $dom->documentElement->appendChild($desc_node); } } /** * Process root node * */ protected function processRootNode() { $dom = $this->getDomDocument(); $root = $this->dom_document->createElement(self::getNodeNS().'svg'); $dom->appendChild($root); foreach ($this->attributes as $attr_name => $attr_value) { if (!is_null($attr_value)) { $dom->documentElement->setAttribute($attr_name, $attr_value); } } } /** * Process used definitions, if any * */ protected function processUsedDefinitions() { return $this->container->processUsedDefinitions($this->getDomDocument()); } /** * Process scripts, if any * */ protected function processScripts() { $dom = $this->getDomDocument(); if (count($this->scripts) > 0) { // Scripts declaration foreach ($this->scripts as $script) { $script_node = $dom->createElement(self::getNodeNS().'script'); $script_node->setAttribute('type', $script[1]); $script_content = $dom->createCDATASection($script[0]); $script_node->appendChild($script_content); $dom->documentElement->appendChild($script_node); } } } /** * Validates document * * @throws csException */ public function validate() { try { $this->getDomDocument()->validate(); } catch (csException $e) { if (preg_match('/php_network_getaddresses/', $e->getMessage())) { throw new csException( 'An internet connection is required to validate document'); } throw new csException( 'SVG document validation failed: '.$e->getMessage()); } } }