meyfa/php-svg

View on GitHub
src/Reading/SVGReader.php

Summary

Maintainability
A
1 hr
Test Coverage
A
94%
<?php

namespace SVG\Reading;

use SimpleXMLElement;
use SVG\SVG;
use SVG\Nodes\SVGNode;
use SVG\Nodes\SVGNodeContainer;
use SVG\Utilities\SVGStyleParser;

/**
 * This class is used to read XML strings or files and turn them into instances
 * of SVG by parsing the document tree.
 *
 * In contrast to SVGWriter, a single instance can perform any number of reads.
 */
class SVGReader
{
    /**
     * Parses the given string as XML and turns it into an instance of SVG.
     * Returns null when parsing fails.
     *
     * @param string $string The XML string to parse.
     *
     * @return SVG|null An image object representing the parse result.
     */
    public function parseString(string $string): ?SVG
    {
        $xml = new SimpleXMLElement($string, LIBXML_PARSEHUGE);

        return $this->parseXML($xml);
    }

    /**
     * Parses the file at the given path/URL as XML and turns it into an
     * instance of SVG.
     *
     * The path can be on the local file system, or a URL on the network.
     * Returns null when parsing fails.
     *
     * @param string $filename The path or URL of the file to parse.
     *
     * @return SVG|null An image object representing the parse result.
     */
    public function parseFile(string $filename): ?SVG
    {
        $xml = simplexml_load_file($filename);
        return $this->parseXML($xml);
    }

    /**
     * Parses the given XML document into an instance of SVG.
     * Returns null when parsing fails.
     *
     * @param SimpleXMLElement $xml The root node of the SVG document to parse.
     *
     * @return SVG|null An image object representing the parse result.
     */
    public function parseXML(SimpleXMLElement $xml): ?SVG
    {
        $name = $xml->getName();
        if ($name !== 'svg') {
            return null;
        }

        $img = new SVG();
        $doc = $img->getDocument();

        $namespaces = $xml->getNamespaces(true);
        $doc->setNamespaces($namespaces);

        $nsKeys = array_keys($namespaces);
        if (!in_array('', $nsKeys, true) && !in_array(null, $nsKeys, true)) {
            $nsKeys[] = '';
        }

        $this->applyAttributes($doc, $xml, $nsKeys);
        $this->applyStyles($doc, $xml);
        $this->addChildren($doc, $xml, $nsKeys);

        return $img;
    }

    /**
     * Iterates over all XML attributes and applies them to the given node.
     *
     * Since styles in SVG can also be expressed with attributes, this method
     * checks the name of each attribute and, if it matches that of a style,
     * applies it as a style instead. The actual 'style' attribute is ignored.
     *
     * @see SVGReader::$styleAttributes The attributes considered styles.
     *
     * @param SVGNode           $node       The node to apply the attributes to.
     * @param SimpleXMLElement  $xml        The attribute source.
     * @param string[]          $namespaces Array of allowed namespace prefixes.
     *
     * @return void
     */
    private function applyAttributes(SVGNode $node, SimpleXMLElement $xml, array $namespaces): void
    {
        foreach ($namespaces as $ns) {
            foreach ($xml->attributes($ns, true) as $key => $value) {
                if ($key === 'style') {
                    continue;
                }
                if (AttributeRegistry::isStyle($key)) {
                    $convertedValue = AttributeRegistry::convertStyleAttribute($key, $value);
                    $node->setStyle($key, $convertedValue);
                    continue;
                }
                if (!empty($ns) && $ns !== 'svg') {
                    $key = $ns . ':' . $key;
                }
                $node->setAttribute($key, $value);
            }
        }
    }

    /**
     * Parses the 'style' attribute (if it exists) and applies all styles to the
     * given node.
     *
     * This method does NOT handle styles expressed as attributes (stroke="").
     * @see SVGReader::applyAttributes() For styles expressed as attributes.
     *
     * @param SVGNode           $node The node to apply the styles to.
     * @param SimpleXMLElement  $xml  The attribute source.
     *
     * @return void
     */
    private function applyStyles(SVGNode $node, SimpleXMLElement $xml): void
    {
        if (!isset($xml['style'])) {
            return;
        }

        $styles = SVGStyleParser::parseStyles($xml['style']);
        foreach ($styles as $key => $value) {
            $node->setStyle($key, $value);
        }
    }

    /**
     * Iterates over all children, parses them into library class instances,
     * and adds them to the given node container.
     *
     * @param SVGNodeContainer  $node       The node to add the children to.
     * @param SimpleXMLElement  $xml        The XML node containing the children.
     * @param string[]          $namespaces Array of allowed namespace prefixes.
     *
     * @return void
     */
    private function addChildren(SVGNodeContainer $node, SimpleXMLElement $xml, array $namespaces): void
    {
        foreach ($namespaces as $ns) {
            foreach ($xml->children($ns, true) as $child) {
                $node->addChild($this->parseNode($ns, $child, $namespaces));
            }
        }
    }

    /**
     * Parses the given XML element into an instance of a SVGNode subclass.
     * Unknown node types use a generic implementation.
     *
     * @param string            $ns         The tag name namespace prefix.
     * @param SimpleXMLElement  $xml        The XML element to parse.
     * @param string[]          $namespaces Array of allowed namespace prefixes.
     *
     * @return SVGNode The parsed node.
     *
     * @SuppressWarnings(PHPMD.ErrorControlOperator)
     */
    private function parseNode(string $ns, SimpleXMLElement $xml, array $namespaces): SVGNode
    {
        $tagName = $xml->getName();
        if (!empty($ns) && $ns !== 'svg') {
            $tagName = $ns . ':' . $tagName;
        }
        $node = NodeRegistry::create($tagName);

        // obtain array of namespaces that are declared directly on this node
        $extraNamespaces = @$xml->getDocNamespaces(false, false);
        if (!empty($extraNamespaces)) {
            $namespaces = array_unique(array_merge($namespaces, array_keys($extraNamespaces)));
            $node->setNamespaces($extraNamespaces);
        }

        $this->applyAttributes($node, $xml, $namespaces);
        $this->applyStyles($node, $xml);
        $node->setValue($xml);

        if ($node instanceof SVGNodeContainer) {
            $this->addChildren($node, $xml, $namespaces);
        }

        return $node;
    }
}