1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15 16use DOMAttr; 17use DOMElement; 18use DOMNode; 19 20 21class SvgDocument extends XmlDocument 22{ 23 24 25 const CANONICAL = "svg"; 26 27 /** 28 * Namespace (used to query with xpath only the svg node) 29 */ 30 const SVG_NAMESPACE_PREFIX = "svg"; 31 const CONF_SVG_OPTIMIZATION_ENABLE = "svgOptimizationEnable"; 32 33 /** 34 * Optimization Configuration 35 */ 36 const CONF_OPTIMIZATION_NAMESPACES_TO_KEEP = "svgOptimizationNamespacesToKeep"; 37 const CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE = "svgOptimizationAttributesToDelete"; 38 const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY = "svgOptimizationElementsToDeleteIfEmpty"; 39 const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE = "svgOptimizationElementsToDelete"; 40 41 /** 42 * The namespace of the editors 43 * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1841 44 */ 45 const EDITOR_NAMESPACE = [ 46 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 47 'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd', 48 'http://www.inkscape.org/namespaces/inkscape', 49 'http://www.bohemiancoding.com/sketch/ns', 50 'http://ns.adobe.com/AdobeIllustrator/10.0/', 51 'http://ns.adobe.com/Graphs/1.0/', 52 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', 53 'http://ns.adobe.com/Variables/1.0/', 54 'http://ns.adobe.com/SaveForWeb/1.0/', 55 'http://ns.adobe.com/Extensibility/1.0/', 56 'http://ns.adobe.com/Flows/1.0/', 57 'http://ns.adobe.com/ImageReplacement/1.0/', 58 'http://ns.adobe.com/GenericCustomNamespace/1.0/', 59 'http://ns.adobe.com/XPath/1.0/', 60 'http://schemas.microsoft.com/visio/2003/SVGExtensions/', 61 'http://taptrix.com/vectorillustrator/svg_extensions', 62 'http://www.figma.com/figma/ns', 63 'http://purl.org/dc/elements/1.1/', 64 'http://creativecommons.org/ns#', 65 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 66 'http://www.serif.com/', 67 'http://www.vector.evaxdesign.sk', 68 ]; 69 70 /** 71 * Default SVG values 72 * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1579 73 * The key are exact (not lowercase) to be able to look them up 74 * for optimization 75 */ 76 const SVG_DEFAULT_ATTRIBUTES_VALUE = array( 77 "x" => '0', 78 "y" => '0', 79 "width" => '100%', 80 "height" => '100%', 81 "preserveAspectRatio" => 'xMidYMid meet', 82 "zoomAndPan" => 'magnify', 83 "version" => '1.1', 84 "baseProfile" => 'none', 85 "contentScriptType" => 'application/ecmascript', 86 "contentStyleType" => 'text/css', 87 ); 88 const CONF_PRESERVE_ASPECT_RATIO_DEFAULT = "svgPreserveAspectRatioDefault"; 89 const SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg"; 90 91 92 /** 93 * Type of svg 94 * * Icon and tile have the same characteristic (ie viewbox = 0 0 A A) and the color can be set) 95 * * An illustration does not have rectangle shape and the color is not set 96 */ 97 const ICON_TYPE = "icon"; 98 const TILE_TYPE = "tile"; 99 const ILLUSTRATION_TYPE = "illustration"; 100 101 /** 102 * There is only two type of svg icon / tile 103 * * fill color is on the surface (known also as Solid) 104 * * stroke, the color is on the path (known as Outline 105 */ 106 const COLOR_TYPE_FILL_SOLID = "fill"; 107 const COLOR_TYPE_STROKE_OUTLINE = self::STROKE_ATTRIBUTE; 108 const DEFAULT_ICON_WIDTH = "24"; 109 110 const CURRENT_COLOR = "currentColor"; 111 const VIEW_BOX = "viewBox"; 112 const PRESERVE_ATTRIBUTE = "preserve"; 113 const STROKE_ATTRIBUTE = "stroke"; 114 115 /** 116 * @var string - a name identifier that is added in the SVG 117 */ 118 private $name; 119 120 /** 121 * @var boolean do the svg should be optimized 122 */ 123 private $shouldBeOptimized; 124 /** 125 * @var Path 126 */ 127 private $path; 128 129 130 public function __construct($text) 131 { 132 parent::__construct($text); 133 $this->shouldBeOptimized = PluginUtility::getConfValue(self::CONF_SVG_OPTIMIZATION_ENABLE, 1); 134 135 } 136 137 /** 138 * @param Path $path 139 * @return SvgDocument 140 * @throws ExceptionCombo - if the file does not exist or is not valid 141 * 142 */ 143 public static function createSvgDocumentFromPath(Path $path): SvgDocument 144 { 145 if (!FileSystems::exists($path)) { 146 throw new ExceptionCombo("The path ($path) does not exist. A svg document cannot be created", self::CANONICAL); 147 } 148 $text = FileSystems::getContent($path); 149 $svg = new SvgDocument($text); 150 $svg->setName($path->getLastNameWithoutExtension()); 151 $svg->setPath($path); 152 return $svg; 153 } 154 155 /** 156 * @throws ExceptionCombo 157 */ 158 public static function createSvgDocumentFromMarkup($markup): SvgDocument 159 { 160 return new SvgDocument($markup); 161 } 162 163 private static function preserveStyle(TagAttributes $tagAttributes): bool 164 { 165 $preserve = $tagAttributes->getValue(self::PRESERVE_ATTRIBUTE); 166 if ($preserve !== null) { 167 if (strpos(strtolower($preserve), "style") !== false) { 168 return true; 169 } 170 } 171 return false; 172 } 173 174 /** 175 * @param TagAttributes|null $tagAttributes 176 * @return string 177 * 178 * TODO: What strange is that this is a XML document that is also an image 179 * This class should be merged with {@link ImageSvg} 180 * Because we use only {@link Image} function that are here not available because we loose the fact that this is an image 181 * For instance {@link Image::getCroppingDimensionsWithRatio()} 182 * @throws ExceptionCombo 183 */ 184 public function getXmlText(TagAttributes $tagAttributes = null): string 185 { 186 187 if ($tagAttributes === null) { 188 $localTagAttributes = TagAttributes::createEmpty(); 189 } else { 190 $localTagAttributes = TagAttributes::createFromTagAttributes($tagAttributes); 191 } 192 193 /** 194 * ViewBox should exist 195 */ 196 $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX); 197 if ($viewBox === "") { 198 $width = $this->getXmlDom()->documentElement->getAttribute("width"); 199 if ($width === "") { 200 LogUtility::msg("Svg processing stopped. Bad svg: We can't determine the width of the svg ($this) (The viewBox and the width does not exist) ", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 201 return parent::getXmlText(); 202 } 203 $height = $this->getXmlDom()->documentElement->getAttribute("height"); 204 if ($height === "") { 205 LogUtility::msg("Svg processing stopped. Bad svg: We can't determine the height of the svg ($this) (The viewBox and the height does not exist) ", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 206 return parent::getXmlText(); 207 } 208 $this->getXmlDom()->documentElement->setAttribute(self::VIEW_BOX, "0 0 $width $height"); 209 } 210 211 if ($this->shouldOptimize()) { 212 $this->optimize($localTagAttributes); 213 } 214 215 // Set the name (icon) attribute for test selection 216 if ($localTagAttributes->hasComponentAttribute("name")) { 217 $name = $localTagAttributes->getValueAndRemove("name"); 218 $this->setRootAttribute('data-name', $name); 219 } 220 221 // Handy variable 222 $documentElement = $this->getXmlDom()->documentElement; 223 224 // With requested 225 $requestedWidth = $localTagAttributes->getValueAndRemove(Dimension::WIDTH_KEY); 226 227 $svgUsageType = $localTagAttributes->getValue(TagAttributes::TYPE_KEY); 228 229 /** 230 * Svg Structure 231 * 232 * All attributes that are applied for all usage (output independent) 233 * and that depends only on the structure of the icon 234 * 235 * Why ? Because {@link \syntax_plugin_combo_pageimage} 236 * can be an icon or an illustrative image 237 * 238 */ 239 try { 240 $mediaWidth = $this->getMediaWidth(); 241 } catch (ExceptionCombo $e) { 242 LogUtility::msg("The media width of ($this) returns the following error ({$e->getMessage()}). The processing was stopped"); 243 return parent::getXmlText(); 244 } 245 try { 246 $mediaHeight = $this->getMediaHeight(); 247 } catch (ExceptionCombo $e) { 248 LogUtility::msg("The media height of ($this) returns the following error ({$e->getMessage()}). The processing was stopped"); 249 return parent::getXmlText(); 250 } 251 if ($mediaWidth !== null 252 && $mediaHeight !== null 253 && $mediaWidth == $mediaHeight 254 && $mediaWidth < 400) // 356 for logos telegram are the size of the twitter emoji but tile may be bigger ? 255 { 256 $svgStructureType = self::ICON_TYPE; 257 } else { 258 $svgStructureType = self::ILLUSTRATION_TYPE; 259 260 // some icon may be bigger 261 // in size than 400. example 1024 for ant-design:table-outlined 262 // https://github.com/ant-design/ant-design-icons/blob/master/packages/icons-svg/svg/outlined/table.svg 263 // or not squared 264 // if the usage is determined or the svg is in the icon directory, it just takes over. 265 if ($svgUsageType === self::ICON_TYPE || $this->isInIconDirectory()) { 266 $svgStructureType = self::ICON_TYPE; 267 } 268 269 } 270 271 /** 272 * Svg type 273 * The svg type is the svg usage 274 * How the svg should be shown (the usage) 275 * 276 * We need it to make the difference between an icon 277 * * in a paragraph (the width and height are the same) 278 * * as an illustration in a page image (the width and height may be not the same) 279 */ 280 if ($svgUsageType === null) { 281 switch ($svgStructureType) { 282 case self::ICON_TYPE: 283 $svgUsageType = self::ICON_TYPE; 284 break; 285 default: 286 $svgUsageType = self::ILLUSTRATION_TYPE; 287 break; 288 } 289 } 290 switch ($svgUsageType) { 291 case self::ICON_TYPE: 292 case self::TILE_TYPE: 293 /** 294 * Dimension 295 * 296 * Using a icon in the navbrand component of bootstrap 297 * require the set of width and height otherwise 298 * the svg has a calculated width of null 299 * and the bar component are below the brand text 300 * 301 */ 302 $appliedWidth = $requestedWidth; 303 if ($requestedWidth === null) { 304 if ($svgUsageType == self::ICON_TYPE) { 305 $appliedWidth = self::DEFAULT_ICON_WIDTH; 306 } else { 307 // tile 308 $appliedWidth = "192"; 309 } 310 } 311 /** 312 * Dimension 313 * The default unit on attribute is pixel, no need to add it 314 * as in CSS 315 */ 316 $localTagAttributes->addOutputAttributeValue("width", $appliedWidth); 317 $height = $localTagAttributes->getValueAndRemove(Dimension::HEIGHT_KEY, $appliedWidth); 318 $localTagAttributes->addOutputAttributeValue("height", $height); 319 break; 320 default: 321 /** 322 * Illustration / Image 323 */ 324 /** 325 * Responsive SVG 326 */ 327 if (!$localTagAttributes->hasComponentAttribute("preserveAspectRatio")) { 328 /** 329 * 330 * Keep the same height 331 * Image in the Middle and border deleted when resizing 332 * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio 333 * Default is xMidYMid meet 334 */ 335 $defaultAspectRatio = PluginUtility::getConfValue(self::CONF_PRESERVE_ASPECT_RATIO_DEFAULT, "xMidYMid slice"); 336 $localTagAttributes->addOutputAttributeValue("preserveAspectRatio", $defaultAspectRatio); 337 } 338 339 /** 340 * Note on dimension width and height 341 * Width and height element attribute are in reality css style properties. 342 * ie the max-width style 343 * They are treated in {@link PluginUtility::processStyle()} 344 */ 345 346 /** 347 * Adapt to the container by default 348 * Height `auto` and not `100%` otherwise you get a layout shift 349 */ 350 $localTagAttributes->addStyleDeclarationIfNotSet("width", "100%"); 351 $localTagAttributes->addStyleDeclarationIfNotSet("height", "auto"); 352 353 354 if ($requestedWidth !== null) { 355 356 /** 357 * If a dimension was set, it's seen by default as a max-width 358 * If it should not such as in a card, this property is already set 359 * and is not overwritten 360 */ 361 try { 362 $widthInPixel = Dimension::toPixelValue($requestedWidth); 363 } catch (ExceptionCombo $e) { 364 LogUtility::msg("The requested width $requestedWidth could not be converted to pixel. It returns the following error ({$e->getMessage()}). Processing was stopped"); 365 return parent::getXmlText(); 366 } 367 $localTagAttributes->addStyleDeclarationIfNotSet("max-width", "{$widthInPixel}px"); 368 369 /** 370 * To have an internal width 371 * and not shrink on the css property `width: auto !important;` 372 * of a table 373 */ 374 $this->setRootAttribute("width", $widthInPixel); 375 376 } 377 378 break; 379 } 380 381 382 switch ($svgStructureType) { 383 case self::ICON_TYPE: 384 case self::TILE_TYPE: 385 /** 386 * Determine if this is a: 387 * * fill one color 388 * * fill two colors 389 * * or stroke svg icon 390 * 391 * The color can be set: 392 * * on fill (surface) 393 * * on stroke (line) 394 * 395 * If the stroke attribute is not present this is a fill icon 396 */ 397 $svgColorType = self::COLOR_TYPE_FILL_SOLID; 398 if ($documentElement->hasAttribute(self::STROKE_ATTRIBUTE)) { 399 $svgColorType = self::COLOR_TYPE_STROKE_OUTLINE; 400 } 401 /** 402 * Double color icon ? 403 */ 404 $isDoubleColor = false; 405 if ($svgColorType === self::COLOR_TYPE_FILL_SOLID) { 406 $svgFillsElement = $this->xpath("//*[@fill]"); 407 $fillColors = []; 408 for ($i = 0; $i < $svgFillsElement->length; $i++) { 409 /** 410 * @var DOMElement $nodeElement 411 */ 412 $nodeElement = $svgFillsElement[$i]; 413 $value = $nodeElement->getAttribute("fill"); 414 if ($value !== "none") { 415 /** 416 * Icon may have none alongside colors 417 * Example: 418 */ 419 $fillColors[$value] = $value; 420 } 421 } 422 if (sizeof($fillColors) > 1) { 423 $isDoubleColor = true; 424 } 425 } 426 427 /** 428 * CurrentColor 429 * 430 * By default, the icon should have this property when downloaded 431 * but if this not the case (such as for Material design), we set them 432 * 433 * Feather set it on the stroke 434 * Example: view-source:https://raw.githubusercontent.com/feathericons/feather/master/icons/airplay.svg 435 * <svg 436 * fill="none" 437 * stroke="currentColor"> 438 */ 439 if (!$isDoubleColor && !$documentElement->hasAttribute("fill")) { 440 441 /** 442 * Note: if fill was not set, the default color would be black 443 */ 444 $localTagAttributes->addOutputAttributeValue("fill", self::CURRENT_COLOR); 445 446 } 447 448 449 /** 450 * Eva/Carbon Source Icon are not optimized at the source 451 * Example: 452 * * eva:facebook-fill 453 * * carbon:logo-tumblr (https://github.com/carbon-design-system/carbon/issues/5568) 454 * 455 * We delete the rectangle 456 * Style should have already been deleted by the optimization 457 * 458 * This optimization should happen if the color is set 459 * or not because we set the color value to `currentColor` 460 * 461 * If the rectangle stay, we just see a black rectangle 462 */ 463 if ($this->path !== null) { 464 $pathString = $this->path->toAbsolutePath()->toString(); 465 if ( 466 preg_match("/carbon|eva/i", $pathString) === 1 467 ) { 468 $this->deleteAllElements("rect"); 469 } 470 } 471 472 $color = $localTagAttributes->getValueAndRemoveIfPresent(ColorRgb::COLOR); 473 if ($svgUsageType === self::ILLUSTRATION_TYPE && $color === null) { 474 $primaryColor = Site::getPrimaryColorValue(); 475 if ($primaryColor !== null) { 476 $color = $primaryColor; 477 } 478 } 479 480 /** 481 * Color 482 * Color applies only if this is an icon. 483 * 484 */ 485 if ($color !== null) { 486 /** 487 * 488 * We say that this is used only for an icon (<72 px) 489 * 490 * Not that an icon svg file can also be used as {@link \syntax_plugin_combo_pageimage} 491 * 492 * We don't set it as a styling attribute 493 * because it's not taken into account if the 494 * svg is used as a background image 495 * fill or stroke should have at minimum "currentColor" 496 */ 497 $colorValue = ColorRgb::createFromString($color)->toCssValue(); 498 499 500 switch ($svgColorType) { 501 case self::COLOR_TYPE_FILL_SOLID: 502 503 504 if (!$isDoubleColor) { 505 506 $localTagAttributes->addOutputAttributeValue("fill", $colorValue); 507 508 if ($colorValue !== self::CURRENT_COLOR) { 509 /** 510 * Update the fill property on sub-path 511 * If the fill is set on sub-path, it will not work 512 * 513 * fill may be set on group or whatever 514 */ 515 $svgPaths = $this->xpath("//*[local-name()='path' or local-name()='g']"); 516 for ($i = 0; $i < $svgPaths->length; $i++) { 517 /** 518 * @var DOMElement $nodeElement 519 */ 520 $nodeElement = $svgPaths[$i]; 521 $value = $nodeElement->getAttribute("fill"); 522 if ($value !== "none") { 523 if ($nodeElement->parentNode->tagName !== "svg") { 524 $nodeElement->setAttribute("fill", self::CURRENT_COLOR); 525 } else { 526 $this->removeAttributeValue("fill", $nodeElement); 527 } 528 } 529 } 530 531 } 532 } else { 533 // double color 534 $firsFillElement = $this->xpath("//*[@fill][1]")->item(0); 535 if ($firsFillElement instanceof DOMElement) { 536 $firsFillElement->setAttribute("fill", $colorValue); 537 } 538 } 539 540 break; 541 case self::COLOR_TYPE_STROKE_OUTLINE: 542 $localTagAttributes->addOutputAttributeValue("fill", "none"); 543 $localTagAttributes->addOutputAttributeValue(self::STROKE_ATTRIBUTE, $colorValue); 544 545 if ($colorValue !== self::CURRENT_COLOR) { 546 /** 547 * Delete the stroke property on sub-path 548 */ 549 // if the fill is set on sub-path, it will not work 550 $svgPaths = $this->xpath("//*[local-name()='path']"); 551 for ($i = 0; $i < $svgPaths->length; $i++) { 552 /** 553 * @var DOMElement $nodeElement 554 */ 555 $nodeElement = $svgPaths[$i]; 556 $value = $nodeElement->getAttribute(self::STROKE_ATTRIBUTE); 557 if ($value !== "none") { 558 $this->removeAttributeValue(self::STROKE_ATTRIBUTE, $nodeElement); 559 } else { 560 $this->removeNode($nodeElement); 561 } 562 } 563 564 } 565 break; 566 } 567 568 } 569 570 571 break; 572 573 } 574 575 /** 576 * Ratio / Cropping (used for ratio cropping) 577 * Width and height used to set the viewBox of a svg 578 * to crop it 579 * (In a raster image, there is not this distinction) 580 * 581 * With an icon, the viewBox can be small but it can be zoomed out 582 * via the {@link Dimension::WIDTH_KEY} 583 */ 584 $processedWidth = $mediaWidth; 585 $processedHeight = $mediaHeight; 586 if ($localTagAttributes->hasComponentAttribute(Dimension::RATIO_ATTRIBUTE)) { 587 // We get a crop, it means that we need to change the viewBox 588 $ratio = $localTagAttributes->getValueAndRemoveIfPresent(Dimension::RATIO_ATTRIBUTE); 589 try { 590 $targetRatio = Dimension::convertTextualRatioToNumber($ratio); 591 } catch (ExceptionCombo $e) { 592 LogUtility::msg("The target ratio attribute ($ratio) returns the following error ({$e->getMessage()}). The svg processing was stopped"); 593 return parent::getXmlText(); 594 } 595 [$processedWidth, $processedHeight] = Image::getCroppingDimensionsWithRatio($targetRatio, $mediaWidth, $mediaHeight); 596 597 $this->setRootAttribute(self::VIEW_BOX, "0 0 $processedWidth $processedHeight"); 598 599 } 600 601 /** 602 * Zoom occurs after the crop if any 603 */ 604 $zoomFactor = $localTagAttributes->getValueAsInteger(Dimension::ZOOM_ATTRIBUTE); 605 if ($zoomFactor === null 606 && $svgStructureType === self::ICON_TYPE 607 && $svgUsageType === self::ILLUSTRATION_TYPE 608 ) { 609 $zoomFactor = -4; 610 } 611 if ($zoomFactor !== null) { 612 // icon case, we zoom out otherwise, this is ugly, the icon takes the whole place 613 if ($zoomFactor < 0) { 614 $processedWidth = -$zoomFactor * $processedWidth; 615 $processedHeight = -$zoomFactor * $processedHeight; 616 } else { 617 $processedWidth = $processedWidth / $zoomFactor; 618 $processedHeight = $processedHeight / $zoomFactor; 619 } 620 // center 621 $actualWidth = $mediaWidth; 622 $actualHeight = $mediaHeight; 623 $x = -($processedWidth - $actualWidth) / 2; 624 $y = -($processedHeight - $actualHeight) / 2; 625 $this->setRootAttribute(self::VIEW_BOX, "$x $y $processedWidth $processedHeight"); 626 } 627 628 629 // Add a class on each path for easy styling 630 if (!empty($this->name)) { 631 $svgPaths = $this->xpath("//*[local-name()='path']"); 632 for ($i = 0; 633 $i < $svgPaths->length; 634 $i++) { 635 636 $stylingClass = $this->name . "-" . $i; 637 $this->addAttributeValue("class", $stylingClass, $svgPaths[$i]); 638 639 } 640 } 641 642 /** 643 * Svg attribute are case sensitive 644 * but not the component attribute key 645 * we get the value and set it then as HTML to have the good casing 646 * on this attribute 647 */ 648 $caseSensitives = ["preserveAspectRatio"]; 649 foreach ($caseSensitives as $caseSensitive) { 650 if ($localTagAttributes->hasComponentAttribute($caseSensitive)) { 651 $aspectRatio = $localTagAttributes->getValueAndRemove($caseSensitive); 652 $localTagAttributes->addOutputAttributeValue($caseSensitive, $aspectRatio); 653 } 654 } 655 656 /** 657 * Old model where the src was parsed in the render 658 * When the attributes are in the {@link Path} we can delete this 659 */ 660 $localTagAttributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME); 661 662 /** 663 * Set the attributes to the root 664 */ 665 $toHtmlArray = $localTagAttributes->toHtmlArray(); 666 foreach ($toHtmlArray as $name => $value) { 667 if (in_array($name, TagAttributes::MULTIPLE_VALUES_ATTRIBUTES)) { 668 $actualValue = $this->getRootAttributeValue($name); 669 if ($actualValue !== null) { 670 $value = TagAttributes::mergeClassNames($value, $actualValue); 671 } 672 } 673 $this->setRootAttribute($name, $value); 674 } 675 676 return parent::getXmlText(); 677 678 } 679 680 681 /** 682 * @param $boolean 683 * @return SvgDocument 684 */ 685 public 686 function setShouldBeOptimized($boolean): SvgDocument 687 { 688 $this->shouldBeOptimized = $boolean; 689 return $this; 690 } 691 692 /** 693 * @throws ExceptionCombo 694 */ 695 public 696 function getMediaWidth(): int 697 { 698 $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX); 699 if ($viewBox !== "") { 700 $attributes = $this->getViewBoxAttributes($viewBox); 701 $viewBoxWidth = $attributes[2]; 702 try { 703 return DataType::toInteger($viewBoxWidth); 704 } catch (ExceptionCombo $e) { 705 throw new ExceptionCombo("The media with ($viewBoxWidth) of the svg image ($this) is not a valid integer value"); 706 } 707 } 708 709 /** 710 * Case with some icon such as 711 * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg 712 */ 713 $width = $this->getXmlDom()->documentElement->getAttribute("width"); 714 if ($width === "") { 715 throw new ExceptionCombo("The svg ($this) does not have a viewBox or width attribute, the intrinsic width cannot be determined"); 716 } 717 try { 718 return DataType::toInteger($width); 719 } catch (ExceptionCombo $e) { 720 throw new ExceptionCombo("The media width ($width) of the svg image ($this) is not a valid integer value"); 721 } 722 723 } 724 725 /** 726 * @throws ExceptionCombo 727 */ 728 public 729 function getMediaHeight(): int 730 { 731 $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX); 732 if ($viewBox !== "") { 733 $attributes = $this->getViewBoxAttributes($viewBox); 734 $viewBoxHeight = $attributes[3]; 735 try { 736 return DataType::toInteger($viewBoxHeight); 737 } catch (ExceptionCombo $e) { 738 throw new ExceptionCombo("The media height of the svg image ($this) is not a valid integer value"); 739 } 740 } 741 /** 742 * Case with some icon such as 743 * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg 744 */ 745 $height = $this->getXmlDom()->documentElement->getAttribute("height"); 746 if ($height === "") { 747 throw new ExceptionCombo("The svg ($this) does not have a viewBox or height attribute, the intrinsic height cannot be determined"); 748 } 749 try { 750 return DataType::toInteger($height); 751 } catch (ExceptionCombo $e) { 752 throw new ExceptionCombo("The media width ($height) of the svg image ($this) is not a valid integer value"); 753 } 754 755 } 756 757 758 private 759 function getSvgPaths() 760 { 761 if ($this->isXmlExtensionLoaded()) { 762 763 /** 764 * If the file was optimized, the svg namespace 765 * does not exist anymore 766 */ 767 $namespace = $this->getDocNamespaces(); 768 if (isset($namespace[self::SVG_NAMESPACE_PREFIX])) { 769 $svgNamespace = self::SVG_NAMESPACE_PREFIX; 770 $query = "//$svgNamespace:path"; 771 } else { 772 $query = "//*[local-name()='path']"; 773 } 774 return $this->xpath($query); 775 } else { 776 return array(); 777 } 778 779 780 } 781 782 783 /** 784 * Optimization 785 * Based on https://jakearchibald.github.io/svgomg/ 786 * (gui of https://github.com/svg/svgo) 787 */ 788 public 789 function optimize($tagAttributes) 790 { 791 792 if ($this->shouldOptimize()) { 793 794 /** 795 * Delete Editor namespace 796 * https://github.com/svg/svgo/blob/master/plugins/removeEditorsNSData.js 797 */ 798 $confNamespaceToKeeps = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_NAMESPACES_TO_KEEP); 799 $namespaceToKeep = StringUtility::explodeAndTrim($confNamespaceToKeeps, ","); 800 foreach ($this->getDocNamespaces() as $namespacePrefix => $namespaceUri) { 801 if ( 802 !empty($namespacePrefix) 803 && $namespacePrefix != "svg" 804 && !in_array($namespacePrefix, $namespaceToKeep) 805 && in_array($namespaceUri, self::EDITOR_NAMESPACE) 806 ) { 807 $this->removeNamespace($namespaceUri); 808 } 809 } 810 811 /** 812 * Delete empty namespace rules 813 */ 814 $documentElement = &$this->getXmlDom()->documentElement; 815 foreach ($this->getDocNamespaces() as $namespacePrefix => $namespaceUri) { 816 $nodes = $this->xpath("//*[namespace-uri()='$namespaceUri']"); 817 $attributes = $this->xpath("//@*[namespace-uri()='$namespaceUri']"); 818 if ($nodes->length == 0 && $attributes->length == 0) { 819 $result = $documentElement->removeAttributeNS($namespaceUri, $namespacePrefix); 820 if ($result === false) { 821 LogUtility::msg("Internal error: The deletion of the empty namespace ($namespacePrefix:$namespaceUri) didn't succeed", LogUtility::LVL_MSG_WARNING, "support"); 822 } 823 } 824 } 825 826 /** 827 * Delete comments 828 */ 829 $commentNodes = $this->xpath("//comment()"); 830 foreach ($commentNodes as $commentNode) { 831 $this->removeNode($commentNode); 832 } 833 834 /** 835 * Delete default value (version=1.1 for instance) 836 */ 837 $defaultValues = self::SVG_DEFAULT_ATTRIBUTES_VALUE; 838 foreach ($documentElement->attributes as $attribute) { 839 /** @var DOMAttr $attribute */ 840 $name = $attribute->name; 841 if (isset($defaultValues[$name])) { 842 if ($defaultValues[$name] == $attribute->value) { 843 $documentElement->removeAttributeNode($attribute); 844 } 845 } 846 } 847 848 /** 849 * Suppress the attributes (by default id, style and class, data-name) 850 */ 851 $attributeConfToDelete = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE, "id, style, class, data-name"); 852 $attributesNameToDelete = StringUtility::explodeAndTrim($attributeConfToDelete, ","); 853 foreach ($attributesNameToDelete as $value) { 854 855 if (in_array($value, ["style", "class", "id"]) && self::preserveStyle($tagAttributes)) { 856 // we preserve the style, we preserve the class 857 continue; 858 } 859 860 $nodes = $this->xpath("//@$value"); 861 foreach ($nodes as $node) { 862 /** @var DOMAttr $node */ 863 /** @var DOMElement $DOMNode */ 864 $DOMNode = $node->parentNode; 865 $DOMNode->removeAttributeNode($node); 866 } 867 } 868 869 /** 870 * Remove width/height that coincides with a viewBox attr 871 * https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute 872 * Example: 873 * <svg width="100" height="50" viewBox="0 0 100 50"> 874 * <svg viewBox="0 0 100 50"> 875 * 876 */ 877 $widthAttributeValue = $documentElement->getAttribute("width"); 878 if (!empty($widthAttributeValue)) { 879 $widthPixel = Unit::toPixel($widthAttributeValue); 880 881 $heightAttributeValue = $documentElement->getAttribute("height"); 882 if (!empty($heightAttributeValue)) { 883 $heightPixel = Unit::toPixel($heightAttributeValue); 884 885 // ViewBox 886 $viewBoxAttribute = $documentElement->getAttribute(self::VIEW_BOX); 887 if (!empty($viewBoxAttribute)) { 888 $viewBoxAttributeAsArray = StringUtility::explodeAndTrim($viewBoxAttribute, " "); 889 890 if (sizeof($viewBoxAttributeAsArray) == 4) { 891 $minX = $viewBoxAttributeAsArray[0]; 892 $minY = $viewBoxAttributeAsArray[1]; 893 $widthViewPort = $viewBoxAttributeAsArray[2]; 894 $heightViewPort = $viewBoxAttributeAsArray[3]; 895 if ( 896 $minX == 0 & 897 $minY == 0 & 898 $widthViewPort == $widthPixel & 899 $heightViewPort == $heightPixel 900 ) { 901 $documentElement->removeAttribute("width"); 902 $documentElement->removeAttribute("height"); 903 } 904 905 } 906 } 907 } 908 } 909 910 911 /** 912 * Suppress script and style 913 * 914 * 915 * Delete of scripts https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script 916 * 917 * And defs/style 918 * 919 * The style can leak in other icon/svg inlined in the document 920 * 921 * Technically on icon, there should be no `style` 922 * on inline icon otherwise, the css style can leak 923 * 924 * Example with carbon that use cls-1 on all icons 925 * https://github.com/carbon-design-system/carbon/issues/5568 926 * The facebook icon has a class cls-1 with an opacity of 0 927 * that leaks to the tumblr icon that has also a cls-1 class 928 * 929 * The illustration uses inline fill to color and styled 930 * For instance, all un-draw: https://undraw.co/illustrations 931 */ 932 $elementsToDeleteConf = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE, "script, style, title, desc"); 933 $elementsToDelete = StringUtility::explodeAndTrim($elementsToDeleteConf, ","); 934 foreach ($elementsToDelete as $elementToDelete) { 935 if ($elementToDelete === "style" && self::preserveStyle($tagAttributes)) { 936 continue; 937 } 938 $this->deleteAllElements($elementToDelete); 939 } 940 941 // Delete If Empty 942 // * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs 943 // * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata 944 $elementsToDeleteIfEmptyConf = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY, "metadata, defs, g"); 945 $elementsToDeleteIfEmpty = StringUtility::explodeAndTrim($elementsToDeleteIfEmptyConf); 946 foreach ($elementsToDeleteIfEmpty as $elementToDeleteIfEmpty) { 947 $elementNodeList = $this->xpath("//*[local-name()='$elementToDeleteIfEmpty']"); 948 foreach ($elementNodeList as $element) { 949 /** @var DOMElement $element */ 950 if (!$element->hasChildNodes()) { 951 $element->parentNode->removeChild($element); 952 } 953 } 954 } 955 956 /** 957 * Delete the svg prefix namespace definition 958 * At the end to be able to query with svg as prefix 959 */ 960 if (!in_array("svg", $namespaceToKeep)) { 961 $documentElement->removeAttributeNS(self::SVG_NAMESPACE_URI, self::SVG_NAMESPACE_PREFIX); 962 } 963 964 } 965 } 966 967 public 968 function shouldOptimize() 969 { 970 971 return $this->shouldBeOptimized; 972 973 } 974 975 /** 976 * The name is used to add class in the svg 977 * @param $name 978 */ 979 private 980 function setName($name) 981 { 982 $this->name = $name; 983 } 984 985 /** 986 * Set the context 987 * @param Path $path 988 */ 989 private 990 function setPath(Path $path) 991 { 992 $this->path = $path; 993 } 994 995 public 996 function __toString() 997 { 998 if ($this->path !== null) { 999 return $this->path->__toString(); 1000 } 1001 if ($this->name !== null) { 1002 return $this->name; 1003 } 1004 return "unknown"; 1005 } 1006 1007 private 1008 function isInIconDirectory(): bool 1009 { 1010 if ($this->path == null) { 1011 return false; 1012 } 1013 $iconNameSpace = PluginUtility::getConfValue(Icon::CONF_ICONS_MEDIA_NAMESPACE, Icon::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT); 1014 if (strpos($this->path->toString(), $iconNameSpace) !== false) { 1015 return true; 1016 } 1017 return false; 1018 } 1019 1020 /** 1021 * An utility function to know how to remove a node 1022 * @param DOMNode $nodeElement 1023 */ 1024 private 1025 function removeNode(DOMNode $nodeElement) 1026 { 1027 $nodeElement->parentNode->removeChild($nodeElement); 1028 } 1029 1030 private 1031 function deleteAllElements(string $elementName) 1032 { 1033 $svgElement = $this->xpath("//*[local-name()='$elementName']"); 1034 for ($i = 0; $i < $svgElement->length; $i++) { 1035 /** 1036 * @var DOMElement $nodeElement 1037 */ 1038 $nodeElement = $svgElement[$i]; 1039 $this->removeNode($nodeElement); 1040 } 1041 } 1042 1043 /** 1044 * @param string $viewBox 1045 * @return string[] 1046 */ 1047 private function getViewBoxAttributes(string $viewBox): array 1048 { 1049 $attributes = explode(" ", $viewBox); 1050 if(sizeof($attributes)===1){ 1051 /** 1052 * We may find also comma. Example: 1053 * viewBox="0,0,433.62,289.08" 1054 */ 1055 $attributes = explode(",", $viewBox); 1056 } 1057 return $attributes; 1058 } 1059 1060 1061} 1062