diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b8861da..0000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -sudo: false - -language: python - -python: - - pypy - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - 3.10-dev -install: - - pip install tox-travis codecov - -script: - - tox - -matrix: - fast_finish: true - - include: - - python: 3.9 - env: - - TOXENV=flake8 - -after_success: - - coverage combine && codecov diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 96cb109..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing - -Contributions to Scour are welcome, feel free to create a pull request! - -In order to be able to merge your PR as fast as possible please try to stick to the following guidelines. - -> _**TL;DR** (if you now what you're doing) – Always run [`make check`](https://github.com/scour-project/scour/blob/master/Makefile) before creating a PR to check for common problems._ - - -## Code Style - -The Scour project tries to follow the coding conventions described in [PEP 8 - The Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/). While there are some inconsistencies in existing code (e.g. with respect to naming conventions and the usage of globals), new code should always abide by the standard. - -To quickly check for common mistakes you can use [`flake8`](https://pypi.python.org/pypi/flake8). Our [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) has a convenience target with the correct options: -```Makefile -make flake8 -``` - -## Unit Tests - -In order to check functionality of Scour and prevent any regressions in existing code a number of tests exist which use the [`unittest`](https://docs.python.org/library/unittest.html) unit testing framework which ships with Python. You can quickly run the tests by using the [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) convenience target: -```Makefile -make test -``` - -These tests are run automatically on all PRs using [TravisCI](https://travis-ci.org/scour-project/scour) and have to pass at all times! When you add new functionality you should always include suitable tests with your PR (see [`test_scour.py`](https://github.com/scour-project/scour/blob/master/test_scour.py)). - -### Coverage - -To ensure that all possible code conditions are covered by a test you can use [`coverage`](https://pypi.python.org/pypi/coverage). The [Makefile](https://github.com/scour-project/scour/blob/master/Makefile) convenience target automatically creates an HTML report in `htmlcov/index.html`: -```Makefile -make coverage -``` - -These reports are also created automatically by our TravisCI builds and are accessible via [Codecov](https://codecov.io/gh/scour-project/scour) diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index de0b503..0000000 --- a/HISTORY.md +++ /dev/null @@ -1,347 +0,0 @@ -# Release Notes for Scour - -## Version 0.38.2 (2020-11-22) -* Fix another regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) - -## Version 0.38.1 (2020-09-02) -* Fix regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) - -## Version 0.38 (2020-08-06) -* Fix issue with dropping xlink:href attribute when collapsing referenced gradients ([#206](https://github.com/scour-project/scour/pull/206)) -* Fix issue with dropping ID while de-duplicating gradients ([#207](https://github.com/scour-project/scour/pull/207)) -* Improve `--shorten-ids` so it re-maps IDs that are already used in the document if they're shorter ([#187](https://github.com/scour-project/scour/pull/187)) -* Fix whitespace handling for SVG 1.2 flowed text ([#235](https://github.com/scour-project/scour/issues/235)) -* Improvement: Merge sibling `` nodes with identical attributes ([#208](https://github.com/scour-project/scour/pull/208)) -* Improve performance of XML serialization ([#247](https://github.com/scour-project/scour/pull/247)) -* Improve performance of gradient de-duplication ([#248](https://github.com/scour-project/scour/pull/248)) -* Some general performance improvements ([#249](https://github.com/scour-project/scour/pull/249)) - -## Version 0.37 (2018-07-04) -* Fix escaping of quotes in attribute values. ([#152](https://github.com/scour-project/scour/pull/152)) -* A lot of performance improvements making processing significantly faster in many cases. ([#167](https://github.com/scour-project/scour/pull/167), [#169](https://github.com/scour-project/scour/pull/169), [#171](https://github.com/scour-project/scour/pull/171), [#185](https://github.com/scour-project/scour/pull/185)) -* Fix exception when removing duplicated gradients while `--keep-unreferenced-defs` is used ([#173](https://github.com/scour-project/scour/pull/173)) -* Remove some illegal optimizations of `m0 0` sub-path commands ([#178](https://github.com/scour-project/scour/pull/178)) -* Fix and improve handling of boolean flags in elliptical arc path commands ([#183](https://github.com/scour-project/scour/pull/183)) -* Fix exception when shorthand transform `scale(1)` with single number is used ([#191](https://github.com/scour-project/scour/pull/191)) -* Fix exception when using two-number forms of the filter attributes `baseFrequency`, `order`, `radius` and `stdDeviation` ([#192](https://github.com/scour-project/scour/pull/192)) -* Improve whitespace handling in text nodes fixing an issue where scouring added spaces in error and reducing file size in many cases ([#199](https://github.com/scour-project/scour/pull/199)) -* Drop official support for Python 3.3. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 3.3 and should find/fix any compatibility issues pull requests are welcome, though.) - - -## Version 0.36 (2017-08-06) -* Fix embedding of raster images which was broken in most cases and did not work at all in Python 3. ([#120](https://github.com/scour-project/scour/issues/120)) -* Some minor fixes for statistics output. -* Greatly improve the algorithm to reduce numeric precision. - * Precision was not properly reduced for some numbers. - * Only use reduced precision if it results in a shorter string representation, otherwise preserve full precision in output (e.g. use "123" instead of "1e2" when precision is set to 1). - * Reduce precision of lengths in `viewBox` ([#127](https://github.com/scour-project/scour/issues/127)) - * Add option `--set-c-precision` which allows to set a reduced numeric precision for control points.
Control points determine how a path is bent in between two nodes and are less sensitive to a reduced precision than the position coordinates of the nodes themselves. This option can be used to save a few additional bytes without affecting visual appearance negatively. -* Fix: Unnecessary whitespace was not stripped from elliptical paths. ([#89](https://github.com/scour-project/scour/issues/89)) -* Improve and fix functionality to collapse straight paths segments. ([#146](https://github.com/scour-project/scour/issues/146)) - * Collapse subpaths of moveto `m` and lineto `l`commands if they have the same direction (before we only collapsed horizontal/vertical `h`/`v` lineto commands). - * Attempt to collapse lineto `l` commands into a preceding moveto `m` command (these are then called "implicit lineto commands") - * Do not collapse straight path segments in paths that have intermediate markers. ([#145](https://github.com/scour-project/scour/issues/145)) - * Preserve empty path segments if they have `stroke-linecap` set to `round` or `square`. They render no visible line but a tiny dot or square. - - -## Version 0.35 (2016-09-14) - -* Drop official support for Python 2.6. (While it will probably continue to work for a while compatibility is not guaranteed anymore. If you continue to use Scour with Python 2.6 and should find/fix any compatibility issues pull requests are welcome, though.) -* Fix: Unused IDs were not shortened when `--shorten-ids` was used. ([#19](https://github.com/scour-project/scour/issues/62)) -* Fix: Most elements were still removed from `` when `--keep-unreferenced-defs` was used. ([#62](https://github.com/scour-project/scour/issues/62)) -* Improve escaping of single/double quotes ('/") in attributes. ([#64](https://github.com/scour-project/scour/issues/64)) -* Print usage information if no input file was specified (and no data is available from `stdin`). ([#65](https://github.com/scour-project/scour/issues/65)) -* Redirect informational output to `stderr` when SVG is output to `stdout`. ([#67](https://github.com/scour-project/scour/issues/67)) -* Allow elements to be found via `Document.getElementById()` in the minidom document returned by scourXmlFile(). ([#68](https://github.com/scour-project/scour/issues/68)) -* Improve code to remove default attribute values and add a lot of new default values. ([#70](https://github.com/scour-project/scour/issues/70)) -* Fix: Only attempt to group elements that the content model allows to be children of a `` when `--create-groups` is specified. ([#98](https://github.com/scour-project/scour/issues/98)) -* Fix: Update list of SVG presentation attributes allowing more styles to be converted to attributes and remove two entries (`line-height` and `visibility`) that were actually invalid. ([#99](https://github.com/scour-project/scour/issues/99)) -* Add three options that work analogous to `--remove-metadata` (removes `` elements) ([#102](https://github.com/scour-project/scour/issues/102)) - * `--remove-titles` (removes `` elements) - * `--remove-descriptions` (removes `<desc>` elements) - * `--remove-descriptive-elements` (removes all of the descriptive elements, i.e. `<title>`, `<desc>` and `<metadata>`) -* Fix removal rules for the `overflow` attribute. ([#104](https://github.com/scour-project/scour/issues/104)) -* Improvement: Automatically order all attributes ([#105](https://github.com/scour-project/scour/issues/105)), as well as `style` declarations ([#107](https://github.com/scour-project/scour/issues/107)) allowing for a constant output across multiple runs of Scour. Before order could change arbitrarily. -* Improve path scouring. ([#108](https://github.com/scour-project/scour/issues/108))<br>Notably Scour performs all calculations with enhanced precision now, guaranteeing maximum accuracy when optimizing path data. Numerical precision is reduced as a last step of the optimization according to the `--precision` option. -* Fix replacement of removed duplicate gradients if the `fill`/`stroke` properties contained a fallback. ([#109](https://github.com/scour-project/scour/issues/109)) -* Fix conversion of cubic Bézier "curveto" commands into "shorthand/smooth curveto" commands. ([#110](https://github.com/scour-project/scour/issues/110)) -* Fix some issues due to removal of properties without considering inheritance rules. ([#111](https://github.com/scour-project/scour/issues/111)) - - -## Version 0.34 (2016-07-25) - -* Add a function to sanitize an arbitrary Python object containing options for Scour as attributes (usage: `Scour.sanitizeOptions(options)`).<br>This simplifies usage of the Scour module by other scripts while avoiding any compatibility issues that might arise when options are added/removed/renamed in Scour. ([#44](https://github.com/scour-project/scour/issues/44)) -* Input/output file can now be specified as positional arguments (e.g. `scour input.svg output.svg`). ([#46](https://github.com/scour-project/scour/issues/46)) -* Improve `--help` output by intuitively arranging options in groups. ([#46](https://github.com/scour-project/scour/issues/46)) -* Add option `--error-on-flowtext` to raise an exception whenever a non-standard `<flowText>` element is found (which is only supported in Inkscape). If this option is not specified a warning will be shown. ([#53](https://github.com/scour-project/scour/issues/53)) -* Automate tests with continuous integration via Travis. ([#52](https://github.com/scour-project/scour/issues/52)) - - -## Version 0.33 (2016-01-29) - -* Add support for removal of editor data of Sketch. ([#37](https://github.com/scour-project/scour/issues/37)) -* Add option `--verbose` (or `-v`) to show detailed statistics after running Scour. By default only a single line containing the most important information is output now. - - -## Version 0.32 (2015-12-10) - -* Add functionality to remove unused XML namespace declarations from the `<svg>` root element. ([#14](https://github.com/scour-project/scour/issues/14)) -* Restore unittests which were lost during move to GitHub. ([#24](https://github.com/scour-project/scour/issues/24)) -* Fix a potential regex matching issue in `points` attribute of `<polygon>` and `<polyline>` elements. ([#24](https://github.com/scour-project/scour/issues/24)) -* Fix a crash with `points` attribute of `<polygon>` and `<polyline>` starting with a negative number. ([#24](https://github.com/scour-project/scour/issues/24)) -* Fix encoding issues when input file contained unicode characters. ([#27](https://github.com/scour-project/scour/issues/27)) -* Fix encoding issues when using `stding`/`stdout` as input/output. ([#27](https://github.com/scour-project/scour/issues/27)) -* Fix removal of comments. If a node contained multiple comments usually not all of them were removed. ([#28](https://github.com/scour-project/scour/issues/28)) - - -## Version 0.31 (2015-11-16) - -* Ensure Python 3 compatibility. ([#8](https://github.com/scour-project/scour/issues/8)) -* Add option `--nindent` to set the number of spaces/tabs used for indentation (defaults to 1). ([#13](https://github.com/scour-project/scour/issues/13)) -* Add option `--no-line-breaks` to suppress output of line breaks and indentation altogether. ([#13](https://github.com/scour-project/scour/issues/13)) -* Add option `--strip-xml-space` which removes the specification of `xml:space="preserve"` on the `<svg>` root element which would otherwise disallow Scour to make any whitespace changes in output. ([#13](https://github.com/scour-project/scour/issues/13)) - - -## Version 0.30 (2014-08-05) - -* Fix ignoring of additional args when invoked from scons. - - -## Version 0.29 (2014-07-26) - -* Add option `--keep-unreferenced-defs` to preserve elements in `<defs>` that are not referenced and would be removed otherwise. ([#2](https://github.com/scour-project/scour/issues/2)) -* Add option to ignore unknown cmd line opts. - - -## Version 0.28 (2014-01-12) - -* Add option `--shorten-ids-prefix` which allows to add a custom prefix to all shortened IDs. ([#1](https://github.com/scour-project/scour/issues/1)) - - -## Version 0.27 (2013-10-26) - -* Allow direct calling of the Scour module. - - -## Version 0.26 (2013-10-22) - -* Re-release of Scour 0.26, re-packaged as a Python module [available from PyPI](https://pypi.python.org/pypi/scour) (Thanks to [Tobias Oberstet](https://github.com/oberstet)!). -* Development moved to GitHub (https://github.com/scour-project/scour). - - -## Version 0.26 (2011-05-09) - -* Fix [Bug 702423](https://bugs.launchpad.net/scour/+bug/702423) to function well in the presence of multiple identical gradients and `--disable-style-to-xml`. -* Fix [Bug 722544](https://bugs.launchpad.net/scour/+bug/722544) to properly optimize transformation matrices. Also optimize more things away in transformation specifications. (Thanks to Johan Sundström for the patch.) -* Fix [Bug 616150](https://bugs.launchpad.net/scour/+bug/616150) to run faster using the `--create-groups` option. -* Fix [Bug 708515](https://bugs.launchpad.net/scour/+bug/562784) to handle raster embedding better in the presence of file:// URLs. -* Fix [Bug 714717](https://bugs.launchpad.net/scour/+bug/714717) to avoid deleting renderable CurveTo commands in paths, which happen to end where they started. -* Per [Bug 714727](https://bugs.launchpad.net/scour/+bug/714727) and [Bug 714720](https://bugs.launchpad.net/scour/+bug/714720), Scour now deletes text attributes, including "text-align", from elements and groups of elements that only contain shapes. (Thanks to Jan Thor for the patches.) -* Per [Bug 714731](https://bugs.launchpad.net/scour/+bug/714731), remove the default value of more SVG attributes. (Thanks to Jan Thor for the patch.) -* Fix [Bug 717826](https://bugs.launchpad.net/scour/+bug/717826) to emit the correct line terminator (CR LF) in optimized SVG content on the version of Scour used in Inkscape on Windows. -* Fix [Bug 734933](https://bugs.launchpad.net/scour/+bug/734933) to avoid deleting renderable LineTo commands in paths, which happen to end where they started, if their stroke-linecap property has the value "round". -* Fix [Bug 717254](https://bugs.launchpad.net/scour/+bug/717254) to delete `<defs>` elements that become empty after unreferenced element removal. (Thanks to Jan Thor for the patch.) -* Fix [Bug 627372](https://bugs.launchpad.net/scour/+bug/627372) to future-proof the parameter passing between Scour and Inkscape. (Thanks to Bernd Feige for the patch.) -* Fix [Bug 638764](https://bugs.launchpad.net/scour/+bug/638764), which crashed Scour due to [Python Issue 2531](http://bugs.python.org/issue2531) regarding floating-point handling in ArcTo path commands. (Thanks to [Walther](https://launchpad.net/~walther-md) for investigating this bug.) -* Per [Bug 654759](https://bugs.launchpad.net/scour/+bug/654759), enable librsvg workarounds by default in Scour. -* Added ID change and removal protection options per [bug 492277](https://bugs.launchpad.net/scour/+bug/492277): `--protect-ids-noninkscape`, `--protect-ids-prefix`, `--protect-ids-list`. (Thanks to Jan Thor for this patch.) - - -## Version 0.25 (2010-07-11) - -* Fix [Bug 541889](https://bugs.launchpad.net/scour/+bug/541889) to parse polygon/polyline points missing whitespace/comma separating a negative value. Always output points attributes as comma-separated. -* Fix [Bug 519698](https://bugs.launchpad.net/scour/+bug/519698) to properly parse move commands that have line segments. -* Fix [Bug 577940](https://bugs.launchpad.net/scour/+bug/577940) to include stroke-dasharray into list of style properties turned into XML attributes. -* Fix [Bug 562784](https://bugs.launchpad.net/scour/+bug/562784), typo in Inkscape description -* Fix [Bug 603988](https://bugs.launchpad.net/scour/+bug/603988), do not commonize attributes if the element is referenced elsewhere. -* Fix [Bug 604000](https://bugs.launchpad.net/scour/+bug/604000), correctly remove default overflow attributes. -* Fix [Bug 603994](https://bugs.launchpad.net/scour/+bug/603994), fix parsing of `<style>` element contents when a CDATA is present -* Fix [Bug 583758](https://bugs.launchpad.net/scour/+bug/583758), added a bit to the Inkscape help text saying that groups aren't collapsed if IDs are also not stripped. -* Fix [Bug 583458](https://bugs.launchpad.net/scour/+bug/583458), another typo in the Inkscape help tab. -* Fix [Bug 594930](https://bugs.launchpad.net/scour/+bug/594930), In a `<switch>`, require one level of `<g>` if there was a `<g>` in the file already. Otherwise, only the first subelement of the `<g>` is chosen and rendered. -* Fix [Bug 576958](https://bugs.launchpad.net/scour/+bug/576958), "Viewbox option doesn't work when units are set", when renderer workarounds are disabled. -* Added many options: `--remove-metadata`, `--quiet`, `--enable-comment-stripping`, `--shorten-ids`, `--renderer-workaround`. - - -## Version 0.24 (2010-02-05) - -* Fix [Bug 517064](https://bugs.launchpad.net/scour/+bug/517064) to make XML well-formed again -* Fix [Bug 503750](https://bugs.launchpad.net/scour/+bug/503750) fix Inkscape extension to correctly pass `--enable-viewboxing` -* Fix [Bug 511186](https://bugs.launchpad.net/scour/+bug/511186) to allow comments outside of the root `<svg>` node - - -## Version 0.23 (2010-01-04) - -* Fix [Bug 482215](https://bugs.launchpad.net/scour/+bug/482215) by using os.linesep to end lines -* Fix unittests to run properly in Windows -* Removed default scaling of image to 100%/100% and creating a viewBox. Added `--enable-viewboxing` option to explicitly turn that on -* Fix [Bug 503034](https://bugs.launchpad.net/scour/+bug/503034) by only removing children of a group if the group itself has not been referenced anywhere else in the file - - -## Version 0.22 (2009-11-09) - -* Fix [Bug 449803](https://bugs.launchpad.net/scour/+bug/449803) by ensuring input and output filenames differ. -* Fix [Bug 453737](https://bugs.launchpad.net/scour/+bug/453737) by updated Inkscape's scour extension with a UI -* Fix whitespace collapsing on non-textual elements that had xml:space="preserve" -* Fix [Bug 479669](https://bugs.launchpad.net/scour/+bug/479669) to handle empty `<style>` elements. - - -## Version 0.21 (2009-09-27) - -* Fix [Bug 427309](https://bugs.launchpad.net/scour/+bug/427309) by updated Scour inkscape extension file to include yocto_css.py -* Fix [Bug 435689](https://bugs.launchpad.net/scour/+bug/435689) by properly preserving whitespace in XML serialization -* Fix [Bug 436569](https://bugs.launchpad.net/scour/+bug/436569) by getting `xlink:href` prefix correct with invalid SVG - - -## Version 0.20 (2009-08-31) - -* Fix [Bug 368716](https://bugs.launchpad.net/scour/+bug/368716) by implementing a really tiny CSS parser to find out if any style element have rules referencing gradients, filters, etc -* Remove unused attributes from parent elements -* Fix a bug with polygon/polyline point parsing if there was whitespace at the end - - -## Version 0.19 (2009-08-13) - -* Fix XML serialization bug: `xmlns:XXX` prefixes not preserved when not in default namespace -* Fix XML serialization bug: remapping to default namespace was not actually removing the old prefix -* Move common attributes to ancestor elements -* Fix [Bug 412754](https://bugs.launchpad.net/scour/+bug/401628): Elliptical arc commands must have comma/whitespace separating the coordinates -* Scour lengths for svg x,y,width,height,*opacity,stroke-width,stroke-miterlimit - - -## Version 0.18 (2009-08-09) - -* Remove attributes of gradients if they contain default values -* Reduce bezier/quadratic (c/q) segments to their shorthand equivalents (s/t) -* Move to a custom XML serialization such that `id`/`xml:id` is printed first (Thanks to Richard Hutch for the suggestion) -* Added `--indent` option to specify indentation type (default='space', other options: 'none', 'tab') - - -## Version 0.17 (2009-08-03) - -* Only convert to #RRGGBB format if the color name will actually be shorter -* Remove duplicate gradients -* Remove empty q,a path segments -* Scour polyline coordinates just like path/polygon -* Scour lengths from most attributes -* Remove redundant SVG namespace declarations and prefixes - - -## Version 0.16 (2009-07-30) - -* Fix [Bug 401628](https://bugs.launchpad.net/scour/+bug/401628): Keep namespace declarations when using `--keep-editor-data` (Thanks YoNoSoyTu!) -* Remove trailing zeros after decimal places for all path coordinates -* Use scientific notation in path coordinates if that representation is shorter -* Scour polygon coordinates just like path coordinates -* Add XML prolog to scour output to ensure valid XML, added `--strip-xml-prolog` option - - -## Version 0.15 (2009-07-05) - -* added `--keep-editor-data` command-line option -* Fix [Bug 395645](https://bugs.launchpad.net/scour/+bug/395645): Keep all identified children inside a defs (Thanks Frederik!) -* Fix [Bug 395647](https://bugs.launchpad.net/scour/+bug/395647): Do not remove closepath (Z) path segments - - -## Version 0.14 (2009-06-10) - -* Collapse adjacent commands of the same type -* Convert straight curves into line commands -* Eliminate last segment in a polygon -* Rework command-line argument parsing -* Fix bug in embedRasters() caused by new command-line parsing -* added `--disable-embed-rasters` command-line option - - -## Version 0.13 (2009-05-19) - -* properly deal with `fill="url("#foo")"` -* properly handle paths with more than 1 pair of coordinates in the first Move command -* remove font/text styles from shape elements (font-weight, font-size, line-height, etc) -* remove -inkscape-font-specification styles -* added `--set-precision` argument to set the number of significant digits (defaults to 5 now) -* collapse consecutive h,v coords/segments that go in the same direction - - -## Version 0.12 (2009-05-17) - -* upgraded enthought's path parser to handle scientific notation in path coordinates -* convert colors to #RRGGBB format -* added option to disable color conversion - - -## Version 0.11 (2009-04-28) - -* convert gradient stop offsets from percentages to float -* convert gradient stop offsets to integers if possible (0 or 1) -* fix bug in line-to-hv conversion -* handle non-ASCII characters (Unicode) -* remove empty line or curve segments from path -* added option to prevent style-to-xml conversion -* handle compressed svg (svgz) on the input and output -* added total time taken to the report -* Removed XML pretty printing because of [this problem](http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/). - - -## Version 0.10 (2009-04-27) - -* Remove path with empty d attributes -* Sanitize path data (remove unnecessary whitespace) -* Convert from absolute to relative path data -* Remove trailing zeroes from path data -* Limit to no more than 6 digits of precision -* Remove empty line segments -* Convert lines to horiz/vertical line segments where possible -* Remove some more default styles (`display:none`, `visibility:visible`, `overflow:visible`, - `marker:none`) - - -## Version 0.09 (2009-04-25) - -* Fix bug when removing stroke styles -* Remove gradients that are only referenced by one other gradient -* Added option to prevent group collapsing -* Prevent groups with title/desc children from being collapsed -* Remove stroke="none" - - -## Version 0.08 (2009-04-22) - -* Remove unnecessary nested `<g>` elements -* Remove duplicate gradient stops (same offset, stop-color, stop-opacity) -* Always keep fonts inside `<defs>`, always keep ids on fonts -* made ID stripping optional (disabled by default) - - -## Version 0.07 (2009-04-15) - -* moved all functionality into a module level function named 'scour' and began adding unit tests -* prevent metadata from being removed if they contain only text nodes -* Remove unreferenced pattern and gradient elements outside of defs -* Removal of extra whitespace, pretty printing of XML - - -## Version 0.06 (2009-04-13) - -* Prevent error when stroke-width property value has a unit -* Convert width/height into a viewBox where possible -* Convert all referenced rasters into base64 encoded URLs if the files can be found - - -## Version 0.05 (2009-04-07) - -* Removes unreferenced elements in a `<defs>` -* Removes all inkscape, sodipodi, adobe elements -* Removes all inkscape, sodipodi, adobe attributes -* Remove all unused namespace declarations on the document element -* Removes any empty `<defs>`, `<metadata>`, or `<g>` elements -* Style fix-ups: -* Fixes any style properties like this: `style="fill: url(#linearGradient1000) rgb(0, 0, 0);"` - * Removes any style property of: `opacity: 1;` - * Removes any stroke properties when `stroke=none` or `stroke-opacity=0` or `stroke-width=0` - * Removes any fill properties when `fill=none` or `fill-opacity=0` - * Removes all fill/stroke properties when `opacity=0` - * Removes any `stop-opacity: 1` - * Removes any `fill-opacity: 1` - * Removes any `stroke-opacity: 1` -* Convert style properties into SVG attributes diff --git a/Makefile b/Makefile index 532618a..2fb7802 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,15 @@ all: clean install install: - python3 setup.py install + python setup.py install clean: rm -rf build rm -rf dist rm -rf scour.egg-info - rm -rf .tox - rm -f .coverage* - rm -rf htmlcov - find . -name "*.pyc" -type f -exec rm -f {} \; - find . -name "*__pycache__" -type d -prune -exec rm -rf {} \; publish: clean - python3 setup.py register - python3 setup.py sdist upload - -check: test flake8 - - - -test: - python3 test_scour.py - -test_version: - PYTHONPATH=. python3 -m scour.scour --version - -test_help: - PYTHONPATH=. python3 -m scour.scour --help - -flake8: - flake8 --max-line-length=119 - -coverage: - coverage run --source=scour test_scour.py - coverage html - coverage report + python setup.py register + python setup.py sdist upload + python setup.py bdist_egg upload + python setup.py bdist_wininst upload diff --git a/README.md b/README.md index c5c0bc8..711de61 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,52 @@ # Scour -[![PyPI](https://img.shields.io/pypi/v/scour.svg)](https://pypi.python.org/pypi/scour "Package listing on PyPI") -  -[![Build status](https://img.shields.io/travis/scour-project/scour.svg)](https://travis-ci.org/scour-project/scour "Build status (via TravisCI)") -[![Codecov](https://img.shields.io/codecov/c/github/scour-project/scour.svg)](https://codecov.io/gh/scour-project/scour "Code coverage (via Codecov)") +Scour is a Python module that takes an input SVG and outputs a cleaner, +more concise SVG file. The goal is that authors will use this script after +editing the file in a GUI editor such as Inkscape or Adobe Illustrator. ---- +Scour was started as a vehicle for me to learn Python. In addition, the goal +is to reduce the amount of time I spend in cleaning up files I find on sites +such as openclipart.org -Scour is an SVG optimizer/cleaner written in Python that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data. +Ideas are pulled from three places: -It can be used to create streamlined vector graphics suitable for web deployment, publishing/sharing or further processing. + * my head + * Sam Ruby's SVG Tidy script: http://intertwingly.net/code/svgtidy/svgtidy.rb + * Inkscape's proposal for a 'cleaned SVG': http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG -The goal of Scour is to output a file that renders identically at a fraction of the size by removing a lot of redundant information created by most SVG editors. Optimization options are typically lossless but can be tweaked for more aggressive cleaning. +Regards, -Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). +Jeff Schiller, 2009-04-06 -Scour was originally developed by Jeff "codedread" Schiller and Louis Simard in in 2010. -The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Patrick "Ede_123" Storz. +codedread@gmail.com -This fork was created by Alexander Olsson ([alex@aleon.se](mailto:alex@aleon.se?subject=Scour)) at Aleon Apps. +http://blog.codedread.com/ -## Installation - -Scour requires [Python](https://www.python.org) 2.7 or 3.4+. Further, for installation, [pip](https://pip.pypa.io) should be used. - -To install this fork: - -```console -sudo make -``` - -## Extension -Place the modified extension files in the Inkscape extension directory -```console -sudo cp extension/* /usr/share/inkscape/extensions/ -``` +http://www.codedread.com/scour/ ## Usage Standard: -```console -scour -i input.svg -o output.svg -``` + scour -i mysvg.svg -o mysvg_opt.svg -Better (for older versions of Internet Explorer): +Better (this works in IE which needs Viewbox): -```console -scour -i input.svg -o output.svg --enable-viewboxing -``` + scour -i mysvg.svg -o mysvg_opt.svg --enable-viewboxing -Maximum scrubbing: +Maximum: -```console -scour -i input.svg -o output.svg --enable-viewboxing --enable-id-stripping \ - --enable-comment-stripping --shorten-ids --indent=none -``` + scour -i mysvg.svg -o mysvg_opt.svg --enable-viewboxing --enable-id-stripping \ + --enable-comment-stripping --shorten-ids --indent=none -Maximum scrubbing and a compressed SVGZ file: +Maximum + Compress: -```console -scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ - --enable-comment-stripping --shorten-ids --indent=none -``` + scour -i mysvg.svg -o mysvg_opt.svgz --enable-viewboxing --enable-id-stripping \ + --enable-comment-stripping --shorten-ids --indent=none -Remove scientific notation from path data: +## Notes -```console -scour -i input.svg -o output.svgz --nonsci-output -``` +Packaging from [sources](http://www.codedread.com/scour/) retrieved on 2013/20/22: + * done by Tavendo GmbH, Tobias Oberstein + * license same as upstream (Apache 2.0) diff --git a/extension/output_scour.inx b/extension/output_scour.inx deleted file mode 100644 index b6ce893..0000000 --- a/extension/output_scour.inx +++ /dev/null @@ -1,132 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> - <name>Optimized SVG Output</name> - <id>org.inkscape.output.scour_inkscape</id> - - <param name="tab" type="notebook"> - <page name="Options" gui-text="Options"> - <param gui-text="Number of significant digits for coordinates:" - gui-description="Specifies the number of significant digits that should be output for coordinates. Note that significant digits are *not* the number of decimals but the overall number of digits in the output. For example if a value of "3" is specified, the coordinate 3.14159 is output as 3.14 while the coordinate 123.675 is output as 124." - name="set-precision" type="int" min="1">5</param> - <spacer/> - <param gui-text="Shorten color values" - gui-description="Convert all color specifications to #RRGGBB (or #RGB where applicable) format." - name="simplify-colors" type="bool">true</param> - <param gui-text="Convert CSS attributes to XML attributes" - gui-description="Convert styles from style tags and inline style="" declarations into XML attributes." - name="style-to-xml" type="bool">true</param> - <spacer/> - <param gui-text="Collapse groups" - gui-description="Remove useless groups, promoting their contents up one level. Requires "Remove unused IDs" to be set." - name="group-collapsing" type="bool">true</param> - <param gui-text="Create groups for similar attributes" - gui-description="Create groups for runs of elements having at least one attribute in common (e.g. fill-color, stroke-opacity, ...)." - name="create-groups" type="bool">true</param> - <spacer/> - <param gui-text="Keep editor data" - gui-description="Don't remove editor-specific elements and attributes. Currently supported: Inkscape, Sodipodi and Adobe Illustrator." - name="keep-editor-data" type="bool">false</param> - <param gui-text="Remove scientific notation" - gui-description="Remove scientific notation from path data." - name="nonsci-output" type="bool">false</param> - <param gui-text="Keep unreferenced definitions" - gui-description="Keep element definitions that are not currently used in the SVG" - name="keep-unreferenced-defs" type="bool">false</param> - <spacer/> - <param gui-text="Work around renderer bugs" - gui-description="Works around some common renderer bugs (mainly libRSVG) at the cost of a slightly larger SVG file." - name="renderer-workaround" type="bool">true</param> - </page> - <page name="Output" gui-text="SVG Output"> - <label appearance="header">Document options</label> - <param gui-text="Remove the XML declaration" - gui-description="Removes the XML declaration (which is optional but should be provided, especially if special characters are used in the document) from the file header." - name="strip-xml-prolog" type="bool">false</param> - <param gui-text="Remove metadata" - gui-description="Remove metadata tags along with all the contained information, which may include license and author information, alternate versions for non-SVG-enabled browsers, etc." - name="remove-metadata" type="bool">false</param> - <param gui-text="Remove comments" - gui-description="Remove all XML comments from output." - name="enable-comment-stripping" type="bool">false</param> - <param gui-text="Embed raster images" - gui-description="Resolve external references to raster images and embed them as Base64-encoded data URLs." - name="embed-rasters" type="bool">true</param> - <param gui-text="Enable viewboxing" - gui-description="Set page size to 100%/100% (full width and height of the display area) and introduce a viewBox specifying the drawings dimensions." - name="enable-viewboxing" type="bool">false</param> - <spacer/> - <label appearance="header">Pretty-printing</label> - <param gui-text="Format output with line-breaks and indentation" - gui-description="Produce nicely formatted output including line-breaks. If you do not intend to hand-edit the SVG file you can disable this option to bring down the file size even more at the cost of clarity." - name="line-breaks" type="bool">true</param> - <param gui-text="Indentation characters:" - gui-description="The type of indentation used for each level of nesting in the output. Specify "None" to disable indentation. This option has no effect if "Format output with line-breaks and indentation" is disabled." - name="indent" type="optiongroup" appearance="combo"> - <option value="space">Space</option> - <option value="tab">Tab</option> - <option context="Indent" value="none">None</option> - </param> - <param gui-text="Depth of indentation:" - gui-description="The depth of the chosen type of indentation. E.g. if you choose "2" every nesting level in the output will be indented by two additional spaces/tabs." - name="nindent" type="int">1</param> - <param gui-text="Strip the "xml:space" attribute from the root SVG element" - gui-description="This is useful if the input file specifies "xml:space='preserve'" in the root SVG element which instructs the SVG editor not to change whitespace in the document at all (and therefore overrides the options above)." - name="strip-xml-space" type="bool">false</param> - </page> - <page name="IDs" gui-text="IDs"> - <param gui-text="Remove unused IDs" - gui-description="Remove all unreferenced IDs from elements. Those are not needed for rendering." - name="enable-id-stripping" type="bool">true</param> - <spacer/> - <param gui-text="Shorten IDs" - gui-description="Minimize the length of IDs using only lowercase letters, assigning the shortest values to the most-referenced elements. For instance, "linearGradient5621" will become "a" if it is the most used element." - name="shorten-ids" type="bool">false</param> - <param gui-text="Prefix shortened IDs with:" - gui-description="Prepend shortened IDs with the specified prefix." - name="shorten-ids-prefix" type="string"></param> - <spacer/> - <param gui-text="Preserve manually created IDs not ending with digits" - gui-description="Descriptive IDs which were manually created to reference or label specific elements or groups (e.g. #arrowStart, #arrowEnd or #textLabels) will be preserved while numbered IDs (as they are generated by most SVG editors including Inkscape) will be removed/shortened." - name="protect-ids-noninkscape" type="bool">true</param> - <param gui-text="Preserve the following IDs:" - gui-description="A comma-separated list of IDs that are to be preserved." - name="protect-ids-list" type="string"></param> - <param gui-text="Preserve IDs starting with:" - gui-description="Preserve all IDs that start with the specified prefix (e.g. specify "flag" to preserve "flag-mx", "flag-pt", etc.)." - name="protect-ids-prefix" type="string"></param> - </page> - <page name="About" gui-text="About"> - <hbox> - <image>output_scour.svg</image> - <spacer/> - <vbox> - <spacer/> - <label>Optimized SVG Output is provided by</label> - <label appearance="header" indent="1">Scour - An SVG Scrubber</label> - <spacer/> - <label>For details please refer to</label> - <label appearance="url" indent="1">https://github.com/scour-project/scour</label> - </vbox> - </hbox> - <spacer size="expand"/> - <hbox> - <label>This version of the extension is designed for</label> - <label>Scour 0.31+</label> - </hbox> - <param name="scour-version" type="string" gui-hidden="true">0.31</param> <!-- this parameter is checked programmatically in the extension to show a warning --> - <param gui-text="Show warnings for older versions of Scour" - name="scour-version-warn-old" type="bool">true</param> - </page> - </param> - - <output> - <extension>.svg</extension> - <mimetype>image/svg+xml</mimetype> - <filetypename>Optimized SVG (*.svg)</filetypename> - <filetypetooltip>Scalable Vector Graphics</filetypetooltip> - </output> - - <script> - <command location="inx" interpreter="python">output_scour.py</command> - </script> -</inkscape-extension> diff --git a/extension/output_scour.py b/extension/output_scour.py deleted file mode 100644 index eebfb8a..0000000 --- a/extension/output_scour.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -""" -Run the scour module on the svg output. -""" - - -import inkex -from inkex.localization import inkex_gettext as _ - -try: - from packaging.version import Version -except ImportError: - raise inkex.DependencyError( - _( - """Failed to import module 'packaging'. -Please make sure it is installed (e.g. using 'pip install packaging' -or 'sudo apt-get install python3-packaging') and try again. -""" - ) - ) - -try: - import scour - from scour.scour import scourString -except ImportError: - raise inkex.DependencyError( - _( - """Failed to import module 'scour'. -Please make sure it is installed (e.g. using 'pip install scour' - or 'sudo apt-get install python3-scour') and try again. -""" - ) - ) - - -class ScourInkscape(inkex.OutputExtension): - """Scour Inkscape Extension""" - - # Scour options - def add_arguments(self, pars): - pars.add_argument("--tab") - pars.add_argument("--simplify-colors", type=inkex.Boolean, dest="simple_colors") - pars.add_argument("--style-to-xml", type=inkex.Boolean) - pars.add_argument( - "--group-collapsing", type=inkex.Boolean, dest="group_collapse" - ) - pars.add_argument("--create-groups", type=inkex.Boolean, dest="group_create") - pars.add_argument("--enable-id-stripping", type=inkex.Boolean, dest="strip_ids") - pars.add_argument("--shorten-ids", type=inkex.Boolean) - pars.add_argument("--shorten-ids-prefix") - pars.add_argument("--embed-rasters", type=inkex.Boolean) - pars.add_argument( - "--keep-unreferenced-defs", type=inkex.Boolean, dest="keep_defs" - ) - pars.add_argument("--keep-editor-data", type=inkex.Boolean) - pars.add_argument("--nonsci-output", type=inkex.Boolean) - pars.add_argument("--remove-metadata", type=inkex.Boolean) - pars.add_argument("--strip-xml-prolog", type=inkex.Boolean) - pars.add_argument("--set-precision", type=int, dest="digits") - pars.add_argument("--indent", dest="indent_type") - pars.add_argument("--nindent", type=int, dest="indent_depth") - pars.add_argument("--line-breaks", type=inkex.Boolean, dest="newlines") - pars.add_argument( - "--strip-xml-space", type=inkex.Boolean, dest="strip_xml_space_attribute" - ) - pars.add_argument("--protect-ids-noninkscape", type=inkex.Boolean) - pars.add_argument("--protect-ids-list") - pars.add_argument("--protect-ids-prefix") - pars.add_argument("--enable-viewboxing", type=inkex.Boolean) - pars.add_argument( - "--enable-comment-stripping", type=inkex.Boolean, dest="strip_comments" - ) - pars.add_argument("--renderer-workaround", type=inkex.Boolean) - - # options for internal use of the extension - pars.add_argument("--scour-version") - pars.add_argument("--scour-version-warn-old", type=inkex.Boolean) - - def save(self, stream): - # version check if enabled in options - if self.options.scour_version_warn_old: - scour_version = scour.__version__ - scour_version_min = self.options.scour_version - if Version(scour_version) < Version(scour_version_min): - raise inkex.AbortExtension( - f""" -The extension 'Optimized SVG Output' is designed for Scour {scour_version_min} or later but you're - using the older version Scour {scour_version}. - -Note: You can permanently disable this message on the 'About' tab of the extension window.""" - ) - del self.options.scour_version - del self.options.scour_version_warn_old - - # do the scouring - stream.write(scourString(self.svg.tostring(), self.options).encode("utf8")) - - -if __name__ == "__main__": - ScourInkscape().run() diff --git a/extension/output_scour.svg b/extension/output_scour.svg deleted file mode 100644 index 8f1b941..0000000 --- a/extension/output_scour.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="100" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg"> - <path d="m84.5 51.5-12.6 0.504c-8.9e-4 -0.00454-0.00106-0.00914-0.00195-0.0137-0.00104 0.00426-9.21e-4 0.0094-0.00195 0.0137l-15.5 0.623c-0.0092-0.0168-0.016-0.0361-0.0254-0.0527-0.00207 0.0177-0.0019 0.037-0.00391 0.0547l-1.95 0.0781c-0.0314-0.0743-0.0854-0.127-0.186-0.133v0.141l-7.76 0.311c-0.0111-0.0184-0.02-0.0426-0.0312-0.0605 0.00141 0.0201 0.00632 0.0423 0.00781 0.0625l-26.5 1.06-3.74 6.81 2.86 1.36 1.02 1.43 0.887 0.271 1.56 0.41 0.273-0.479 0.953-0.135h2.72l1.29 0.0684 1.22-0.682 0.48 0.525 0.609 0.223 0.887-0.203 1.02-0.205 0.406 0.381 1.3-0.176 1.84-0.273 0.953-0.271 1.02 0.0684 1.09 0.137 0.682 0.0664 0.408-0.477 0.691 0.537 2.1-0.537 1.36 0.273 1.16-0.137 1.8-0.184 0.24-0.361 3.4 0.408 2.18-0.816 1.09 0.408 2.45-0.613h4.46l0.445-0.34 1.02-0.205 0.953 0.137 1.29-0.34 1.34 0.107c0.00197 0.0173 0.00182 0.0397 0.00391 0.0566 0.0223-0.0242 0.0276-0.032 0.0469-0.0527l1.13 0.0918 1.36 0.205 0.387-0.439 2.95 0.234 0.748 0.156 0.584-0.195 1.39-0.369 2.04 0.477 0.848 0.283 1.4-0.623 0.547 0.232 0.543-0.369 5.88 0.369 0.656-0.0957-1.7-9.74zm-69.2 0.297c-0.493-0.0229-0.788 1.14-0.584 1.17 0.186-0.398 0.398-0.77 0.584-1.17zm1.75 0.584c-0.342 0.826-0.871 1.47-1.17 2.34 0.626 0.12 0.267-0.702 0.779-0.195-5e-3 -0.727 0.76-1.69 0.391-2.14zm-0.779 0.193c-0.547 1.33-1.42 2.35-1.95 3.7 1.01-0.744 1.76-2.78 1.95-3.7zm-0.465 0.148c-0.126-0.05-0.42 0.311-0.119 0.631 0.211-0.423 0.195-0.601 0.119-0.631zm-1.87 0.0469c-0.0439 0.426-0.88 1.05-0.391 1.36-0.0281-0.498 0.863-1.06 0.391-1.36zm71.3 0.232c-0.059-0.034-0.0993 0.0425-0.0879 0.352 1.16 2.15 1.33 5.28 2.72 7.2-0.562-2.55-1.39-4.84-2.34-7.01-0.052-0.124-0.202-0.49-0.301-0.547zm-67.6 0.742c-0.835 1.07 0.486 0.719 0 0zm1.27 0.121c-0.0209-0.00292-0.052 0.0184-0.0977 0.0723-0.281 0.726-0.26 0.492-0.584 0 0.151 0.515-0.72 1.51-0.195 1.17 0.0412-0.218 0.215-0.302 0.389-0.389 0.116 0.397-0.365 1.07 0 0.584 0.226-0.169 0.635-1.42 0.488-1.44zm-1.46 1.05c-0.513-0.0589-0.639 0.268-0.584 0.777 0.513 0.0589 0.639-0.268 0.584-0.777zm-2.21 1.29c-0.0245 2.56e-4 -0.0629 0.0204-0.121 0.0664-0.041 0.367-0.782 1.01-0.391 1.17 0.113-0.242 0.683-1.24 0.512-1.24z" fill="#f6e7a1"/> - <path d="m78.9 35.6c-0.411-0.056-0.94 0.163-1.36 0.195-6.84 0.524-14.7 0.699-22.8 1.17-14 0.812-28.8 1.07-41.1 2.14-0.0167 2.54 0.323 3.93 0.195 6.81 0.657 0.901 1.29 1.83 1.56 3.12-1.3 2.27-2.86 4.28-4.28 6.43 0.811 1.33 1.81 2.47 2.73 3.7 1.55-0.0584 1.65 1.33 2.53 1.95 0.809-0.614 1.24-2.09 1.56-2.73 0.662-1.32 1.18-2.69 1.95-3.7-0.412 1.49-0.95 2.35-1.56 3.5-0.827 1.57-2.33 3.59-0.777 4.48 1.15-0.244 1.44 0.493 2.53 1.17 0.109-1.58 1.13-3.28 0.973-4.48-0.574 1.18-0.783 2.72-1.75 3.51 0.872-2.47 1.2-4.42 2.14-6.62 0.211 0.499-0.592 0.93 0 1.17 0.302-0.866 0.392-1.95 1.17-2.34-0.356 3.75-1.28 5.82-2.34 8.96 0.762-0.193 1.13 0.43 1.56 0 0.146-3.74 0.93-6.28 1.56-9.15 0.23 0.381-0.181 1.8-0.195 2.92-0.0045 0.355 0.196 2.8 0 1.17-0.128-1.07-0.163 0.525-0.193 0.779-0.191 1.6-0.63 2.93-0.779 4.48 0.613 0.0939 0.441-0.597 1.17-0.389 0.122-0.592-0.248-1.68 0.195-1.95 0.155 0.754-0.497 2.31 0.779 1.95-0.972-0.939-0.114-2.68-0.584-3.89-0.0436 0.151-0.00608 0.385-0.195 0.391 0.321-1.38-0.0526-3.73 0.584-5.26 0.333 2.26 0.298 5.71 0 7.98 0.701-0.117 0.574 0.594 1.17 0.584 0.371-3.13 0.502-6.51 1.56-8.96 0.322 2.8-1.19 5.9-0.777 9.15 0.757 0.269 2.74 0.24 2.91-0.908 0.00126-0.0852 0.00253-0.171 0.00781-0.26 0.00732 0.0922 4e-3 0.179-0.00781 0.26-0.00558 0.376 0.0493 0.69 0.396 0.713-0.031-1.27 0.26-2.85-0.777-3.12-0.156 0.805 0.706 0.592 0.584 1.36-0.307-0.0176-0.152-0.496-0.584-0.389-0.121 0.529 0.247 1.55-0.195 1.75-0.0605-1.89 0.0118-0.482-0.584 0 0.434-2.81 0.119-6.37 0.973-8.76 0.328 1.95-0.583 4.53 0.195 5.65 0.129-1.95-0.258-4.41 0.195-6.04 0.401 1.03 0.248 2.61 0.584 3.7 0.264-0.948-0.134-2.05 0.389-2.34-0.0319 1.71 0.101 2.41 0.195 3.7 0.153 0.804 0.492-1.81 0.195-2.14 0.878 0.472 0.0244-2.25 0.973-2.34-0.123 1.96-0.463 4.41-0.584 7.59-0.57 0.284-0.947 1.4 0.195 1.36 0.348-3.29 0.287-6.98 1.36-9.54-0.148 3.42-0.457 6.68-1.17 9.54 0.55-0.161 0.506 0.275 0.975 0.195 0.284-0.874-0.131-1.64 0-2.34 0.0441-0.235 0.375-0.192 0.389-0.391-5e-3 0.0753-0.367-0.157-0.389-0.389-0.109-1.19 0.556-1.5 0.584-1.75-0.00214-0.257-0.342-0.179-0.391-0.391 0.956 0.0474-0.0676-1.88 0.584-2.14 0.0329 0.291-0.069 0.719 0.195 0.779 0.397-1.03-0.163-3.02 0.584-3.7 0.076 3.4-0.482 6.49-0.391 9.35-0.257-0.00262-0.177-0.342-0.389-0.391-0.141 0.443-0.175 0.995-0.391 1.36 1.01-0.384 1.56 0.0746 2.53-0.389 0.428-0.553-0.352-1.79-0.389-2.14-0.0591-0.572 0.192-7 0.389-7.01 0.133-0.00477-0.293 2.11 0.195 0.973 0.19-0.442-0.276-1.71 0.195-1.56 0.0429 1.99 0.383 4.16 0.584 5.45 0.197 1.26-0.156 2.78 0.584 3.89 0.0956-0.874-0.0993-1.46-0.195-2.14 0.356 0.0765 0.25 0.258 0.389-0.195 0.494-1.61 0.319-5.87 1.17-7.01 0.0164 2.03-0.489 3.53-0.391 5.64 0.597-0.931 0.19-4.27 0.975-5.26-0.291 2.21 0.367 4.42-0.195 6.23-0.0303 0.0968-0.303-0.37-0.389 0.195-0.163 1.08-0.417 2.54 0.193 2.92 0.53 0.0403 0.365-0.779 0.391-1.17 0.165-2.55 0.42-5.85 1.17-8.18 0.153 2.03-0.506 3.51 0.193 4.87 0.131-2.22-0.28-4.04 0.391-5.45 0.0396 0.368-0.131 1.92 0.195 1.36 0.106-0.283-0.229-1.01 0.193-0.975 0.774 0.411 0.36 2.06-0.193 2.34 0.487-0.0243 0.608-0.373 0.777 0.193 0.109 0.63 0.654 1.26 0.779 2.14 0.218 1.53-0.458 3.66 0.195 4.67 0.116-1.28 0.28-4.49 0.344-6.29-0.053 0.155-0.0894-2.05 0.24-2.08 0.00405-4.77e-4 -0.0488 0.938 0.193 0.777 0.0276-0.751-0.151-1.71 0.391-1.95-0.261 3.24-0.207 6.8-0.975 9.54 0.403-0.116 1.28 0.242 1.36-0.193-0.706-0.975 0.362-2.88-0.195-3.7-0.138 0.705-0.0731 1.61-0.389 2.14 0.24-2.62 0.0955-5.62 0.779-7.79 0.55 0.195 0.488 0.284 0.779 0.389-0.127 3.18 0.0117 6.63-0.779 9.15 1.49 0.29 0.922-1.21 0.973-1.95 0.152-2.23 0.239-5.84 0.779-8.18 0.361 3.38 1.31 6.03 0.779 9.73 1.01-0.558 2.32 0.301 3.7 0.195 1.09-0.0834 1.92-1.06 2.73-0.584 0.111-0.342-0.237-1.15 0.193-1.17 0.424 0.407-0.097 0.557 0 1.17 2.81-0.0119 3.95-0.632 7.21-0.584v-3.51c-0.118-1.01-0.281 0.393-0.195 0.779v1.95c-0.613-0.631-0.145 0.507-0.584 0.584-0.0713-0.523 0.178-2.37-0.195-1.95 0.0272 0.806-0.0531 1.5-0.389 1.95 0.227-3.42 0.271-5.83 1.17-8.57 0.256 0.603-0.577 1.08 0 1.36 0.108-0.347 0.0996-0.808 0.389-0.973 0.214 0.992-0.53 1.73 0.195 2.14 0.127-1.04-0.257-2.59 0.193-3.31 0.162 1.71 0.488 4.13 0.586 6.23 0.0503 1.09-0.572 3.31 0.779 3.12-0.0305-0.866-1.06-1.45-0.391-3.12 0.107 0.283-0.227 1.01 0.195 0.973 0.361-1.5-0.488-2.84-0.584-4.09-0.0388-0.503-0.321-1.56 0.389-1.95-0.109 1.79 0.751 4.14 0.973 6.23 0.452-2.14 0.324-4.87 0.975-6.81 0.167 3.03-0.492 4.41-0.391 7.59-0.766-0.312-0.0675 0.84-0.584 0.777-0.0946-0.23-0.207-0.441-0.389-0.584-0.186 1.97 2.86 1.09 3.31 0.584 0.715 0.577 1.15 0.235 2.34 0.195-0.245-3.96-1.14-5.75-0.779-8.96 0.0117 1.61 0.384 2.86 0.779 4.09-0.0255 0.256-0.0307 1.41 0.193 0.779 0.0245-1.34 0.0891-2.64 0.391-3.7-0.0593 3.12-0.362 5.63-0.391 7.4 1.35 0.649 2.3 0.498 3.51 0.389-0.487-1.91 0.135-4.77 0.389-6.04 0.247-1.23-0.171-2.63 0.584-3.51 0.546 1.55 0.854 3.89 0.975 5.84 0.0355 0.57 0.703 1.52-0.195 1.75-0.081-1.19-0.141-2.91-0.584-4.48 0.2 2.39-0.0405 3.13 0 5.84-0.342-0.28-0.443-0.179-0.389 0.389 0.444 0.619 1.03-0.252 1.56-0.195 0.0956-0.614-0.351-0.686-0.195-1.36 0.316 0.0553 0.305 0.286 0.391-0.195 0.429-2.41-0.329-5.39 0.779-6.62 0.0953 3.83-0.0694 5.11-0.391 8.57 0.783-0.134 1.11 0.187 1.75 0.195 0.431-1.83-0.61-5.63 0-8.18 0.164 0.745 0.255 1.56 0.195 2.53 1.13-0.587-0.379-2.77 0.584-3.31 0.208 3.28-0.0247 6.02-0.389 8.96 0.665-0.0493 0.935-0.494 1.75-0.391-0.324-2.96-0.751-6.09-0.195-8.96 0.349 2.38 1.64 4.85 1.17 7.4-0.286-1.92-0.636-3.78-1.17-5.45 0.308 2.47 0.384 4.63 0.391 7.01 2.58-0.19 4.01 0.398 6.42 0-0.0322-2.86-1.44-5.51-0.584-7.59 0.631 3.03 0.974 4.95 0.779 7.59 1.03-0.0217 1.65-0.39 3.12-0.389-0.406-3.49-2.4-7.07-3.5-10.5 0.471-0.373 1.56-0.123 2.34-0.195 1.74-3.19 0.693-6.95-1.56-9.73-1.31-1.62-5.37-4.62-7.2-4.87zm0.973 0.779c-2.09 0.742-4.43 0.482-6.62 0.584-14.6 0.679-30.9 1.71-45 2.34 10.3-0.00382 20.6-0.925 31-1.36 3.42-0.144 6.89-0.506 10.1-0.389 3.75 0.136-1.31 0.358-1.95 0.389-11.3 0.535-24.1 1.06-35.2 1.56 16.2-0.105 33.1-1.36 48.9-2.14-0.555 0.774-1.59 0.333-2.34 0.391-11.8 0.904-24.2 1.08-35 1.75 13.2 0.0322 25.6-1.19 38.9-1.56-11.2 1.06-23.3 1.14-34.7 1.95 10.8-0.0257 22.1-1.08 33.7-1.36 0.348-0.0551 0.398 0.185 0.193 0.195-1.72-0.00667-2.6 0.106-4.48 0.195 1.9 0.601 4.1-0.178 6.23 0-16.1 1.32-32.5 1.51-48.1 2.72 14.9-0.175 30.6-1.86 45.2-2.14 0.594-0.0114 3.23-0.102 2.34 0.193-15.8 0.773-33 1.47-49.3 2.53 15.5-0.627 29.6-1.22 43.8-1.95 2.27-0.117 4.7-0.654 6.62-0.389 0.334 0.0462 0.411 0.107 0.195 0.195-22.8 1.21-44.8 2.25-67.8 3.31 4.55-0.786 9.43-0.404 14.4-0.973-4.56-0.207-10.1 0.295-14.8 0.584 2.02-0.708 4.94-0.516 7.59-0.584-0.819-0.569-2.47-0.268-3.89-0.195-1.45 0.0739-2.99 0.154-4.09 0.391 0.245-0.759 1.26-0.339 1.75-0.391 7.91-0.827 18.6-0.949 27.5-1.36 9.98-0.464 20.2-1.26 30.2-1.56-16.5 0.356-34.8 1.36-51.2 2.14-0.945 0.0448-2.58 0.597-3.31 0 0.011 0.00882 0.202-0.119-0.193-0.195-1.49-0.286-3.36 0.235-4.87 0.389-0.151-0.753 0.972-0.359 1.17-0.389 7.4-1.14 17.9-0.654 26.5-1.36-0.956-0.526-2.2-0.253-3.31-0.195-8.28 0.434-16.8 0.947-24.3 1.17 2.49-0.689 6.09-0.273 8.76-0.779-1.32-0.218-4.18 0.0249-6.23 0.193 0.562-0.759 1.75-0.343 2.53-0.389 2.34-0.136 5.07-0.341 7.4-0.389-4.46-0.394-8.38 0.32-12.9 0.389 1.47-0.409 2.58-0.499 4.09-0.584 19.9-1.12 40.7-1.51 60.5-2.92zm0.195 0.584c-2.41 0.769-6.07 0.294-8.76 0.779 0.216-0.752 1.79-0.304 2.92-0.391 1.41-0.108 4.16-0.291 5.84-0.389zm-2.92 1.95c-0.861 0.288-1.4-0.265-1.75 0.389 0.63-0.0839 1.68 0.257 1.75-0.389zm-45.2 0.584c-0.723 0.315-1.91 0.16-2.92 0.193-0.331-0.0436-0.482 0.22 0 0.195 0.883-0.22 2.67 0.465 2.92-0.389zm-4.22 0.223c-0.195 0.00524-0.442 0.0981-0.643 0.166h-1.56c-2.37 0.198 1.66 0.235 2.53 0.195 0.00703-0.284-0.135-0.367-0.33-0.361zm19.6 0.125c-0.673-0.00186 0.00284 0.561 0.377 0.041-0.156-0.0281-0.281-0.0408-0.377-0.041zm-0.598 0.041c-7.59 0.391-15.8 0.914-23.6 1.17-0.49 0.016-1.46-0.355-1.75 0.389 5.72-0.283 12.1-0.709 17.5-0.973 2.68-0.13 5.34 0.151 7.79-0.584zm-29.4 0.154c0.318 0.0103 0.294 0.0844-0.605 0.234-0.471 0.0784-0.476 0.266-1.36 0.195-0.0435-0.286 1.44-0.447 1.97-0.43zm66.6 0.408c0.246 0.0191 0.534 0.0775 0.705 0.0215-0.0243 0.43-0.827 0.0835-1.17 0.195 0.0122-0.215 0.219-0.236 0.465-0.217zm-0.854 0.0215c-3.08 0.722-6.23 0.25-9.35 0.779 2.61 0.466 5.3-0.486 8.76-0.195 1.79 0.15-0.0346 0.166-0.391 0.195-7.82 0.652-18.1 0.974-26.5 1.36-12.3 0.572-26.2 0.999-37.6 1.95 16.3-0.727 33.8-1.47 50.8-2.34 3.53-0.179 10.7-1.06 15.8-0.777 2.54 0.14-2.54 0.306-3.7 0.389-20.3 1.45-41.7 1.64-62.5 3.12 0.73 0.54 2.84-0.0633 3.31 0.195-2.28 0.719-3.56 0.183-4.87-1.17 5.37-0.75 10.9-0.938 16.4-1.17 16.5-0.697 33-1.52 49.8-2.34zm1.15 0.367c0.28 0.0203 0.599 0.0814 0.801 0.0234-0.0841 0.435-0.96 0.0775-1.36 0.193 0.0419-0.218 0.283-0.237 0.562-0.217zm-10.7 0.412c-14.7 0.589-27.4 1.3-42.8 1.95-4.36 0.184-8.77 0.295-13.2 0.973 18.2-1.04 37.3-1.57 56.1-2.92zm-38.7 0.193c-3.72 0.333-7.5 0.398-10.9 0.584 2.48 0.442 6.41-0.0578 8.76-0.193 0.819-0.0474 2.08 0.366 2.53-0.391h-0.391zm-15.4 0.00195c-0.568 0.859-2.75 0.11-3.7 0.584 0.201-0.453 2.6-0.385 3.7-0.584zm64.4 0.566c1.14 0.0312 1.69 0.179 0.0918 0.211-1.57 0.0315-4.85 0.365-6.04 0.195 1.26-0.43 3.34-0.0328 4.67-0.389 0.45-0.0242 0.893-0.028 1.27-0.0176zm-51.5 0.211c-2.45 0.0491 0.0431 0.467 1.17 0-0.443 0.0072-0.703-0.0093-1.17 0zm53.7 0.195c-21.5 1.66-43.8 2.03-66.6 3.7 0.766-0.827 2.46-0.5 4.09-0.584 20.1-1.03 40.5-2.13 62.5-3.12zm-13.2 0.195c0.158-0.0076 4.42 0.0822 4.67 0-1.61 0.535-5.47 0.107-7.2 0.193-11.7 0.583-26.9 1.4-40.3 2.14-2.02 0.112-6.48 0.578-7.01 0.195 0.0527 0.0381 4.46-0.316 6.04-0.391 6.14-0.29 12.6-0.649 18.9-0.973 9.15-0.47 17.1-0.797 24.9-1.17zm12.9 0.287c0.248-0.017 0.429 0.0328 0.387 0.297-19.4 0.681-38.6 2.08-58.4 2.92-2.18 0.0918-4.11 0.111-6.23 0.389-0.454 0.0596-0.909 0.322-1.36 0 0.238-0.406 0.949-0.356 1.36-0.389 9.57-0.75 20.2-1.02 29.8-1.56 10.2-0.563 20.6-1.03 31.2-1.56 0.273-0.0138 1.4 0.0067 2.53 0 0.219-0.00143 0.533-0.0846 0.781-0.102zm1.36 0.49c-10 0.993-20.6 1.22-30.8 1.75-9.64 0.505-19.5 0.99-28.8 1.56-0.104-0.771 0.952-0.368 1.17-0.389 8.21-0.77 18.4-0.994 27.1-1.36 10.5-0.446 21-1.14 31.3-1.56zm-0.162 0.574c0.358 9.23e-4 0.673 0.167 0.746 0.789-0.149 0.823-1.14 0.346-1.56 0.389-3.91 0.393-9.9 0.556-14.6 0.779-16.4 0.779-34.7 2.11-50.4 2.34 0.285-1.26 2.49-0.874 3.7-0.973 8.83-0.723 18.9-0.919 28.4-1.36 7.84-0.365 15.7-0.993 24.1-1.36 2.9-0.128 5.88 0.0632 8.57-0.389 0.247-0.0416 0.648-0.206 1.01-0.205zm-3.34 0.594c-0.305 0.815 1.19 0.213 0.391 0-0.0412 0.235-0.217 0.12-0.391 0zm2.14 0c-0.291 0.0331-0.719-0.069-0.779 0.195 0.291-0.0334 0.719 0.069 0.779-0.195zm-58.8 1.75c-0.446 0.765-1.63 0.328-2.53 0.389-0.969 0.066-1.84 0.343-2.73 0.195-1.45-0.15 1.47-0.353 1.95-0.391 0.855-0.0665 2.52-0.0499 3.31-0.193zm60 0.348c0.205 8.05e-4 0.455 0.0127 0.752 0.041-1.47 0.505-2.19-0.0466-0.752-0.041zm-3.85 0.211c0.575 0.0234 1.19 0.0887 1.68 0.0254-0.654 0.45-2.14 0.067-3.12 0.193 0.327-0.225 0.863-0.242 1.44-0.219zm-2.72 0.182c0.188 8.7e-5 0.42 0.00994 0.699 0.0371-1.22 0.509-2.02-0.0377-0.699-0.0371zm-2.05 0.0137c0.346 0.0217 0.732 0.0837 0.996 0.0234-0.208 0.442-1.22 0.075-1.75 0.195 0.104-0.221 0.41-0.24 0.756-0.219zm6.95 0.18c0.605 0.0175 1.17 0.0805 1.64 0.234-2.31 0.0217-4.55 0.118-6.62 0.389 0.622-0.51 2.06-0.534 3.12-0.584 0.616-0.0293 1.26-0.0566 1.86-0.0391zm-10.1 0.0332c0.472-0.00705 0.951-0.0057 1.44 0.00586-2.42 0.249-8.48 0.82-11.1 0.389 3.44 0.0938 6.36-0.345 9.66-0.395zm11.6 0.348c0.0603-0.00636 0.115 0.00558 0.156 0.0469 0.134 0.134-0.768 0.537-0.584 0.195 0.0216-0.0399 0.247-0.223 0.428-0.242zm-8.21 0.0469c0.515-0.0102 1.06 0.0076 1.56 0-1.29 0.461-4.48 0.0586-1.56 0zm7.4 0c0.396 0.72-1.49 0.341-2.34 0.391-1.54 0.0903-3.9 0.385-5.84 0.389h-1.36c-0.849 0.00429-0.0334-0.336 0.389-0.195h1.36c2.39-0.396 5.43-0.153 7.79-0.584zm-24.1 0.188c0.666-0.0157 1.4 0.0365 2.05 0.00781 2.52 0.0686-0.542 0.303-1.56 0.195-0.691 0.117-0.98 0.215-2.14 0.193 0.383-0.298 0.983-0.381 1.65-0.396zm10.6 0.00781c-0.664 0.755-2 0.339-3.12 0.391-0.959 0.0446-2.16 0.369-3.12 0.193 2.25 0.663 7.04-0.279 9.54 0-3.35 0.35-6.93 0.469-10.5 0.584 0.801 0.562 3.53 0.0739 5.06 0 5.06-0.243 11.1-0.328 16.2-0.777-1.05 0.752-2.37 0.339-3.5 0.389-15 0.659-28.2 1.64-43.6 2.34-6.1 0.277-12.9 0.347-17.9 0.973-0.421-0.299-0.997-2.28-0.195-2.53-0.716 0.466 1.17 0.791-0.193 0.777 1.19 0.855 6.22-0.584 7.98 0-0.553-0.184-1.05 0.453-2.92 0.195-1.4 0.357-3.49 0.0133-4.87 0.389 14.6-0.151 29.5-1.48 44.8-1.95-0.832-0.667-4.3 0.156-5.45-0.193 3.21-0.449 7.82-0.311 11.9-0.779zm1.75 0c0.609-0.0098 1.17 0.0074 1.75 0-1.47 0.466-5.05 0.0529-1.75 0zm-15.5 0.146c0.533 0 1.07 0.0806 1.27 0.242-1.05-0.124-1.94 0.216-2.53 0 0.2-0.162 0.733-0.242 1.27-0.242zm-2.71 0.197c0.352-0.00562 0.703 0.00566 1.06 0.0449-3.07 0.511-8.85 0.362-12.1 0.779 2.24-0.724 5.1-0.462 7.79-0.584 1.11-0.0506 2.17-0.223 3.23-0.24zm2.69 0.412c0.51 0.0231 1.06 0.0862 1.48 0.0234-0.525 0.449-1.88 0.0678-2.72 0.193 0.262-0.224 0.732-0.24 1.24-0.217zm-1.44 0.0234c-0.187 0.396-0.748 0.42-1.36 0.389 0.23-0.354 1.02-0.152 1.36-0.389zm-2.47 0.115c0.257-0.0032 0.5 0.0189 0.721 0.0801-0.318-0.0882-1.42 0.159-1.95 0.193-5.7 0.379-12.3 0.975-18.9 0.975-1.77 0 0.453-0.189 0.584-0.195 0.204-0.00953 0.914 0.14 1.17-0.195 4.42-0.0906 10.9-0.347 16-0.584 0.738-0.0341 1.62-0.264 2.39-0.273zm-32.8 0.0137c0.34 0.03 0.424 0.179-0.375 0.26-0.343 0.0348-0.416 0.194-0.779 0.195 0.219-0.396 0.815-0.485 1.15-0.455zm22.4 0.0664c-0.632 0.167-5.16 0.677-6.23 0.193 2.29 0.151 4-0.286 6.23-0.193zm43.2 0.17c0.359 0.00626 0.0638 0.412-0.197 0.414-10.8 1.26-22.4 1.32-33.5 1.95-10.6 0.6-23.1 1.32-32.5 1.36 0.362-0.769 1.36-0.333 1.95-0.389 8.74-0.831 20.5-1.02 30.6-1.56 11-0.588 22.1-0.89 32.3-1.56 0.451-0.0293 0.463-0.287 1.17-0.195 0.08-0.0177 0.146-0.0243 0.197-0.0234zm-22.8 0.0234c-13.7 0.661-27.8 1.54-40.5 2.14-1.18 0.0565-2.63-0.367-3.11 0.389 12.8-0.768 28-1.25 41.1-2.14 0.742-0.0503 1.91 0.379 2.53-0.391zm-31.5 0.162c0.344-0.0091 0.67-2.82e-4 0.973 0.0332-0.834 0.514-2.73 0.512-4.28 0.584-1.88 0.0872-2.52 0.326-3.5 0 1.19 0.394 4.41-0.554 6.81-0.617zm2.51 0.00977c0.542 0.0232 1.12 0.0865 1.58 0.0234-0.589 0.449-2.01 0.0692-2.92 0.195 0.294-0.225 0.797-0.242 1.34-0.219zm-10.7 0.502c0.399-0.0211 0.74 0.0018 0.943 0.105-0.243-0.0584-3.59 0.696-4.09 0.195 0.214 0.214 1.95-0.237 3.15-0.301zm8.54 0.105c-0.775 0.782-2.9 0.218-4.28 0.389 1.14-0.413 3.05-0.0624 4.28-0.389zm-15 0.584c0.43 0.0241 0.0813 0.826 0.193 1.17-0.375-0.0791-0.152-0.755-0.777-0.584-0.142-0.64 0.386-0.298 0.389 0 0.228-0.0312 0.165-0.355 0.195-0.584zm0.664 1.27c0.322 0.0442 1.2 0.44 1.09 0.676-0.593 0.00882-0.757-0.411-1.17-0.584-0.0724-0.0824-0.0271-0.107 0.0801-0.0918zm-4.17 0.0918c-0.187 0.398-0.398 0.77-0.584 1.17-0.204-0.0298 0.0914-1.19 0.584-1.17zm56.7 0.193c0.592 2.99-0.396 6.09-0.389 9.15-1.23-0.33-0.278-1.72-0.195-2.73 0.183-2.23 0.136-4.58 0.584-6.43zm-54.9 0.391c0.37 0.446-0.396 1.41-0.391 2.14-0.512-0.506-0.154 0.315-0.779 0.195 0.299-0.869 0.827-1.51 1.17-2.34zm-0.779 0.193c-0.19 0.915-0.937 2.96-1.95 3.7 0.528-1.35 1.4-2.36 1.95-3.7zm38 0c0.43 0.0243 0.0831 0.827 0.195 1.17 0.203 2.01-0.387 4.24-0.391 6.62-0.24 0.0851-0.399 0.249-0.389 0.584-0.567-2.26 0.468-4.59 0.584-6.81v-1.56zm2.14 0c1.14 2.04 1.23 5.13 1.75 7.79 0.966-2.29 0.603-4.98 1.36-7.21 0.399 2.61-0.524 5.58-0.975 7.98-0.301-0.469-0.408-0.831-0.389 0.195-0.978-1.75-0.915-4.54-1.36-6.81-0.165 2.17 0.356 4.41 0.389 7.21-0.565-1.28 0.0139-0.138-0.777 0.193 0.0648-3.42-0.308-6.71 0-9.35zm-40.6 0.148c0.0756 0.03 0.0914 0.208-0.119 0.631-0.3-0.32-0.00686-0.681 0.119-0.631zm-1.87 0.0469c0.473 0.299-0.419 0.865-0.391 1.36-0.489-0.315 0.347-0.937 0.391-1.36zm56.3 0c0.958 1.9 0.462 5.25 0.584 7.98-0.466-1.56-0.72-4.9-0.584-7.98zm-29.2 0.195c0.261 0.128 0.362 0.417 0.389 0.779-0.586 0.196-0.345-0.434-0.389-0.779zm5.45 0c0.855 1.37 1.25 4.18 1.17 5.84 0.705-0.497 0.459-1.91 0.584-2.92 0.0922-0.739 0.207-1.43 0.389-1.95 0.568 2.38-0.791 5.36-0.389 7.59-0.62-1-0.133 0.546-0.584 0.779-0.977-0.454-0.148-1.37-0.195-2.14-0.137-2.22-0.812-4.91-0.973-7.2zm3.89 0c0.529 3.03-0.0947 5.54-0.584 7.98-0.421-2.5 0.585-5.26 0.584-7.98zm0.586 0c0.0518 0.802 0.45 0.271 0.434 0.0684-0.0111-0.0131-0.0379-0.0667-0.0449-0.0684 0.0303 0.00731 0.0422 0.0341 0.0449 0.0684 0.128 0.151 0.478 0.913 0.344 0.127 0.746 1.59 1.1 3.58 1.17 5.84 0.693-0.559 0.315 2.03 0.195 2.92-0.79-2.32-0.884-5-1.36-7.98-0.709 2.29 0.473 5.69-0.193 8.18-0.33 0.682-0.502-0.381-0.391-0.779-0.551-0.0319 0.162 1.2-0.389 1.17-1.07-0.886-0.108-2.18 0-3.31 0.185-1.94 0.0807-4.14 0.195-6.23zm4.67 0c0.32 1.91 0.709 5.21 0.584 7.79-0.011 0.225 0.0668 1.62-0.195 0.779v-1.17c-0.117-0.466-0.278-0.889-0.193-1.56-0.32 0.718-0.246 1.83-0.391 2.73-0.062 0.95-0.308-0.188-0.195-0.584 0.192-3.33 0.231-5.21 0.391-7.98zm10.7 0c0.328 0.924 0.416 3.92 0.389 5.84-0.331 0.0715-0.367-0.151-0.389-0.389-0.412 0.325-0.121 1.15-0.195 1.75-0.113 0.915 0.0233 0.718-0.391 1.17-0.227-1.85 0.225-5.5 0.586-8.37zm3.11 0c0.531 0.215 0.146 1.27 0.195 1.95 0.157 2.13 0.226 3.96-0.195 6.04-0.737-2.5 0.186-5.14 0-7.98zm15.9 0.0371c0.0984 0.0567 0.249 0.423 0.301 0.547 0.943 2.17 1.77 4.45 2.34 7.01-1.39-1.92-1.56-5.06-2.72-7.2-0.0114-0.309 0.0289-0.386 0.0879-0.352zm-0.992 0.0234c0.0777-0.0047 0.165 0.325 0.125 0.523-0.0744 0.371-0.248-0.125-0.195-0.391 0.0186-0.0926 0.0444-0.131 0.0703-0.133zm-19.3 0.135c0.439 0.145 0.0752 1.09 0.193 1.56 0.435-0.0837 0.0795-0.96 0.195-1.36 0.587 1.7-0.126 5.55-0.389 7.59-0.604-0.366 0.193-1.55-0.195-1.75-0.536 0.242 0.246 1.8-0.779 1.56 0.385-2.47 0.617-5.09 0.975-7.59zm-9.93 0.193c0.315 3.2-0.183 6.07-0.779 8.96-0.48-1.91 0.625-5.97 0.779-8.96zm12.1 0.195c0.357 0.888 0.353 4.16 0.584 6.04-0.107 0.432 0.373 0.277 0.391 0.584 0.145 0.882-0.536-0.298-0.391 0.584-0.0362 0.166 0.0766 0.184 0.195 0.195-0.334 0.569-0.483-0.666-0.389-1.17-0.581 0.133 0.334 1.76-0.779 1.36 1.03-2.03-0.268-5.19 0.389-7.59zm14.2 0c0.426 0.163 0.0859 2.77 0.195 3.89-0.426-0.163-0.0866-2.77-0.195-3.89zm2.14 0c0.307 1.41 0.818 4.47 0.584 6.81-0.49-2.05-0.685-4.73-0.584-6.81zm-65.8 0.195c0.486 0.719-0.835 1.07 0 0zm58.6 0c0.461 1.7 0.122 4.27 0 6.04-0.662-2.05-0.158-3.48 0-6.04zm5.65 0c0.843-0.00953 0.422 0.921 0.584 1.56 0.452 1.77 0.994 3.94 0.973 5.06-0.0205 1.13 0.0162-0.569-0.582-0.975v-1.36c-0.43 0.0243-0.0831 0.825-0.195 1.17v1.17c-0.319-1.69-0.394-3.63-0.389-5.65-0.498-0.0851-0.114-0.185-0.391-0.973zm-63 0.121c0.146 0.0205-0.262 1.27-0.488 1.44-0.365 0.489 0.116-0.187 0-0.584-0.173 0.0865-0.347 0.17-0.389 0.389-0.525 0.34 0.346-0.653 0.195-1.17 0.324 0.492 0.303 0.726 0.584 0 0.0457-0.0538 0.0767-0.0752 0.0977-0.0723zm30.3 0.0723c0.479 2.26-0.0221 6.24-0.389 8.37-0.676-2.82 0.412-5.56 0.389-8.37zm-4.74 0.0156c-0.0307-0.0235-0.0729 0.0222-0.129 0.18-0.0257 0.256-0.029 1.41 0.195 0.779 0.0191-0.192 0.0258-0.889-0.0664-0.959zm36.5 0.18c0.292 0.754 0.144 2.35 0 3.5-0.0806 0.647-0.0137 1.64-0.584 1.75 0.335-1.61 0.268-3.63 0.584-5.26zm-46.3 0.195c0.226-0.0317 0.346 0.0415 0.389 0.193-0.128 2.2 0.079 4.87-0.193 6.81-0.0989 0.707-0.352 1.57-0.779 1.95-0.129-3.31 0.533-5.83 0.584-8.96zm-6.81 0.389c0.661 1.07-0.717 3.58 0.195 4.28-0.512 0.0558-0.182 0.0788-0.391 1.17-0.201 1.05-0.864 2.23-0.777 3.31-0.439-0.368-0.253-1.53-0.195-2.14 0.197-2.11 0.925-4.68 1.17-6.62zm-10.3 0.195c0.0548 0.509-0.0709 0.836-0.584 0.777-0.0548-0.509 0.0709-0.836 0.584-0.777zm64.6 0.434c0.0921 0.12 0.0495 0.909 0.0508 1.12-0.268 0.62-0.193-0.687-0.195-0.973 0.0671-0.155 0.114-0.19 0.145-0.15zm-21.8 0.0664c-0.0764-0.111-0.101 0.675-0.0996 0.668-0.118 0.601-0.189 2.05 0 2.92 0.0567-1.17 0.281-2.42 0.193-3.12-0.0377-0.299-0.0683-0.436-0.0938-0.473zm10.9 0.0977c0.0922 0.0705 0.0855 0.767 0.0664 0.959-0.224 0.63-0.221-0.523-0.195-0.779 0.056-0.157 0.0982-0.203 0.129-0.18zm-10.1 0.375c-0.88 0.141-0.061 0.961 0 0zm-45.8 0.322c0.171-0.00179-0.398 0.995-0.512 1.24-0.391-0.157 0.35-0.803 0.391-1.17 0.0582-0.0461 0.0966-0.0662 0.121-0.0664zm57.6 0.104c-0.0266-0.0184-0.0563 0.021-0.0879 0.158-0.171 0.741 0.105 0.975 0.195 0.584 0.0232-0.102-0.0276-0.687-0.107-0.742zm-31.7 0.193c-0.0266-0.0184-0.0563 0.0229-0.0879 0.16-0.171 0.741 0.103 0.975 0.193 0.584 0.0234-0.102-0.0257-0.689-0.105-0.744zm40.8 0.354c0.45 0.653 0.069 2.14 0.195 3.12-0.701-0.332-0.425-2.48-0.195-3.12zm-35.8 0.258c-0.0259 0.00159-0.0517 0.0402-0.0703 0.133-0.0529 0.265 0.121 0.759 0.195 0.389 0.0399-0.199-0.0473-0.526-0.125-0.521zm-5.13 0.717c0.238 0.577-0.2 1.24 0.389 1.75-0.00691-0.707 0.11-1.54-0.389-1.75zm40.2 0.0957c0.193-0.0244 0.0431 1.29 0.0898 1.66-0.346 0.519-0.141-1.14-0.193-1.56 0.0431-0.0649 0.076-0.0961 0.104-0.0996zm-56.5 0.285c0.0922 0.0214 0.133 0.608 0.0859 0.787 0.741 0.313 0.33 2.17 0.389 3.5-0.263-0.473-0.389-0.12-0.584-0.389 0.273-0.306 0.525-0.62 0.391-1.75-0.422-0.0329-0.0888 0.69-0.195 0.973-0.409 0.142-0.0983-2.1-0.195-2.92 0.0408-0.155 0.0786-0.21 0.109-0.203zm36.5 0.00781c0.05 1.12-0.147 2.48 0.193 3.31-0.245-0.0427-0.397-0.303-0.389 0.195 1.31-0.13 0.192-3.43 0.195-3.51zm11.1 0v3.31c0.722-0.341 0.518-2.93 0-3.31zm-26.3 0.195c0.403 1.87-1.12 3.65 0 4.67 0.883-0.412 0.16-4.14 0-4.67zm27.6 0.404c0.0922 0.0704 0.0854 0.767 0.0664 0.959-0.224 0.63-0.222-0.524-0.195-0.779 0.056-0.158 0.0982-0.203 0.129-0.18zm-21.6 0.225c0.0921 0.12 0.0515 0.909 0.0527 1.12-0.268 0.62-0.193-0.687-0.195-0.973 0.0671-0.155 0.112-0.19 0.143-0.15zm-12.4 0.15c-0.192 1.5-0.0842 3.29-0.584 4.48 0.55-0.02 0.825-0.732 1.36-0.193 0.0415-1.87-0.0725-2.95-0.193-4.28h-0.584zm-4.09 0.193c0.121 1.47-0.151 3.33 0.584 3.89-0.254-0.969-0.313-3.53-0.584-3.89zm40 0.0352c0.0799 0.0553 0.129 0.643 0.105 0.744-0.0906 0.391-0.365 0.157-0.193-0.584 0.0316-0.137 0.0613-0.179 0.0879-0.16zm-37.9 0.744c0.0941 0.992-1.07 2.99 0 3.12-0.0255-0.362 0.162-3.34 0-3.12zm-7.59 0.584c-0.591 0.656 0.72 0.59 0 0zm11.3 0c-0.183 0.424-0.563 2.43 0.195 2.53 0.474-0.613 0.0678-2.01-0.195-2.53zm-2.79 0.0156c0.0923 0.0705 0.0857 0.767 0.0664 0.959-0.224 0.63-0.221-0.523-0.195-0.779 0.0561-0.157 0.0981-0.203 0.129-0.18zm-15.4 0.0625c0.186-0.073 0.0589 1.16 0.0938 1.48-0.326 0.561-0.153-0.995-0.193-1.36 0.0408-0.0701 0.073-0.107 0.0996-0.117zm23 0.193c0.186-0.0729 0.0591 1.16 0.0938 1.48-0.326 0.561-0.155-0.995-0.195-1.36 0.0408-0.0701 0.075-0.107 0.102-0.117zm5.94 0.312c0.434 0.588 0.153 0.87-0.391 0.391 0.0486-0.211 0.388-0.133 0.391-0.391zm-17.4 1.01c0.0799 0.0552 0.131 0.64 0.107 0.742-0.0903 0.391-0.367 0.157-0.195-0.584 0.0316-0.137 0.0613-0.177 0.0879-0.158zm-13.3 0.221c0.0775-0.00467 0.165 0.323 0.125 0.521-0.0741 0.37-0.247-0.124-0.193-0.389 0.0185-0.0926 0.0425-0.131 0.0684-0.133z"/> - <path d="m79.9 36.4c-19.9 1.41-40.7 1.8-60.5 2.92-1.51 0.0851-2.62 0.175-4.09 0.584 4.47-0.0684 8.39-0.783 12.8-0.389-2.32 0.0481-5.06 0.252-7.4 0.389-0.777 0.0455-1.97-0.37-2.53 0.389 2.05-0.168 4.91-0.411 6.23-0.193-2.67 0.506-6.27 0.091-8.76 0.779 7.5-0.223 16.1-0.736 24.3-1.17 1.11-0.0582 2.35-0.33 3.31 0.195-8.59 0.709-19.1 0.225-26.5 1.36-0.195 0.0303-1.32-0.364-1.17 0.389 1.5-0.153 3.37-0.675 4.87-0.389 0.396 0.076 0.204 0.202 0.193 0.193 0.731 0.597 2.37 0.0448 3.31 0 16.4-0.78 34.7-1.78 51.2-2.14-9.95 0.303-20.2 1.09-30.2 1.56-8.88 0.413-19.5 0.534-27.5 1.36-0.489 0.0512-1.51-0.369-1.75 0.391 1.1-0.236 2.64-0.317 4.09-0.391 1.43-0.0727 3.07-0.374 3.89 0.195-2.66 0.0679-5.57-0.123-7.59 0.584 4.71-0.289 10.2-0.791 14.8-0.584-4.98 0.568-9.86 0.187-14.4 0.973 23-1.06 45-2.1 67.8-3.31 0.215-0.0882 0.139-0.149-0.195-0.195-1.92-0.266-4.35 0.272-6.62 0.389-14.2 0.73-28.3 1.32-43.8 1.95 16.3-1.06 33.4-1.76 49.3-2.53 0.888-0.295-1.74-0.205-2.34-0.193-14.6 0.28-30.3 1.97-45.2 2.14 15.6-1.21 31.9-1.41 48.1-2.72-2.13-0.178-4.33 0.601-6.23 0 1.88-0.0894 2.76-0.202 4.48-0.195 0.205-0.0107 0.152-0.25-0.195-0.195-11.6 0.285-22.9 1.34-33.7 1.36 11.3-0.811 23.4-0.889 34.7-1.95-13.4 0.373-25.8 1.59-38.9 1.56 10.8-0.671 23.3-0.848 35-1.75 0.75-0.0577 1.78 0.383 2.34-0.391-15.8 0.781-32.7 2.04-48.9 2.14 11.1-0.495 23.9-1.02 35.2-1.56 0.642-0.0303 5.7-0.253 1.95-0.389-3.24-0.117-6.7 0.245-10.1 0.389-10.4 0.438-20.7 1.36-31 1.36 14-0.623 30.4-1.66 45-2.34 2.19-0.102 4.53 0.158 6.62-0.584zm0.195 0.584c-1.68 0.0977-4.43 0.28-5.84 0.389-1.13 0.087-2.71-0.362-2.92 0.391 2.69-0.485 6.35-0.0107 8.76-0.779zm-2.92 1.95c-0.0684 0.646-1.12 0.305-1.75 0.389 0.347-0.653 0.891-0.1 1.75-0.389zm-45.2 0.584c-0.249 0.854-2.04 0.169-2.92 0.389-0.482 0.0245-0.331-0.239 0-0.195 1.01-0.0329 2.2 0.122 2.92-0.193zm-4.22 0.223c0.195-0.00524 0.339 0.0777 0.332 0.361-0.869 0.0396-4.9 0.00322-2.53-0.195h1.56c0.2-0.0679 0.448-0.161 0.643-0.166zm19.6 0.125c0.0961 2.66e-4 0.221 0.0129 0.377 0.041-0.374 0.52-1.05-0.0429-0.377-0.041zm-0.598 0.041c-2.45 0.735-5.11 0.454-7.79 0.584-5.46 0.264-11.8 0.69-17.5 0.973 0.288-0.744 1.26-0.373 1.75-0.389 7.78-0.254 16-0.777 23.6-1.17zm-29.4 0.154c-0.531-0.0172-2.01 0.144-1.97 0.43 0.887 0.071 0.892-0.117 1.36-0.195 0.9-0.15 0.924-0.224 0.605-0.234zm66.6 0.408c-0.246-0.0191-0.453 0.00183-0.465 0.217 0.342-0.112 1.15 0.235 1.17-0.195-0.171 0.0559-0.459-0.00239-0.705-0.0215zm-0.854 0.0215c-16.9 0.814-33.3 1.64-49.8 2.34-5.47 0.23-11 0.418-16.4 1.17 1.31 1.35 2.59 1.89 4.87 1.17-0.467-0.259-2.58 0.347-3.31-0.193 20.8-1.48 42.2-1.67 62.5-3.12 1.16-0.0825 6.24-0.251 3.7-0.391-5.06-0.279-12.2 0.6-15.8 0.779-17.1 0.867-34.5 1.61-50.8 2.34 11.4-0.948 25.3-1.37 37.6-1.95 8.37-0.389 18.7-0.712 26.5-1.36 0.356-0.0298 2.17-0.0454 0.389-0.195-3.46-0.29-6.15 0.662-8.76 0.195 3.12-0.529 6.26-0.0572 9.35-0.779zm1.15 0.367c-0.28-0.0203-0.521-9.15e-4 -0.562 0.217 0.403-0.116 1.28 0.242 1.36-0.193-0.202 0.0578-0.521-0.00318-0.801-0.0234zm-10.7 0.412c-18.8 1.35-37.8 1.88-56.1 2.92 4.47-0.677 8.87-0.789 13.2-0.973 15.4-0.65 28.1-1.36 42.8-1.95zm-54.1 0.193c-1.1 0.199-3.5 0.132-3.7 0.584 0.954-0.474 3.13 0.275 3.7-0.584zm15.4 0.00195h0.391c-0.454 0.757-1.71 0.341-2.53 0.389-2.35 0.136-6.28 0.637-8.76 0.195 3.4-0.186 7.18-0.251 10.9-0.584zm49 0.566c-0.379-0.0104-0.822-0.00661-1.27 0.0176-1.33 0.356-3.41-0.0413-4.67 0.389 1.19 0.17 4.47-0.164 6.04-0.195 1.6-0.032 1.04-0.18-0.0918-0.211zm-51.5 0.211c0.465-0.0093 0.725 0.0072 1.17 0-1.12 0.467-3.62 0.0491-1.17 0zm53.7 0.195c-22 0.986-42.4 2.08-62.5 3.12-1.63 0.0839-3.32-0.243-4.09 0.584 22.8-1.67 45.1-2.04 66.6-3.7zm-13.2 0.195c-7.79 0.371-15.8 0.698-24.9 1.17-6.32 0.324-12.7 0.682-18.9 0.973-1.57 0.0744-5.98 0.429-6.04 0.391 0.527 0.382 4.99-0.0833 7.01-0.195 13.4-0.743 28.6-1.56 40.3-2.14 1.74-0.086 5.59 0.342 7.2-0.193-0.248 0.0822-4.51-0.0076-4.67 0zm12.9 0.287c-0.248 0.017-0.562 0.1-0.781 0.102-1.14 0.0067-2.26-0.0138-2.53 0-10.5 0.531-21 0.993-31.2 1.56-9.62 0.532-20.2 0.809-29.8 1.56-0.412 0.0324-1.13-0.0174-1.36 0.389 0.452 0.322 0.909 0.0596 1.36 0 2.12-0.278 4.05-0.297 6.23-0.389 19.9-0.837 39.1-2.24 58.4-2.92 0.0428-0.264-0.139-0.314-0.387-0.297zm1.36 0.49c-10.3 0.415-20.8 1.11-31.3 1.56-8.69 0.37-18.9 0.594-27.1 1.36-0.216 0.0203-1.27-0.383-1.17 0.389 9.29-0.567 19.2-1.05 28.8-1.56 10.1-0.531 20.7-0.761 30.8-1.75zm-0.162 0.574c-0.358-9.23e-4 -0.76 0.163-1.01 0.205-2.69 0.452-5.67 0.263-8.56 0.391-8.43 0.37-16.3 0.997-24.1 1.36-9.55 0.444-19.6 0.641-28.4 1.36-1.21 0.0989-3.41-0.292-3.7 0.973 15.7-0.222 34.1-1.56 50.4-2.34 4.7-0.224 10.7-0.384 14.6-0.777 0.42-0.0424 1.41 0.433 1.56-0.391-0.0733-0.622-0.388-0.788-0.746-0.789zm-3.34 0.594c0.174 0.12 0.349 0.235 0.391 0 0.799 0.213-0.696 0.815-0.391 0zm2.14 0c-0.0598 0.264-0.488 0.162-0.779 0.195 0.0601-0.264 0.488-0.162 0.779-0.195zm-58.8 1.75c-0.784 0.143-2.45 0.127-3.31 0.193-0.481 0.0374-3.4 0.241-1.95 0.391 0.885 0.148 1.76-0.129 2.73-0.195 0.898-0.0612 2.08 0.376 2.53-0.389zm60 0.348c-1.44-0.00563-0.719 0.546 0.752 0.041-0.297-0.0283-0.547-0.0402-0.752-0.041zm-3.85 0.211c-0.575-0.0234-1.11-0.00623-1.44 0.219 0.977-0.126 2.46 0.257 3.12-0.193-0.489 0.0632-1.1-0.00204-1.68-0.0254zm-2.72 0.182c-1.32-6.1e-4 -0.521 0.546 0.699 0.0371-0.279-0.0272-0.511-0.037-0.699-0.0371zm-2.05 0.0156c-0.346-0.0217-0.652-0.00413-0.756 0.217 0.528-0.121 1.55 0.246 1.75-0.195-0.264 0.0602-0.652 1.73e-4 -0.998-0.0215zm6.95 0.178c-0.605-0.0175-1.25 0.00975-1.86 0.0391-1.06 0.0503-2.49 0.0733-3.11 0.584 2.07-0.271 4.3-0.367 6.62-0.389-0.469-0.154-1.04-0.217-1.64-0.234zm-10.1 0.0332c-3.3 0.0494-6.21 0.488-9.66 0.395 2.62 0.431 8.68-0.14 11.1-0.389-0.49-0.0116-0.97-0.0129-1.44-0.00586zm11.6 0.348c-0.181 0.019-0.406 0.202-0.428 0.242-0.184 0.342 0.718-0.0609 0.584-0.195-0.0413-0.0413-0.096-0.0532-0.156-0.0469zm-8.21 0.0469c-2.93 0.0586 0.269 0.461 1.56 0-0.498 0.0076-1.04-0.0102-1.56 0zm7.4 0c-2.36 0.431-5.39 0.188-7.79 0.584h-1.36c-0.422-0.141-1.24 0.2-0.389 0.195h1.36c1.94-0.00381 4.3-0.299 5.84-0.389 0.846-0.0496 2.73 0.329 2.34-0.391zm-24.1 0.188c-0.666 0.0157-1.27 0.0986-1.65 0.396 1.16 0.0212 1.45-0.0766 2.14-0.193 1.01 0.108 4.07-0.127 1.56-0.195-0.653 0.0287-1.39-0.0235-2.05-0.00781zm10.6 0.00781c-4.05 0.468-8.67 0.331-11.9 0.779 1.15 0.35 4.62-0.473 5.45 0.193-15.3 0.466-30.2 1.8-44.8 1.95 1.38-0.375 3.47-0.0314 4.87-0.389 1.87 0.258 2.37-0.379 2.92-0.195-1.76-0.584-6.79 0.855-7.98 0 1.36 0.0138-0.521-0.313 0.195-0.779-0.802 0.251-0.228 2.23 0.193 2.53 5.02-0.625 11.8-0.696 17.9-0.973 15.4-0.699 28.6-1.68 43.6-2.34 1.13-0.0498 2.45 0.362 3.5-0.391-5.1 0.45-11.1 0.536-16.2 0.779-1.53 0.0739-4.26 0.562-5.06 0 3.58-0.115 7.16-0.234 10.5-0.584-2.5-0.279-7.29 0.663-9.54 0 0.957 0.176 2.16-0.151 3.12-0.195 1.11-0.0517 2.45 0.367 3.11-0.389zm1.75 0c-3.3 0.0529 0.287 0.466 1.75 0-0.579 0.0074-1.14-0.0098-1.75 0zm-15.5 0.146c-0.533 0-1.07 0.0806-1.27 0.242 0.591 0.216 1.48-0.124 2.53 0-0.2-0.162-0.733-0.242-1.27-0.242zm-2.71 0.197c-1.06 0.0169-2.11 0.19-3.23 0.24-2.68 0.122-5.55-0.14-7.79 0.584 3.22-0.417 9-0.268 12.1-0.779-0.353-0.0393-0.705-0.0505-1.06-0.0449zm2.69 0.412c-0.51-0.0231-0.978-0.00747-1.24 0.217 0.848-0.126 2.2 0.255 2.72-0.193-0.424 0.0628-0.974-3.49e-4 -1.48-0.0234zm-1.44 0.0234c-0.347 0.236-1.13 0.0348-1.36 0.389 0.615 0.0312 1.18 0.00767 1.36-0.389zm-2.47 0.115c-0.77 0.00955-1.65 0.239-2.39 0.273-5.1 0.237-11.5 0.493-16 0.584-0.254 0.336-0.964 0.186-1.17 0.195-0.131 0.00596-2.35 0.195-0.584 0.195 6.63 0 13.2-0.596 18.9-0.975 0.526-0.0348 1.63-0.282 1.95-0.193-0.221-0.0612-0.466-0.0833-0.723-0.0801zm-32.8 0.0137c-0.34-0.03-0.935 0.0593-1.15 0.455 0.363-0.00143 0.436-0.161 0.779-0.195 0.799-0.081 0.715-0.23 0.375-0.26zm22.4 0.0645c-2.23-0.0922-3.94 0.347-6.23 0.195 1.07 0.484 5.6-0.0285 6.23-0.195zm43.2 0.172c-0.0514-8.94e-4 -0.115 0.00571-0.195 0.0234-0.705-0.0915-0.717 0.166-1.17 0.195-10.2 0.667-21.3 0.969-32.3 1.56-10.1 0.54-21.8 0.728-30.6 1.56-0.587 0.056-1.59-0.38-1.95 0.389 9.42-0.0427 21.9-0.763 32.5-1.36 11.1-0.626 22.7-0.69 33.5-1.95 0.261-0.00209 0.555-0.408 0.195-0.414zm-22.8 0.0234c-0.624 0.77-1.79 0.34-2.53 0.391-13.1 0.889-28.3 1.37-41.1 2.14 0.483-0.755 1.93-0.332 3.12-0.389 12.7-0.606 26.8-1.48 40.5-2.14zm-31.5 0.162c-2.41 0.0637-5.63 1.01-6.81 0.617 0.981 0.326 1.63 0.0872 3.5 0 1.55-0.0725 3.45-0.0697 4.28-0.584-0.303-0.0335-0.629-0.0423-0.973-0.0332zm2.51 0.00977c-0.542-0.0232-1.05-0.00587-1.34 0.219 0.912-0.126 2.33 0.254 2.92-0.195-0.456 0.063-1.04-2.01e-4 -1.58-0.0234zm-10.7 0.502c-1.2 0.0633-2.93 0.515-3.15 0.301 0.501 0.501 3.85-0.254 4.09-0.195-0.204-0.104-0.544-0.127-0.943-0.105zm8.54 0.105c-1.23 0.326-3.14-0.0239-4.28 0.389 1.39-0.171 3.51 0.394 4.28-0.389zm-15 0.584c-0.0303 0.229 0.033 0.553-0.195 0.584-0.00238-0.298-0.53-0.64-0.389 0 0.625-0.171 0.402 0.505 0.777 0.584-0.112-0.342 0.237-1.14-0.193-1.17zm0.664 1.27c-0.107-0.0147-0.152 0.00946-0.0801 0.0918 0.411 0.173 0.575 0.593 1.17 0.584 0.108-0.236-0.766-0.632-1.09-0.676zm21.5 1.26c0.0441 0.345-0.197 0.975 0.389 0.779-0.0267-0.363-0.128-0.651-0.389-0.779zm43.3 0.0625c-0.0259 0.00159-0.0517 0.0402-0.0703 0.133-0.0527 0.265 0.121 0.759 0.195 0.389 0.04-0.199-0.0473-0.526-0.125-0.521z" fill="#cbc1b6"/> -</svg> diff --git a/scour/__init__.py b/scour/__init__.py index 591803a..f3f6b3e 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -1,19 +1,22 @@ ############################################################################### -# -# Copyright (C) 2010 Jeff Schiller, 2010 Louis Simard, 2013-2015 Tavendo GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# +## +## Copyright (C) 2013 Tavendo GmbH +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## ############################################################################### -__version__ = u'0.38.2' +import scour +import svg_regex +import svg_transform +import yocto_css diff --git a/scour/scour.py b/scour/scour.py index 8c907de..f85a8ed 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -32,8 +32,8 @@ # * Collapse all group based transformations # Even more ideas here: http://esw.w3.org/topic/SvgTidy -# * analysis of path elements to see if rect can be used instead? -# (must also need to look at rounded corners) +# * analysis of path elements to see if rect can be used instead? (must also need to look +# at rounded corners) # Next Up: # - why are marker-start, -end not removed from the style attribute? @@ -44,47 +44,37 @@ # - parse transform attribute # - if a <g> has only one element in it, collapse the <g> (ensure transform, etc are carried down) +# necessary to get true division +from __future__ import division -from __future__ import division # use "true" division instead of integer division in Python 2 (see PEP 238) -from __future__ import print_function # use print() as a function in Python 2 (see PEP 3105) -from __future__ import absolute_import # use absolute imports by default in Python 2 (see PEP 328) - -import math -import optparse import os -import re import sys -import time import xml.dom.minidom -from xml.dom import Node, NotFoundErr -from collections import namedtuple, defaultdict -from decimal import Context, Decimal, InvalidOperation, getcontext +import re +import math +from svg_regex import svg_parser +from svg_transform import svg_transform_parser +import optparse +from yocto_css import parseCssString -import six -from six.moves import range, urllib +# Python 2.3- did not have Decimal +try: + from decimal import * +except ImportError: + print >>sys.stderr, "Scour requires Python 2.4." -from scour.stats import ScourStats -from scour.svg_regex import svg_parser -from scour.svg_transform import svg_transform_parser -from scour.yocto_css import parseCssString -from scour import __version__ +# Import Psyco if available +try: + import psyco + psyco.full() +except ImportError: + pass +APP = 'scour' +VER = '0.29' +COPYRIGHT = 'Copyright Jeff Schiller, Louis Simard, 2010' -APP = u'scour' -VER = __version__ -COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' - - -XML_ENTS_NO_QUOTES = {'<': '<', '>': '>', '&': '&'} -XML_ENTS_ESCAPE_APOS = XML_ENTS_NO_QUOTES.copy() -XML_ENTS_ESCAPE_APOS["'"] = ''' -XML_ENTS_ESCAPE_QUOT = XML_ENTS_NO_QUOTES.copy() -XML_ENTS_ESCAPE_QUOT['"'] = '"' - -# Used to split values where "x y" or "x,y" or a mix of the two is allowed -RE_COMMA_WSP = re.compile(r"\s*[\s,]\s*") - -NS = {'SVG': 'http://www.w3.org/2000/svg', +NS = { 'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 'INKSCAPE': 'http://www.inkscape.org/namespaces/inkscape', @@ -97,4103 +87,3196 @@ NS = {'SVG': 'http://www.w3.org/2000/svg', 'ADOBE_FLOWS': 'http://ns.adobe.com/Flows/1.0/', 'ADOBE_IMAGE_REPLACEMENT': 'http://ns.adobe.com/ImageReplacement/1.0/', 'ADOBE_CUSTOM': 'http://ns.adobe.com/GenericCustomNamespace/1.0/', - 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/', - 'SKETCH': 'http://www.bohemiancoding.com/sketch/ns' + 'ADOBE_XPATH': 'http://ns.adobe.com/XPath/1.0/' } -unwanted_ns = [NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], - NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], - NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], - NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], - NS['ADOBE_XPATH'], NS['SKETCH']] +unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], + NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], + NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], + NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], NS['ADOBE_XPATH'] ] -# A list of all SVG presentation properties -# -# Sources for this list: -# https://www.w3.org/TR/SVG/propidx.html (implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) -# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) -# svgAttributes = [ - # SVG 1.1 - 'alignment-baseline', - 'baseline-shift', - 'clip', - 'clip-path', - 'clip-rule', - 'color', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'cursor', - 'direction', - 'display', - 'dominant-baseline', - 'enable-background', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'flood-color', - 'flood-opacity', - 'font', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'glyph-orientation-horizontal', - 'glyph-orientation-vertical', - 'image-rendering', - 'kerning', - 'letter-spacing', - 'lighting-color', - 'marker', - 'marker-end', - 'marker-mid', - 'marker-start', - 'mask', - 'opacity', - 'overflow', - 'pointer-events', - 'shape-rendering', - 'stop-color', - 'stop-opacity', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'unicode-bidi', - 'visibility', - 'word-spacing', - 'writing-mode', - # SVG 1.2 Tiny - 'audio-level', - 'buffered-rendering', - 'display-align', - 'line-increment', - 'solid-color', - 'solid-opacity', - 'text-align', - 'vector-effect', - 'viewport-fill', - 'viewport-fill-opacity', -] + 'clip-rule', + 'display', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'font-family', + 'font-size', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'line-height', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'opacity', + 'overflow', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'visibility' + ] colors = { - 'aliceblue': 'rgb(240, 248, 255)', - 'antiquewhite': 'rgb(250, 235, 215)', - 'aqua': 'rgb( 0, 255, 255)', - 'aquamarine': 'rgb(127, 255, 212)', - 'azure': 'rgb(240, 255, 255)', - 'beige': 'rgb(245, 245, 220)', - 'bisque': 'rgb(255, 228, 196)', - 'black': 'rgb( 0, 0, 0)', - 'blanchedalmond': 'rgb(255, 235, 205)', - 'blue': 'rgb( 0, 0, 255)', - 'blueviolet': 'rgb(138, 43, 226)', - 'brown': 'rgb(165, 42, 42)', - 'burlywood': 'rgb(222, 184, 135)', - 'cadetblue': 'rgb( 95, 158, 160)', - 'chartreuse': 'rgb(127, 255, 0)', - 'chocolate': 'rgb(210, 105, 30)', - 'coral': 'rgb(255, 127, 80)', - 'cornflowerblue': 'rgb(100, 149, 237)', - 'cornsilk': 'rgb(255, 248, 220)', - 'crimson': 'rgb(220, 20, 60)', - 'cyan': 'rgb( 0, 255, 255)', - 'darkblue': 'rgb( 0, 0, 139)', - 'darkcyan': 'rgb( 0, 139, 139)', - 'darkgoldenrod': 'rgb(184, 134, 11)', - 'darkgray': 'rgb(169, 169, 169)', - 'darkgreen': 'rgb( 0, 100, 0)', - 'darkgrey': 'rgb(169, 169, 169)', - 'darkkhaki': 'rgb(189, 183, 107)', - 'darkmagenta': 'rgb(139, 0, 139)', - 'darkolivegreen': 'rgb( 85, 107, 47)', - 'darkorange': 'rgb(255, 140, 0)', - 'darkorchid': 'rgb(153, 50, 204)', - 'darkred': 'rgb(139, 0, 0)', - 'darksalmon': 'rgb(233, 150, 122)', - 'darkseagreen': 'rgb(143, 188, 143)', - 'darkslateblue': 'rgb( 72, 61, 139)', - 'darkslategray': 'rgb( 47, 79, 79)', - 'darkslategrey': 'rgb( 47, 79, 79)', - 'darkturquoise': 'rgb( 0, 206, 209)', - 'darkviolet': 'rgb(148, 0, 211)', - 'deeppink': 'rgb(255, 20, 147)', - 'deepskyblue': 'rgb( 0, 191, 255)', - 'dimgray': 'rgb(105, 105, 105)', - 'dimgrey': 'rgb(105, 105, 105)', - 'dodgerblue': 'rgb( 30, 144, 255)', - 'firebrick': 'rgb(178, 34, 34)', - 'floralwhite': 'rgb(255, 250, 240)', - 'forestgreen': 'rgb( 34, 139, 34)', - 'fuchsia': 'rgb(255, 0, 255)', - 'gainsboro': 'rgb(220, 220, 220)', - 'ghostwhite': 'rgb(248, 248, 255)', - 'gold': 'rgb(255, 215, 0)', - 'goldenrod': 'rgb(218, 165, 32)', - 'gray': 'rgb(128, 128, 128)', - 'grey': 'rgb(128, 128, 128)', - 'green': 'rgb( 0, 128, 0)', - 'greenyellow': 'rgb(173, 255, 47)', - 'honeydew': 'rgb(240, 255, 240)', - 'hotpink': 'rgb(255, 105, 180)', - 'indianred': 'rgb(205, 92, 92)', - 'indigo': 'rgb( 75, 0, 130)', - 'ivory': 'rgb(255, 255, 240)', - 'khaki': 'rgb(240, 230, 140)', - 'lavender': 'rgb(230, 230, 250)', - 'lavenderblush': 'rgb(255, 240, 245)', - 'lawngreen': 'rgb(124, 252, 0)', - 'lemonchiffon': 'rgb(255, 250, 205)', - 'lightblue': 'rgb(173, 216, 230)', - 'lightcoral': 'rgb(240, 128, 128)', - 'lightcyan': 'rgb(224, 255, 255)', - 'lightgoldenrodyellow': 'rgb(250, 250, 210)', - 'lightgray': 'rgb(211, 211, 211)', - 'lightgreen': 'rgb(144, 238, 144)', - 'lightgrey': 'rgb(211, 211, 211)', - 'lightpink': 'rgb(255, 182, 193)', - 'lightsalmon': 'rgb(255, 160, 122)', - 'lightseagreen': 'rgb( 32, 178, 170)', - 'lightskyblue': 'rgb(135, 206, 250)', - 'lightslategray': 'rgb(119, 136, 153)', - 'lightslategrey': 'rgb(119, 136, 153)', - 'lightsteelblue': 'rgb(176, 196, 222)', - 'lightyellow': 'rgb(255, 255, 224)', - 'lime': 'rgb( 0, 255, 0)', - 'limegreen': 'rgb( 50, 205, 50)', - 'linen': 'rgb(250, 240, 230)', - 'magenta': 'rgb(255, 0, 255)', - 'maroon': 'rgb(128, 0, 0)', - 'mediumaquamarine': 'rgb(102, 205, 170)', - 'mediumblue': 'rgb( 0, 0, 205)', - 'mediumorchid': 'rgb(186, 85, 211)', - 'mediumpurple': 'rgb(147, 112, 219)', - 'mediumseagreen': 'rgb( 60, 179, 113)', - 'mediumslateblue': 'rgb(123, 104, 238)', - 'mediumspringgreen': 'rgb( 0, 250, 154)', - 'mediumturquoise': 'rgb( 72, 209, 204)', - 'mediumvioletred': 'rgb(199, 21, 133)', - 'midnightblue': 'rgb( 25, 25, 112)', - 'mintcream': 'rgb(245, 255, 250)', - 'mistyrose': 'rgb(255, 228, 225)', - 'moccasin': 'rgb(255, 228, 181)', - 'navajowhite': 'rgb(255, 222, 173)', - 'navy': 'rgb( 0, 0, 128)', - 'oldlace': 'rgb(253, 245, 230)', - 'olive': 'rgb(128, 128, 0)', - 'olivedrab': 'rgb(107, 142, 35)', - 'orange': 'rgb(255, 165, 0)', - 'orangered': 'rgb(255, 69, 0)', - 'orchid': 'rgb(218, 112, 214)', - 'palegoldenrod': 'rgb(238, 232, 170)', - 'palegreen': 'rgb(152, 251, 152)', - 'paleturquoise': 'rgb(175, 238, 238)', - 'palevioletred': 'rgb(219, 112, 147)', - 'papayawhip': 'rgb(255, 239, 213)', - 'peachpuff': 'rgb(255, 218, 185)', - 'peru': 'rgb(205, 133, 63)', - 'pink': 'rgb(255, 192, 203)', - 'plum': 'rgb(221, 160, 221)', - 'powderblue': 'rgb(176, 224, 230)', - 'purple': 'rgb(128, 0, 128)', - 'red': 'rgb(255, 0, 0)', - 'rosybrown': 'rgb(188, 143, 143)', - 'royalblue': 'rgb( 65, 105, 225)', - 'saddlebrown': 'rgb(139, 69, 19)', - 'salmon': 'rgb(250, 128, 114)', - 'sandybrown': 'rgb(244, 164, 96)', - 'seagreen': 'rgb( 46, 139, 87)', - 'seashell': 'rgb(255, 245, 238)', - 'sienna': 'rgb(160, 82, 45)', - 'silver': 'rgb(192, 192, 192)', - 'skyblue': 'rgb(135, 206, 235)', - 'slateblue': 'rgb(106, 90, 205)', - 'slategray': 'rgb(112, 128, 144)', - 'slategrey': 'rgb(112, 128, 144)', - 'snow': 'rgb(255, 250, 250)', - 'springgreen': 'rgb( 0, 255, 127)', - 'steelblue': 'rgb( 70, 130, 180)', - 'tan': 'rgb(210, 180, 140)', - 'teal': 'rgb( 0, 128, 128)', - 'thistle': 'rgb(216, 191, 216)', - 'tomato': 'rgb(255, 99, 71)', - 'turquoise': 'rgb( 64, 224, 208)', - 'violet': 'rgb(238, 130, 238)', - 'wheat': 'rgb(245, 222, 179)', - 'white': 'rgb(255, 255, 255)', - 'whitesmoke': 'rgb(245, 245, 245)', - 'yellow': 'rgb(255, 255, 0)', - 'yellowgreen': 'rgb(154, 205, 50)', -} + 'aliceblue': 'rgb(240, 248, 255)', + 'antiquewhite': 'rgb(250, 235, 215)', + 'aqua': 'rgb( 0, 255, 255)', + 'aquamarine': 'rgb(127, 255, 212)', + 'azure': 'rgb(240, 255, 255)', + 'beige': 'rgb(245, 245, 220)', + 'bisque': 'rgb(255, 228, 196)', + 'black': 'rgb( 0, 0, 0)', + 'blanchedalmond': 'rgb(255, 235, 205)', + 'blue': 'rgb( 0, 0, 255)', + 'blueviolet': 'rgb(138, 43, 226)', + 'brown': 'rgb(165, 42, 42)', + 'burlywood': 'rgb(222, 184, 135)', + 'cadetblue': 'rgb( 95, 158, 160)', + 'chartreuse': 'rgb(127, 255, 0)', + 'chocolate': 'rgb(210, 105, 30)', + 'coral': 'rgb(255, 127, 80)', + 'cornflowerblue': 'rgb(100, 149, 237)', + 'cornsilk': 'rgb(255, 248, 220)', + 'crimson': 'rgb(220, 20, 60)', + 'cyan': 'rgb( 0, 255, 255)', + 'darkblue': 'rgb( 0, 0, 139)', + 'darkcyan': 'rgb( 0, 139, 139)', + 'darkgoldenrod': 'rgb(184, 134, 11)', + 'darkgray': 'rgb(169, 169, 169)', + 'darkgreen': 'rgb( 0, 100, 0)', + 'darkgrey': 'rgb(169, 169, 169)', + 'darkkhaki': 'rgb(189, 183, 107)', + 'darkmagenta': 'rgb(139, 0, 139)', + 'darkolivegreen': 'rgb( 85, 107, 47)', + 'darkorange': 'rgb(255, 140, 0)', + 'darkorchid': 'rgb(153, 50, 204)', + 'darkred': 'rgb(139, 0, 0)', + 'darksalmon': 'rgb(233, 150, 122)', + 'darkseagreen': 'rgb(143, 188, 143)', + 'darkslateblue': 'rgb( 72, 61, 139)', + 'darkslategray': 'rgb( 47, 79, 79)', + 'darkslategrey': 'rgb( 47, 79, 79)', + 'darkturquoise': 'rgb( 0, 206, 209)', + 'darkviolet': 'rgb(148, 0, 211)', + 'deeppink': 'rgb(255, 20, 147)', + 'deepskyblue': 'rgb( 0, 191, 255)', + 'dimgray': 'rgb(105, 105, 105)', + 'dimgrey': 'rgb(105, 105, 105)', + 'dodgerblue': 'rgb( 30, 144, 255)', + 'firebrick': 'rgb(178, 34, 34)', + 'floralwhite': 'rgb(255, 250, 240)', + 'forestgreen': 'rgb( 34, 139, 34)', + 'fuchsia': 'rgb(255, 0, 255)', + 'gainsboro': 'rgb(220, 220, 220)', + 'ghostwhite': 'rgb(248, 248, 255)', + 'gold': 'rgb(255, 215, 0)', + 'goldenrod': 'rgb(218, 165, 32)', + 'gray': 'rgb(128, 128, 128)', + 'grey': 'rgb(128, 128, 128)', + 'green': 'rgb( 0, 128, 0)', + 'greenyellow': 'rgb(173, 255, 47)', + 'honeydew': 'rgb(240, 255, 240)', + 'hotpink': 'rgb(255, 105, 180)', + 'indianred': 'rgb(205, 92, 92)', + 'indigo': 'rgb( 75, 0, 130)', + 'ivory': 'rgb(255, 255, 240)', + 'khaki': 'rgb(240, 230, 140)', + 'lavender': 'rgb(230, 230, 250)', + 'lavenderblush': 'rgb(255, 240, 245)', + 'lawngreen': 'rgb(124, 252, 0)', + 'lemonchiffon': 'rgb(255, 250, 205)', + 'lightblue': 'rgb(173, 216, 230)', + 'lightcoral': 'rgb(240, 128, 128)', + 'lightcyan': 'rgb(224, 255, 255)', + 'lightgoldenrodyellow': 'rgb(250, 250, 210)', + 'lightgray': 'rgb(211, 211, 211)', + 'lightgreen': 'rgb(144, 238, 144)', + 'lightgrey': 'rgb(211, 211, 211)', + 'lightpink': 'rgb(255, 182, 193)', + 'lightsalmon': 'rgb(255, 160, 122)', + 'lightseagreen': 'rgb( 32, 178, 170)', + 'lightskyblue': 'rgb(135, 206, 250)', + 'lightslategray': 'rgb(119, 136, 153)', + 'lightslategrey': 'rgb(119, 136, 153)', + 'lightsteelblue': 'rgb(176, 196, 222)', + 'lightyellow': 'rgb(255, 255, 224)', + 'lime': 'rgb( 0, 255, 0)', + 'limegreen': 'rgb( 50, 205, 50)', + 'linen': 'rgb(250, 240, 230)', + 'magenta': 'rgb(255, 0, 255)', + 'maroon': 'rgb(128, 0, 0)', + 'mediumaquamarine': 'rgb(102, 205, 170)', + 'mediumblue': 'rgb( 0, 0, 205)', + 'mediumorchid': 'rgb(186, 85, 211)', + 'mediumpurple': 'rgb(147, 112, 219)', + 'mediumseagreen': 'rgb( 60, 179, 113)', + 'mediumslateblue': 'rgb(123, 104, 238)', + 'mediumspringgreen': 'rgb( 0, 250, 154)', + 'mediumturquoise': 'rgb( 72, 209, 204)', + 'mediumvioletred': 'rgb(199, 21, 133)', + 'midnightblue': 'rgb( 25, 25, 112)', + 'mintcream': 'rgb(245, 255, 250)', + 'mistyrose': 'rgb(255, 228, 225)', + 'moccasin': 'rgb(255, 228, 181)', + 'navajowhite': 'rgb(255, 222, 173)', + 'navy': 'rgb( 0, 0, 128)', + 'oldlace': 'rgb(253, 245, 230)', + 'olive': 'rgb(128, 128, 0)', + 'olivedrab': 'rgb(107, 142, 35)', + 'orange': 'rgb(255, 165, 0)', + 'orangered': 'rgb(255, 69, 0)', + 'orchid': 'rgb(218, 112, 214)', + 'palegoldenrod': 'rgb(238, 232, 170)', + 'palegreen': 'rgb(152, 251, 152)', + 'paleturquoise': 'rgb(175, 238, 238)', + 'palevioletred': 'rgb(219, 112, 147)', + 'papayawhip': 'rgb(255, 239, 213)', + 'peachpuff': 'rgb(255, 218, 185)', + 'peru': 'rgb(205, 133, 63)', + 'pink': 'rgb(255, 192, 203)', + 'plum': 'rgb(221, 160, 221)', + 'powderblue': 'rgb(176, 224, 230)', + 'purple': 'rgb(128, 0, 128)', + 'red': 'rgb(255, 0, 0)', + 'rosybrown': 'rgb(188, 143, 143)', + 'royalblue': 'rgb( 65, 105, 225)', + 'saddlebrown': 'rgb(139, 69, 19)', + 'salmon': 'rgb(250, 128, 114)', + 'sandybrown': 'rgb(244, 164, 96)', + 'seagreen': 'rgb( 46, 139, 87)', + 'seashell': 'rgb(255, 245, 238)', + 'sienna': 'rgb(160, 82, 45)', + 'silver': 'rgb(192, 192, 192)', + 'skyblue': 'rgb(135, 206, 235)', + 'slateblue': 'rgb(106, 90, 205)', + 'slategray': 'rgb(112, 128, 144)', + 'slategrey': 'rgb(112, 128, 144)', + 'snow': 'rgb(255, 250, 250)', + 'springgreen': 'rgb( 0, 255, 127)', + 'steelblue': 'rgb( 70, 130, 180)', + 'tan': 'rgb(210, 180, 140)', + 'teal': 'rgb( 0, 128, 128)', + 'thistle': 'rgb(216, 191, 216)', + 'tomato': 'rgb(255, 99, 71)', + 'turquoise': 'rgb( 64, 224, 208)', + 'violet': 'rgb(238, 130, 238)', + 'wheat': 'rgb(245, 222, 179)', + 'white': 'rgb(255, 255, 255)', + 'whitesmoke': 'rgb(245, 245, 245)', + 'yellow': 'rgb(255, 255, 0)', + 'yellowgreen': 'rgb(154, 205, 50)', + } -# A list of default poperties that are safe to remove -# -# Sources for this list: -# https://www.w3.org/TR/SVG/propidx.html (implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) -# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) -# -default_properties = { # excluded all properties with 'auto' as default - # SVG 1.1 presentation attributes - 'baseline-shift': 'baseline', - 'clip-path': 'none', - 'clip-rule': 'nonzero', - 'color': '#000', - 'color-interpolation-filters': 'linearRGB', - 'color-interpolation': 'sRGB', - 'direction': 'ltr', - 'display': 'inline', - 'enable-background': 'accumulate', - 'fill': '#000', - 'fill-opacity': '1', - 'fill-rule': 'nonzero', - 'filter': 'none', - 'flood-color': '#000', - 'flood-opacity': '1', - 'font-size-adjust': 'none', - 'font-size': 'medium', - 'font-stretch': 'normal', - 'font-style': 'normal', - 'font-variant': 'normal', - 'font-weight': 'normal', - 'glyph-orientation-horizontal': '0deg', - 'letter-spacing': 'normal', - 'lighting-color': '#fff', - 'marker': 'none', - 'marker-start': 'none', - 'marker-mid': 'none', - 'marker-end': 'none', - 'mask': 'none', - 'opacity': '1', - 'pointer-events': 'visiblePainted', - 'stop-color': '#000', - 'stop-opacity': '1', - 'stroke': 'none', - 'stroke-dasharray': 'none', - 'stroke-dashoffset': '0', - 'stroke-linecap': 'butt', - 'stroke-linejoin': 'miter', - 'stroke-miterlimit': '4', - 'stroke-opacity': '1', - 'stroke-width': '1', - 'text-anchor': 'start', - 'text-decoration': 'none', - 'unicode-bidi': 'normal', - 'visibility': 'visible', - 'word-spacing': 'normal', - 'writing-mode': 'lr-tb', - # SVG 1.2 tiny properties - 'audio-level': '1', - 'solid-color': '#000', - 'solid-opacity': '1', - 'text-align': 'start', - 'vector-effect': 'none', - 'viewport-fill': 'none', - 'viewport-fill-opacity': '1', -} - - -def is_same_sign(a, b): - return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) - - -def is_same_direction(x1, y1, x2, y2): - if is_same_sign(x1, x2) and is_same_sign(y1, y2): - diff = y1/x1 - y2/x2 - return scouringContext.plus(1 + diff) == 1 - else: - return False +default_attributes = { # excluded all attributes with 'auto' as default + # SVG 1.1 presentation attributes + 'baseline-shift': 'baseline', + 'clip-path': 'none', + 'clip-rule': 'nonzero', + 'color': '#000', + 'color-interpolation-filters': 'linearRGB', + 'color-interpolation': 'sRGB', + 'direction': 'ltr', + 'display': 'inline', + 'enable-background': 'accumulate', + 'fill': '#000', + 'fill-opacity': '1', + 'fill-rule': 'nonzero', + 'filter': 'none', + 'flood-color': '#000', + 'flood-opacity': '1', + 'font-size-adjust': 'none', + 'font-size': 'medium', + 'font-stretch': 'normal', + 'font-style': 'normal', + 'font-variant': 'normal', + 'font-weight': 'normal', + 'glyph-orientation-horizontal': '0deg', + 'letter-spacing': 'normal', + 'lighting-color': '#fff', + 'marker': 'none', + 'marker-start': 'none', + 'marker-mid': 'none', + 'marker-end': 'none', + 'mask': 'none', + 'opacity': '1', + 'pointer-events': 'visiblePainted', + 'stop-color': '#000', + 'stop-opacity': '1', + 'stroke': 'none', + 'stroke-dasharray': 'none', + 'stroke-dashoffset': '0', + 'stroke-linecap': 'butt', + 'stroke-linejoin': 'miter', + 'stroke-miterlimit': '4', + 'stroke-opacity': '1', + 'stroke-width': '1', + 'text-anchor': 'start', + 'text-decoration': 'none', + 'unicode-bidi': 'normal', + 'visibility': 'visible', + 'word-spacing': 'normal', + 'writing-mode': 'lr-tb', + # SVG 1.2 tiny properties + 'audio-level': '1', + 'solid-color': '#000', + 'solid-opacity': '1', + 'text-align': 'start', + 'vector-effect': 'none', + 'viewport-fill': 'none', + 'viewport-fill-opacity': '1', + } +def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+") number = re.compile(r"[-+]?(\d*\.?)?\d+") sciExponent = re.compile(r"[eE]([-+]?\d+)") unit = re.compile("(em|ex|px|pt|pc|cm|mm|in|%){1,1}$") - class Unit(object): - # Integer constants for units. - INVALID = -1 - NONE = 0 - PCT = 1 - PX = 2 - PT = 3 - PC = 4 - EM = 5 - EX = 6 - CM = 7 - MM = 8 - IN = 9 + # Integer constants for units. + INVALID = -1 + NONE = 0 + PCT = 1 + PX = 2 + PT = 3 + PC = 4 + EM = 5 + EX = 6 + CM = 7 + MM = 8 + IN = 9 - # String to Unit. Basically, converts unit strings to their integer constants. - s2u = { - '': NONE, - '%': PCT, - 'px': PX, - 'pt': PT, - 'pc': PC, - 'em': EM, - 'ex': EX, - 'cm': CM, - 'mm': MM, - 'in': IN, - } + # String to Unit. Basically, converts unit strings to their integer constants. + s2u = { + '': NONE, + '%': PCT, + 'px': PX, + 'pt': PT, + 'pc': PC, + 'em': EM, + 'ex': EX, + 'cm': CM, + 'mm': MM, + 'in': IN, + } - # Unit to String. Basically, converts unit integer constants to their corresponding strings. - u2s = { - NONE: '', - PCT: '%', - PX: 'px', - PT: 'pt', - PC: 'pc', - EM: 'em', - EX: 'ex', - CM: 'cm', - MM: 'mm', - IN: 'in', - } + # Unit to String. Basically, converts unit integer constants to their corresponding strings. + u2s = { + NONE: '', + PCT: '%', + PX: 'px', + PT: 'pt', + PC: 'pc', + EM: 'em', + EX: 'ex', + CM: 'cm', + MM: 'mm', + IN: 'in', + } # @staticmethod - def get(unitstr): - if unitstr is None: - return Unit.NONE - try: - return Unit.s2u[unitstr] - except KeyError: - return Unit.INVALID + def get(unitstr): + if unitstr is None: return Unit.NONE + try: + return Unit.s2u[unitstr] + except KeyError: + return Unit.INVALID # @staticmethod - def str(unitint): - try: - return Unit.u2s[unitint] - except KeyError: - return 'INVALID' - - get = staticmethod(get) - str = staticmethod(str) + def str(unitint): + try: + return Unit.u2s[unitint] + except KeyError: + return 'INVALID' + get = staticmethod(get) + str = staticmethod(str) class SVGLength(object): + def __init__(self, str): + try: # simple unitless and no scientific notation + self.value = float(str) + if int(self.value) == self.value: + self.value = int(self.value) + self.units = Unit.NONE + except ValueError: + # we know that the length string has an exponent, a unit, both or is invalid - def __init__(self, str): - try: # simple unitless and no scientific notation - self.value = float(str) - if int(self.value) == self.value: - self.value = int(self.value) - self.units = Unit.NONE - except ValueError: - # we know that the length string has an exponent, a unit, both or is invalid + # parse out number, exponent and unit + self.value = 0 + unitBegin = 0 + scinum = scinumber.match(str) + if scinum != None: + # this will always match, no need to check it + numMatch = number.match(str) + expMatch = sciExponent.search(str, numMatch.start(0)) + self.value = (float(numMatch.group(0)) * + 10 ** float(expMatch.group(1))) + unitBegin = expMatch.end(1) + else: + # unit or invalid + numMatch = number.match(str) + if numMatch != None: + self.value = float(numMatch.group(0)) + unitBegin = numMatch.end(0) - # parse out number, exponent and unit + if int(self.value) == self.value: + self.value = int(self.value) + + if unitBegin != 0 : + unitMatch = unit.search(str, unitBegin) + if unitMatch != None : + self.units = Unit.get(unitMatch.group(0)) + + # invalid + else: + # TODO: this needs to set the default for the given attribute (how?) self.value = 0 - unitBegin = 0 - scinum = scinumber.match(str) - if scinum is not None: - # this will always match, no need to check it - numMatch = number.match(str) - expMatch = sciExponent.search(str, numMatch.start(0)) - self.value = (float(numMatch.group(0)) * - 10 ** float(expMatch.group(1))) - unitBegin = expMatch.end(1) - else: - # unit or invalid - numMatch = number.match(str) - if numMatch is not None: - self.value = float(numMatch.group(0)) - unitBegin = numMatch.end(0) - - if int(self.value) == self.value: - self.value = int(self.value) - - if unitBegin != 0: - unitMatch = unit.search(str, unitBegin) - if unitMatch is not None: - self.units = Unit.get(unitMatch.group(0)) - - # invalid - else: - # TODO: this needs to set the default for the given attribute (how?) - self.value = 0 - self.units = Unit.INVALID - + self.units = Unit.INVALID def findElementsWithId(node, elems=None): - """ - Returns all elements with id attributes - """ - if elems is None: - elems = {} - id = node.getAttribute('id') - if id != '': - elems[id] = node - if node.hasChildNodes(): - for child in node.childNodes: - # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html - # we are only really interested in nodes of type Element (1) - if child.nodeType == Node.ELEMENT_NODE: - findElementsWithId(child, elems) - return elems - - -referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-end', 'marker-mid'] + """ + Returns all elements with id attributes + """ + if elems is None: + elems = {} + id = node.getAttribute('id') + if id != '' : + elems[id] = node + if node.hasChildNodes() : + for child in node.childNodes: + # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html + # we are only really interested in nodes of type Element (1) + if child.nodeType == 1 : + findElementsWithId(child, elems) + return elems +referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', + 'marker-end', 'marker-mid'] def findReferencedElements(node, ids=None): - """ - Returns IDs of all referenced elements - - node is the node at which to start the search. - - returns a map which has the id as key and - each value is is a set of nodes + """ + Returns the number of times an ID is referenced as well as all elements + that reference it. node is the node at which to start the search. The + return value is a map which has the id as key and each value is an array + where the first value is a count and the second value is a list of nodes + that referenced it. - Currently looks at 'xlink:href' and all attributes in 'referencingProps' - """ - global referencingProps - if ids is None: - ids = {} - # TODO: input argument ids is clunky here (see below how it is called) - # GZ: alternative to passing dict, use **kwargs + Currently looks at fill, stroke, clip-path, mask, marker, and + xlink:href attributes. + """ + global referencingProps + if ids is None: + ids = {} + # TODO: input argument ids is clunky here (see below how it is called) + # GZ: alternative to passing dict, use **kwargs - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # one stretch of text, please! (we could use node.normalize(), but - # this actually modifies the node, and we don't want to keep - # whitespace around if there's any) - stylesheet = "".join(child.nodeValue for child in node.childNodes) - if stylesheet != '': - cssRules = parseCssString(stylesheet) - for rule in cssRules: - for propname in rule['properties']: - propval = rule['properties'][propname] - findReferencingProperty(node, propname, propval, ids) - return ids + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # one stretch of text, please! (we could use node.normalize(), but + # this actually modifies the node, and we don't want to keep + # whitespace around if there's any) + stylesheet = "".join([child.nodeValue for child in node.childNodes]) + if stylesheet != '': + cssRules = parseCssString(stylesheet) + for rule in cssRules: + for propname in rule['properties']: + propval = rule['properties'][propname] + findReferencingProperty(node, propname, propval, ids) + return ids - # else if xlink:href is set, then grab the id - href = node.getAttributeNS(NS['XLINK'], 'href') - if href != '' and len(href) > 1 and href[0] == '#': - # we remove the hash mark from the beginning of the id - id = href[1:] - if id in ids: - ids[id].add(node) - else: - ids[id] = {node} + # else if xlink:href is set, then grab the id + href = node.getAttributeNS(NS['XLINK'],'href') + if href != '' and len(href) > 1 and href[0] == '#': + # we remove the hash mark from the beginning of the id + id = href[1:] + if id in ids: + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] - # now get all style properties and the fill, stroke, filter attributes - styles = node.getAttribute('style').split(';') + # now get all style properties and the fill, stroke, filter attributes + styles = node.getAttribute('style').split(';') + for attr in referencingProps: + styles.append(':'.join([attr, node.getAttribute(attr)])) - for style in styles: - propval = style.split(':') - if len(propval) == 2: - prop = propval[0].strip() - val = propval[1].strip() - findReferencingProperty(node, prop, val, ids) - - for attr in referencingProps: - val = node.getAttribute(attr).strip() - if not val: - continue - findReferencingProperty(node, attr, val, ids) - - if node.hasChildNodes(): - for child in node.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - findReferencedElements(child, ids) - return ids + for style in styles: + propval = style.split(':') + if len(propval) == 2 : + prop = propval[0].strip() + val = propval[1].strip() + findReferencingProperty(node, prop, val, ids) + if node.hasChildNodes() : + for child in node.childNodes: + if child.nodeType == 1 : + findReferencedElements(child, ids) + return ids def findReferencingProperty(node, prop, val, ids): - global referencingProps - if prop in referencingProps and val != '': - if len(val) >= 7 and val[0:5] == 'url(#': - id = val[5:val.find(')')] - if id in ids: - ids[id].add(node) + global referencingProps + if prop in referencingProps and val != '' : + if len(val) >= 7 and val[0:5] == 'url(#' : + id = val[5:val.find(')')] + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + # if the url has a quote in it, we need to compensate + elif len(val) >= 8 : + id = None + # double-quote + if val[0:6] == 'url("#' : + id = val[6:val.find('")')] + # single-quote + elif val[0:6] == "url('#" : + id = val[6:val.find("')")] + if id != None: + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) else: - ids[id] = {node} - # if the url has a quote in it, we need to compensate - elif len(val) >= 8: - id = None - # double-quote - if val[0:6] == 'url("#': - id = val[6:val.find('")')] - # single-quote - elif val[0:6] == "url('#": - id = val[6:val.find("')")] - if id is not None: - if id in ids: - ids[id].add(node) - else: - ids[id] = {node} + ids[id] = [1,[node]] +numIDsRemoved = 0 +numElemsRemoved = 0 +numAttrsRemoved = 0 +numRastersEmbedded = 0 +numPathSegmentsReduced = 0 +numCurvesStraightened = 0 +numBytesSavedInPathData = 0 +numBytesSavedInColors = 0 +numBytesSavedInIDs = 0 +numBytesSavedInLengths = 0 +numBytesSavedInTransforms = 0 +numPointsRemovedFromPolygon = 0 +numCommentBytes = 0 -def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): - if elemsToRemove is None: - elemsToRemove = [] +def removeUnusedDefs(doc, defElem, elemsToRemove=None): + if elemsToRemove is None: + elemsToRemove = [] - # removeUnusedDefs do not change the XML itself; therefore there is no point in - # recomputing findReferencedElements when we recurse into child nodes. - if referencedIDs is None: - referencedIDs = findReferencedElements(doc.documentElement) + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) - keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] - for elem in defElem.childNodes: - # only look at it if an element and not referenced anywhere else - if elem.nodeType != Node.ELEMENT_NODE: - continue + keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] + for elem in defElem.childNodes: + # only look at it if an element and not referenced anywhere else + if elem.nodeType == 1 and (elem.getAttribute('id') == '' or \ + (not elem.getAttribute('id') in referencedIDs)): - elem_id = elem.getAttribute('id') + # we only inspect the children of a group in a defs if the group + # is not referenced anywhere else + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) + # we only remove if it is not one of our tags we always keep (see above) + elif not elem.nodeName in keepTags: + elemsToRemove.append(elem) + return elemsToRemove - if elem_id == '' or elem_id not in referencedIDs: - # we only inspect the children of a group in a defs if the group - # is not referenced anywhere else - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove, referencedIDs=referencedIDs) - # we only remove if it is not one of our tags we always keep (see above) - elif elem.nodeName not in keepTags: - elemsToRemove.append(elem) - return elemsToRemove +def removeUnreferencedElements(doc, keepDefs): + """ + Removes all unreferenced elements except for <svg>, <font>, <metadata>, <title>, and <desc>. + Also vacuums the defs of any non-referenced renderable elements. + Returns the number of unreferenced elements removed from the document. + """ + global numElemsRemoved + num = 0 -def remove_unreferenced_elements(doc, keepDefs, stats): - """ - Removes all unreferenced elements except for <svg>, <font>, <metadata>, <title>, and <desc>. - Also vacuums the defs of any non-referenced renderable elements. + # Remove certain unreferenced elements outside of defs + removeTags = ['linearGradient', 'radialGradient', 'pattern'] + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) - Returns the number of unreferenced elements removed from the document. - """ - num = 0 + for id in identifiedElements: + if not id in referencedIDs: + goner = identifiedElements[id] + if goner != None and goner.parentNode != None and goner.nodeName in removeTags: + goner.parentNode.removeChild(goner) + num += 1 + numElemsRemoved += 1 - # Remove certain unreferenced elements outside of defs - removeTags = ['linearGradient', 'radialGradient', 'pattern'] - identifiedElements = findElementsWithId(doc.documentElement) - referencedIDs = findReferencedElements(doc.documentElement) + if not keepDefs: + # Remove most unreferenced elements inside defs + defs = doc.documentElement.getElementsByTagName('defs') + for aDef in defs: + elemsToRemove = removeUnusedDefs(doc, aDef) + for elem in elemsToRemove: + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num - if not keepDefs: - # Remove most unreferenced elements inside defs - defs = doc.documentElement.getElementsByTagName('defs') - for aDef in defs: - elemsToRemove = removeUnusedDefs(doc, aDef, referencedIDs=referencedIDs) - for elem in elemsToRemove: - elem.parentNode.removeChild(elem) - stats.num_elements_removed += len(elemsToRemove) - num += len(elemsToRemove) +def shortenIDs(doc, prefix, unprotectedElements=None): + """ + Shortens ID names used in the document. ID names referenced the most often are assigned the + shortest ID names. + If the list unprotectedElements is provided, only IDs from this list will be shortened. - for id in identifiedElements: - if id not in referencedIDs: - goner = identifiedElements[id] - if (goner is not None and goner.nodeName in removeTags - and goner.parentNode is not None - and goner.parentNode.tagName != 'defs'): - goner.parentNode.removeChild(goner) - num += 1 - stats.num_elements_removed += 1 + Returns the number of bytes saved by shortening ID names in the document. + """ + num = 0 - return num + identifiedElements = findElementsWithId(doc.documentElement) + if unprotectedElements is None: + unprotectedElements = identifiedElements + referencedIDs = findReferencedElements(doc.documentElement) + # Make idList (list of idnames) sorted by reference count + # descending, so the highest reference count is first. + # First check that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + idList = [(referencedIDs[rid][0], rid) for rid in referencedIDs + if rid in unprotectedElements] + idList.sort(reverse=True) + idList = [rid for count, rid in idList] -def shortenIDs(doc, prefix, options): - """ - Shortens ID names used in the document. ID names referenced the most often are assigned the - shortest ID names. + curIdNum = 1 - Returns the number of bytes saved by shortening ID names in the document. - """ - num = 0 - - identifiedElements = findElementsWithId(doc.documentElement) - # This map contains maps the (original) ID to the nodes referencing it. - # At the end of this function, it will no longer be valid and while we - # could keep it up to date, it will complicate the code for no gain - # (as we do not reuse the data structure beyond this function). - referencedIDs = findReferencedElements(doc.documentElement) - - # Make idList (list of idnames) sorted by reference count - # descending, so the highest reference count is first. - # First check that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - idList = [(len(referencedIDs[rid]), rid) for rid in referencedIDs - if rid in identifiedElements] - idList.sort(reverse=True) - idList = [rid for count, rid in idList] - - # Add unreferenced IDs to end of idList in arbitrary order - idList.extend([rid for rid in identifiedElements if rid not in idList]) - # Ensure we do not reuse a protected ID by accident - protectedIDs = protected_ids(identifiedElements, options) - # IDs that have been allocated and should not be remapped. - consumedIDs = set() - - # List of IDs that need to be assigned a new ID. The list is ordered - # such that earlier entries will be assigned a shorter ID than those - # later in the list. IDs in this list *can* obtain an ID that is - # longer than they already are. - need_new_id = [] - - id_allocations = list(compute_id_lengths(len(idList) + 1)) - # Reverse so we can use it as a stack and still work from "shortest to - # longest" ID. - id_allocations.reverse() - - # Here we loop over all current IDs (that we /might/ want to remap) - # and group them into two. 1) The IDs that already have a perfect - # length (these are added to consumedIDs) and 2) the IDs that need - # to change length (these are appended to need_new_id). - optimal_id_length, id_use_limit = 0, 0 - for current_id in idList: - # If we are out of IDs of the current length, then move on - # to the next length - if id_use_limit < 1: - optimal_id_length, id_use_limit = id_allocations.pop() - # Reserve an ID from this length - id_use_limit -= 1 - # We check for strictly equal to optimal length because our ID - # remapping may have to assign one node a longer ID because - # another node needs a shorter ID. - if len(current_id) == optimal_id_length: - # This rid is already of optimal length - lets just keep it. - consumedIDs.add(current_id) - else: - # Needs a new (possibly longer) ID. - need_new_id.append(current_id) - - curIdNum = 1 - - for old_id in need_new_id: - new_id = intToID(curIdNum, prefix) - - # Skip ahead if the new ID has already been used or is protected. - while new_id in protectedIDs or new_id in consumedIDs: + for rid in idList: + curId = intToID(curIdNum, prefix) + # First make sure that *this* element isn't already using + # the ID name we want to give it. + if curId != rid: + # Then, skip ahead if the new ID is already in identifiedElement. + while curId in identifiedElements: curIdNum += 1 - new_id = intToID(curIdNum, prefix) - - # Now that we have found the first available ID, do the remap. - num += renameID(old_id, new_id, identifiedElements, referencedIDs.get(old_id)) - curIdNum += 1 - - return num - - -def compute_id_lengths(highest): - """Compute how many IDs are available of a given size - - Example: - >>> lengths = list(compute_id_lengths(512)) - >>> lengths - [(1, 26), (2, 676)] - >>> total_limit = sum(x[1] for x in lengths) - >>> total_limit - 702 - >>> intToID(total_limit, '') - 'zz' - - Which tells us that we got 26 IDs of length 1 and up to 676 IDs of length two - if we need to allocate 512 IDs. - - :param highest: Highest ID that need to be allocated - :return: An iterator that returns tuples of (id-length, use-limit). The - use-limit applies only to the given id-length (i.e. it is excluding IDs - of shorter length). Note that the sum of the use-limit values is always - equal to or greater than the highest param. - """ - step = 26 - id_length = 0 - use_limit = 1 - while highest: - id_length += 1 - use_limit *= step - yield (id_length, use_limit) - highest = int((highest - 1) / step) + curId = intToID(curIdNum, prefix) + # Then go rename it. + num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + curIdNum += 1 + return num def intToID(idnum, prefix): - """ - Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, - then from aa to az, ba to bz, etc., until zz. - """ - rid = '' + """ + Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, + then from aa to az, ba to bz, etc., until zz. + """ + rid = '' - while idnum > 0: - idnum -= 1 - rid = chr((idnum % 26) + ord('a')) + rid - idnum = int(idnum / 26) + while idnum > 0: + idnum -= 1 + rid = chr((idnum % 26) + ord('a')) + rid + idnum = int(idnum / 26) - return prefix + rid + return prefix + rid +def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): + """ + Changes the ID name from idFrom to idTo, on the declaring element + as well as all references in the document doc. -def renameID(idFrom, idTo, identifiedElements, referringNodes): - """ - Changes the ID name from idFrom to idTo, on the declaring element - as well as all nodes in referringNodes. + Updates identifiedElements and referencedIDs. + Does not handle the case where idTo is already the ID name + of another element in doc. - Updates identifiedElements. + Returns the number of bytes saved by this replacement. + """ - Returns the number of bytes saved by this replacement. - """ + num = 0 - num = 0 + definingNode = identifiedElements[idFrom] + definingNode.setAttribute("id", idTo) + del identifiedElements[idFrom] + identifiedElements[idTo] = definingNode - definingNode = identifiedElements[idFrom] - definingNode.setAttribute("id", idTo) - num += len(idFrom) - len(idTo) + referringNodes = referencedIDs[idFrom] - # Update references to renamed node - if referringNodes is not None: + # Look for the idFrom ID name in each of the referencing elements, + # exactly like findReferencedElements would. + # Cyn: Duplicated processing! - # Look for the idFrom ID name in each of the referencing elements, - # exactly like findReferencedElements would. - # Cyn: Duplicated processing! + for node in referringNodes[1]: + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # node.firstChild will be either a CDATA or a Text node now + if node.firstChild != None: + # concatenate the value of all children, in case + # there's a CDATASection node surrounded by whitespace + # nodes + # (node.normalize() will NOT work here, it only acts on Text nodes) + oldValue = "".join([child.nodeValue for child in node.childNodes]) + # not going to reparse the whole thing + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') + # and now replace all the children with this new stylesheet. + # again, this is in case the stylesheet was a CDATASection + node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] + num += len(oldValue) - len(newValue) - for node in referringNodes: - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # node.firstChild will be either a CDATA or a Text node now - if node.firstChild is not None: - # concatenate the value of all children, in case - # there's a CDATASection node surrounded by whitespace - # nodes - # (node.normalize() will NOT work here, it only acts on Text nodes) - oldValue = "".join(child.nodeValue for child in node.childNodes) - # not going to reparse the whole thing - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') - # and now replace all the children with this new stylesheet. - # again, this is in case the stylesheet was a CDATASection - node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] - num += len(oldValue) - len(newValue) + # if xlink:href is set to #idFrom, then change the id + href = node.getAttributeNS(NS['XLINK'],'href') + if href == '#' + idFrom: + node.setAttributeNS(NS['XLINK'],'href', '#' + idTo) + num += len(idFrom) - len(idTo) - # if xlink:href is set to #idFrom, then change the id - href = node.getAttributeNS(NS['XLINK'], 'href') - if href == '#' + idFrom: - node.setAttributeNS(NS['XLINK'], 'href', '#' + idTo) - num += len(idFrom) - len(idTo) + # if the style has url(#idFrom), then change the id + styles = node.getAttribute('style') + if styles != '': + newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute('style', newValue) + num += len(styles) - len(newValue) - # if the style has url(#idFrom), then change the id - styles = node.getAttribute('style') - if styles != '': - newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute('style', newValue) - num += len(styles) - len(newValue) + # now try the fill, stroke, filter attributes + for attr in referencingProps: + oldValue = node.getAttribute(attr) + if oldValue != '': + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute(attr, newValue) + num += len(oldValue) - len(newValue) - # now try the fill, stroke, filter attributes - for attr in referencingProps: - oldValue = node.getAttribute(attr) - if oldValue != '': - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute(attr, newValue) - num += len(oldValue) - len(newValue) - - return num - - -def protected_ids(seenIDs, options): - """Return a list of protected IDs out of the seenIDs""" - protectedIDs = [] - if options.protect_ids_prefix or options.protect_ids_noninkscape or options.protect_ids_list: - protect_ids_prefixes = [] - protect_ids_list = [] - if options.protect_ids_list: - protect_ids_list = options.protect_ids_list.split(",") - if options.protect_ids_prefix: - protect_ids_prefixes = options.protect_ids_prefix.split(",") - for id in seenIDs: - protected = False - if options.protect_ids_noninkscape and not id[-1].isdigit(): - protected = True - elif protect_ids_list and id in protect_ids_list: - protected = True - elif protect_ids_prefixes: - if any(id.startswith(prefix) for prefix in protect_ids_prefixes): - protected = True - if protected: - protectedIDs.append(id) - return protectedIDs + del referencedIDs[idFrom] + referencedIDs[idTo] = referringNodes + return num def unprotected_ids(doc, options): - u"""Returns a list of unprotected IDs within the document doc.""" - identifiedElements = findElementsWithId(doc.documentElement) - protectedIDs = protected_ids(identifiedElements, options) - if protectedIDs: - for id in protectedIDs: - del identifiedElements[id] - return identifiedElements + u"""Returns a list of unprotected IDs within the document doc.""" + identifiedElements = findElementsWithId(doc.documentElement) + if not (options.protect_ids_noninkscape or + options.protect_ids_list or + options.protect_ids_prefix): + return identifiedElements + if options.protect_ids_list: + protect_ids_list = options.protect_ids_list.split(",") + if options.protect_ids_prefix: + protect_ids_prefixes = options.protect_ids_prefix.split(",") + for id in identifiedElements.keys(): + protected = False + if options.protect_ids_noninkscape and not id[-1].isdigit(): + protected = True + if options.protect_ids_list and id in protect_ids_list: + protected = True + if options.protect_ids_prefix: + for prefix in protect_ids_prefixes: + if id.startswith(prefix): + protected = True + if protected: + del identifiedElements[id] + return identifiedElements +def removeUnreferencedIDs(referencedIDs, identifiedElements): + """ + Removes the unreferenced ID attributes. -def remove_unreferenced_ids(referencedIDs, identifiedElements): - """ - Removes the unreferenced ID attributes. - - Returns the number of ID attributes removed - """ - keepTags = ['font'] - num = 0 - for id in identifiedElements: - node = identifiedElements[id] - if id not in referencedIDs and node.nodeName not in keepTags: - node.removeAttribute('id') - num += 1 - return num - + Returns the number of ID attributes removed + """ + global numIDsRemoved + keepTags = ['font'] + num = 0; + for id in identifiedElements.keys(): + node = identifiedElements[id] + if referencedIDs.has_key(id) == False and not node.nodeName in keepTags: + node.removeAttribute('id') + numIDsRemoved += 1 + num += 1 + return num def removeNamespacedAttributes(node, namespaces): - num = 0 - if node.nodeType == Node.ELEMENT_NODE: - # remove all namespace'd attributes from this element - attrList = node.attributes - attrsToRemove = [] - for attrNum in range(attrList.length): - attr = attrList.item(attrNum) - if attr is not None and attr.namespaceURI in namespaces: - attrsToRemove.append(attr.nodeName) - for attrName in attrsToRemove: - node.removeAttribute(attrName) - num += len(attrsToRemove) - - # now recurse for children - for child in node.childNodes: - num += removeNamespacedAttributes(child, namespaces) - return num + global numAttrsRemoved + num = 0 + if node.nodeType == 1 : + # remove all namespace'd attributes from this element + attrList = node.attributes + attrsToRemove = [] + for attrNum in xrange(attrList.length): + attr = attrList.item(attrNum) + if attr != None and attr.namespaceURI in namespaces: + attrsToRemove.append(attr.nodeName) + for attrName in attrsToRemove : + num += 1 + numAttrsRemoved += 1 + node.removeAttribute(attrName) + # now recurse for children + for child in node.childNodes: + num += removeNamespacedAttributes(child, namespaces) + return num def removeNamespacedElements(node, namespaces): - num = 0 - if node.nodeType == Node.ELEMENT_NODE: - # remove all namespace'd child nodes from this element - childList = node.childNodes - childrenToRemove = [] - for child in childList: - if child is not None and child.namespaceURI in namespaces: - childrenToRemove.append(child) - for child in childrenToRemove: - node.removeChild(child) - num += len(childrenToRemove) + global numElemsRemoved + num = 0 + if node.nodeType == 1 : + # remove all namespace'd child nodes from this element + childList = node.childNodes + childrenToRemove = [] + for child in childList: + if child != None and child.namespaceURI in namespaces: + childrenToRemove.append(child) + for child in childrenToRemove : + num += 1 + numElemsRemoved += 1 + node.removeChild(child) - # now recurse for children - for child in node.childNodes: - num += removeNamespacedElements(child, namespaces) - return num + # now recurse for children + for child in node.childNodes: + num += removeNamespacedElements(child, namespaces) + return num +def removeMetadataElements(doc): + global numElemsRemoved + num = 0 + # clone the list, as the tag list is live from the DOM + elementsToRemove = [element for element in doc.documentElement.getElementsByTagName('metadata')] -def remove_descriptive_elements(doc, options): - elementTypes = [] - if options.remove_descriptive_elements: - elementTypes.extend(("title", "desc", "metadata")) - else: - if options.remove_titles: - elementTypes.append("title") - if options.remove_descriptions: - elementTypes.append("desc") - if options.remove_metadata: - elementTypes.append("metadata") - if not elementTypes: - return 0 + for element in elementsToRemove: + element.parentNode.removeChild(element) + num += 1 + numElemsRemoved += 1 - elementsToRemove = [] - for elementType in elementTypes: - elementsToRemove.extend(doc.documentElement.getElementsByTagName(elementType)) + return num - for element in elementsToRemove: - element.parentNode.removeChild(element) +def removeNestedGroups(node): + """ + This walks further and further down the tree, removing groups + which do not have any attributes or a title/desc child and + promoting their children up one level + """ + global numElemsRemoved + num = 0 - return len(elementsToRemove) + groupsToRemove = [] + # Only consider <g> elements for promotion if this element isn't a <switch>. + # (partial fix for bug 594930, required by the SVG spec however) + if not (node.nodeType == 1 and node.nodeName == 'switch'): + for child in node.childNodes: + if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: + # only collapse group if it does not have a title or desc as a direct descendant, + for grandchild in child.childNodes: + if grandchild.nodeType == 1 and grandchild.namespaceURI == NS['SVG'] and \ + grandchild.nodeName in ['title','desc']: + break + else: + groupsToRemove.append(child) + for g in groupsToRemove: + while g.childNodes.length > 0: + g.parentNode.insertBefore(g.firstChild, g) + g.parentNode.removeChild(g) + numElemsRemoved += 1 + num += 1 -def g_tag_is_mergeable(node): - """Check if a <g> tag can be merged or not - - <g> tags with a title or descriptions should generally be left alone. - """ - if any( - True for n in node.childNodes - if n.nodeType == Node.ELEMENT_NODE and n.nodeName in ('title', 'desc') - and n.namespaceURI == NS['SVG'] - ): - return False - return True - - -def remove_nested_groups(node, stats): - """ - This walks further and further down the tree, removing groups - which do not have any attributes or a title/desc child and - promoting their children up one level - """ - num = 0 - - groupsToRemove = [] - # Only consider <g> elements for promotion if this element isn't a <switch>. - # (partial fix for bug 594930, required by the SVG spec however) - if not (node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'switch'): - for child in node.childNodes: - if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: - # only collapse group if it does not have a title or desc as a direct descendant, - if g_tag_is_mergeable(child): - groupsToRemove.append(child) - - for g in groupsToRemove: - while g.childNodes.length > 0: - g.parentNode.insertBefore(g.firstChild, g) - g.parentNode.removeChild(g) - - num += len(groupsToRemove) - stats.num_elements_removed += len(groupsToRemove) - - # now recurse for children - for child in node.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - num += remove_nested_groups(child, stats) - return num - + # now recurse for children + for child in node.childNodes: + if child.nodeType == 1: + num += removeNestedGroups(child) + return num def moveCommonAttributesToParentGroup(elem, referencedElements): - """ - This recursively calls this function on all children of the passed in element - and then iterates over all child elements and removes common inheritable attributes - from the children and places them in the parent group. But only if the parent contains - nothing but element children and whitespace. The attributes are only removed from the - children if the children are not referenced by other elements in the document. - """ - num = 0 + """ + This recursively calls this function on all children of the passed in element + and then iterates over all child elements and removes common inheritable attributes + from the children and places them in the parent group. But only if the parent contains + nothing but element children and whitespace. The attributes are only removed from the + children if the children are not referenced by other elements in the document. + """ + num = 0 - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - # only add and recurse if the child is not referenced elsewhere - if not child.getAttribute('id') in referencedElements: - childElements.append(child) - num += moveCommonAttributesToParentGroup(child, referencedElements) - # else if the parent has non-whitespace text children, do not - # try to move common attributes - elif child.nodeType == Node.TEXT_NODE and child.nodeValue.strip(): - return num + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + # only add and recurse if the child is not referenced elsewhere + if not child.getAttribute('id') in referencedElements: + childElements.append(child) + num += moveCommonAttributesToParentGroup(child, referencedElements) + # else if the parent has non-whitespace text children, do not + # try to move common attributes + elif child.nodeType == 3 and child.nodeValue.strip(): + return num - # only process the children if there are more than one element - if len(childElements) <= 1: - return num + # only process the children if there are more than one element + if len(childElements) <= 1: return num - commonAttrs = {} - # add all inheritable properties of the first child element - # FIXME: Note there is a chance that the first child is a set/animate in which case - # its fill attribute is not what we want to look at, we should look for the first - # non-animate/set element - attrList = childElements[0].attributes - for index in range(attrList.length): - attr = attrList.item(index) - # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html - # and http://www.w3.org/TR/SVGTiny12/attributeTable.html - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # we just add all the attributes from the first child - commonAttrs[attr.nodeName] = attr.nodeValue + commonAttrs = {} + # add all inheritable properties of the first child element + # FIXME: Note there is a chance that the first child is a set/animate in which case + # its fill attribute is not what we want to look at, we should look for the first + # non-animate/set element + attrList = childElements[0].attributes + for num in xrange(attrList.length): + attr = attrList.item(num) + # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html + # and http://www.w3.org/TR/SVGTiny12/attributeTable.html + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # we just add all the attributes from the first child + commonAttrs[attr.nodeName] = attr.nodeValue - # for each subsequent child element - for childNum in range(len(childElements)): - # skip first child - if childNum == 0: - continue + # for each subsequent child element + for childNum in xrange(len(childElements)): + # skip first child + if childNum == 0: + continue - child = childElements[childNum] - # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) - if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: - continue + child = childElements[childNum] + # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) + if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: + continue - distinctAttrs = [] - # loop through all current 'common' attributes - for name in commonAttrs: - # if this child doesn't match that attribute, schedule it for removal - if child.getAttribute(name) != commonAttrs[name]: - distinctAttrs.append(name) - # remove those attributes which are not common - for name in distinctAttrs: - del commonAttrs[name] + distinctAttrs = [] + # loop through all current 'common' attributes + for name in commonAttrs.keys(): + # if this child doesn't match that attribute, schedule it for removal + if child.getAttribute(name) != commonAttrs[name]: + distinctAttrs.append(name) + # remove those attributes which are not common + for name in distinctAttrs: + del commonAttrs[name] - # commonAttrs now has all the inheritable attributes which are common among all child elements - for name in commonAttrs: - for child in childElements: - child.removeAttribute(name) - elem.setAttribute(name, commonAttrs[name]) + # commonAttrs now has all the inheritable attributes which are common among all child elements + for name in commonAttrs.keys(): + for child in childElements: + child.removeAttribute(name) + elem.setAttribute(name, commonAttrs[name]) - # update our statistic (we remove N*M attributes and add back in M attributes) - num += (len(childElements) - 1) * len(commonAttrs) - return num + # update our statistic (we remove N*M attributes and add back in M attributes) + num += (len(childElements)-1) * len(commonAttrs) + return num +def createGroupsForCommonAttributes(elem): + """ + Creates <g> elements to contain runs of 3 or more + consecutive child elements having at least one common attribute. -def mergeSiblingGroupsWithCommonAttributes(elem): - """ - Merge two or more sibling <g> elements with the identical attributes. + Common attributes are not promoted to the <g> by this function. + This is handled by moveCommonAttributesToParentGroup. - This function acts recursively on the given element. - """ + If all children have a common attribute, an extra <g> is not created. - num = 0 - i = elem.childNodes.length - 1 - while i >= 0: - currentNode = elem.childNodes.item(i) - if currentNode.nodeType != Node.ELEMENT_NODE or currentNode.nodeName != 'g' or \ - currentNode.namespaceURI != NS['SVG']: - i -= 1 - continue - attributes = {a.nodeName: a.nodeValue for a in currentNode.attributes.values()} - if not attributes: - i -= 1 - continue - runStart, runEnd = i, i - runElements = 1 - while runStart > 0: - nextNode = elem.childNodes.item(runStart - 1) - if nextNode.nodeType == Node.ELEMENT_NODE: - if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: - break - nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} - if attributes != nextAttributes or not g_tag_is_mergeable(nextNode): - break - else: - runElements += 1 - runStart -= 1 + This function acts recursively on the given element. + """ + num = 0 + global numElemsRemoved + + # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement + # could be added here + # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be + for curAttr in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # Iterate through the children in reverse order, so item(i) for + # items we have yet to visit still returns the correct nodes. + curChild = elem.childNodes.length - 1 + while curChild >= 0: + childNode = elem.childNodes.item(curChild) + + if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '': + # We're in a possible run! Track the value and run length. + value = childNode.getAttribute(curAttr) + runStart, runEnd = curChild, curChild + # Run elements includes only element tags, no whitespace/comments/etc. + # Later, we calculate a run length which includes these. + runElements = 1 + + # Backtrack to get all the nodes having the same + # attribute value, preserving any nodes in-between. + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == 1: + if nextNode.getAttribute(curAttr) != value: break + else: + runElements += 1 + runStart -= 1 + else: runStart -= 1 + + if runElements >= 3: + # Include whitespace/comment/etc. nodes in the run. + while runEnd < elem.childNodes.length - 1: + if elem.childNodes.item(runEnd + 1).nodeType == 1: break + else: runEnd += 1 + + runLength = runEnd - runStart + 1 + if runLength == elem.childNodes.length: # Every child has this + # If the current parent is a <g> already, + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + # do not act altogether on this attribute; all the + # children have it in common. + # Let moveCommonAttributesToParentGroup do it. + curChild = -1 + continue + # otherwise, it might be an <svg> element, and + # even if all children have the same attribute value, + # it's going to be worth making the <g> since + # <svg> doesn't support attributes like 'stroke'. + # Fall through. + + # Create a <g> element from scratch. + # We need the Document for this. + document = elem.ownerDocument + group = document.createElementNS(NS['SVG'], 'g') + # Move the run of elements to the group. + # a) ADD the nodes to the new group. + group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] + for child in group.childNodes: + child.parentNode = group + # b) REMOVE the nodes from the element. + elem.childNodes[runStart:runEnd + 1] = [] + # Include the group in elem's children. + elem.childNodes.insert(runStart, group) + group.parentNode = elem + num += 1 + curChild = runStart - 1 + numElemsRemoved -= 1 else: - runStart -= 1 + curChild -= 1 + else: + curChild -= 1 - # Next loop will start from here - i = runStart - 1 - - if runElements < 2: - continue - - # Find the <g> entry that starts the run (we might have run - # past it into a text node or a comment node. - while True: - node = elem.childNodes.item(runStart) - if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: - break - runStart += 1 - primaryGroup = elem.childNodes.item(runStart) - runStart += 1 - nodes = elem.childNodes[runStart:runEnd+1] - for node in nodes: - if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: - # Merge - for child in node.childNodes[:]: - primaryGroup.appendChild(child) - elem.removeChild(node).unlink() - else: - primaryGroup.appendChild(node) - - # each child gets the same treatment, recursively - for childNode in elem.childNodes: - if childNode.nodeType == Node.ELEMENT_NODE: - num += mergeSiblingGroupsWithCommonAttributes(childNode) - - return num - - -def create_groups_for_common_attributes(elem, stats): - """ - Creates <g> elements to contain runs of 3 or more - consecutive child elements having at least one common attribute. - - Common attributes are not promoted to the <g> by this function. - This is handled by moveCommonAttributesToParentGroup. - - If all children have a common attribute, an extra <g> is not created. - - This function acts recursively on the given element. - """ - - # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement - # could be added here - # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be - for curAttr in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # Iterate through the children in reverse order, so item(i) for - # items we have yet to visit still returns the correct nodes. - curChild = elem.childNodes.length - 1 - while curChild >= 0: - childNode = elem.childNodes.item(curChild) - - if ( - childNode.nodeType == Node.ELEMENT_NODE and - childNode.getAttribute(curAttr) != '' and - childNode.nodeName in [ - # only attempt to group elements that the content model allows to be children of a <g> - - # SVG 1.1 (see https://www.w3.org/TR/SVG/struct.html#GElement) - 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'set', # animation elements - 'desc', 'metadata', 'title', # descriptive elements - 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect', # shape elements - 'defs', 'g', 'svg', 'symbol', 'use', # structural elements - 'linearGradient', 'radialGradient', # gradient elements - 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', 'foreignObject', 'image', 'marker', 'mask', - 'pattern', 'script', 'style', 'switch', 'text', 'view', - - # SVG 1.2 (see https://www.w3.org/TR/SVGTiny12/elementTable.html) - 'animation', 'audio', 'discard', 'handler', 'listener', - 'prefetch', 'solidColor', 'textArea', 'video' - ] - ): - # We're in a possible run! Track the value and run length. - value = childNode.getAttribute(curAttr) - runStart, runEnd = curChild, curChild - # Run elements includes only element tags, no whitespace/comments/etc. - # Later, we calculate a run length which includes these. - runElements = 1 - - # Backtrack to get all the nodes having the same - # attribute value, preserving any nodes in-between. - while runStart > 0: - nextNode = elem.childNodes.item(runStart - 1) - if nextNode.nodeType == Node.ELEMENT_NODE: - if nextNode.getAttribute(curAttr) != value: - break - else: - runElements += 1 - runStart -= 1 - else: - runStart -= 1 - - if runElements >= 3: - # Include whitespace/comment/etc. nodes in the run. - while runEnd < elem.childNodes.length - 1: - if elem.childNodes.item(runEnd + 1).nodeType == Node.ELEMENT_NODE: - break - else: - runEnd += 1 - - runLength = runEnd - runStart + 1 - if runLength == elem.childNodes.length: # Every child has this - # If the current parent is a <g> already, - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - # do not act altogether on this attribute; all the - # children have it in common. - # Let moveCommonAttributesToParentGroup do it. - curChild = -1 - continue - # otherwise, it might be an <svg> element, and - # even if all children have the same attribute value, - # it's going to be worth making the <g> since - # <svg> doesn't support attributes like 'stroke'. - # Fall through. - - # Create a <g> element from scratch. - # We need the Document for this. - document = elem.ownerDocument - group = document.createElementNS(NS['SVG'], 'g') - # Move the run of elements to the group. - # a) ADD the nodes to the new group. - group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] - for child in group.childNodes: - child.parentNode = group - # b) REMOVE the nodes from the element. - elem.childNodes[runStart:runEnd + 1] = [] - # Include the group in elem's children. - elem.childNodes.insert(runStart, group) - group.parentNode = elem - curChild = runStart - 1 - stats.num_elements_removed -= 1 - else: - curChild -= 1 - else: - curChild -= 1 - - # each child gets the same treatment, recursively - for childNode in elem.childNodes: - if childNode.nodeType == Node.ELEMENT_NODE: - create_groups_for_common_attributes(childNode, stats) + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == 1: + num += createGroupsForCommonAttributes(childNode) + return num def removeUnusedAttributesOnParent(elem): - """ - This recursively calls this function on all children of the element passed in, - then removes any unused attributes on this elem if none of the children inherit it - """ - num = 0 + """ + This recursively calls this function on all children of the element passed in, + then removes any unused attributes on this elem if none of the children inherit it + """ + num = 0 - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - childElements.append(child) - num += removeUnusedAttributesOnParent(child) + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + childElements.append(child) + num += removeUnusedAttributesOnParent(child) - # only process the children if there are more than one element - if len(childElements) <= 1: - return num + # only process the children if there are more than one element + if len(childElements) <= 1: return num - # get all attribute values on this parent - attrList = elem.attributes - unusedAttrs = {} - for index in range(attrList.length): - attr = attrList.item(index) - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - unusedAttrs[attr.nodeName] = attr.nodeValue + # get all attribute values on this parent + attrList = elem.attributes + unusedAttrs = {} + for num in xrange(attrList.length): + attr = attrList.item(num) + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + unusedAttrs[attr.nodeName] = attr.nodeValue - # for each child, if at least one child inherits the parent's attribute, then remove - for child in childElements: - inheritedAttrs = [] - for name in unusedAttrs: - val = child.getAttribute(name) - if val == '' or val == 'inherit': - inheritedAttrs.append(name) - for a in inheritedAttrs: - del unusedAttrs[a] + # for each child, if at least one child inherits the parent's attribute, then remove + for childNum in xrange(len(childElements)): + child = childElements[childNum] + inheritedAttrs = [] + for name in unusedAttrs.keys(): + val = child.getAttribute(name) + if val == '' or val == None or val == 'inherit': + inheritedAttrs.append(name) + for a in inheritedAttrs: + del unusedAttrs[a] - # unusedAttrs now has all the parent attributes that are unused - for name in unusedAttrs: - elem.removeAttribute(name) - num += 1 + # unusedAttrs now has all the parent attributes that are unused + for name in unusedAttrs.keys(): + elem.removeAttribute(name) + num += 1 - return num + return num +def removeDuplicateGradientStops(doc): + global numElemsRemoved + num = 0 -def remove_duplicate_gradient_stops(doc, stats): - num = 0 + for gradType in ['linearGradient', 'radialGradient']: + for grad in doc.getElementsByTagName(gradType): + stops = {} + stopsToRemove = [] + for stop in grad.getElementsByTagName('stop'): + # convert percentages into a floating point number + offsetU = SVGLength(stop.getAttribute('offset')) + if offsetU.units == Unit.PCT: + offset = offsetU.value / 100.0 + elif offsetU.units == Unit.NONE: + offset = offsetU.value + else: + offset = 0 + # set the stop offset value to the integer or floating point equivalent + if int(offset) == offset: stop.setAttribute('offset', str(int(offset))) + else: stop.setAttribute('offset', str(offset)) - for gradType in ['linearGradient', 'radialGradient']: - for grad in doc.getElementsByTagName(gradType): - stops = {} - stopsToRemove = [] - for stop in grad.getElementsByTagName('stop'): - # convert percentages into a floating point number - offsetU = SVGLength(stop.getAttribute('offset')) - if offsetU.units == Unit.PCT: - offset = offsetU.value / 100.0 - elif offsetU.units == Unit.NONE: - offset = offsetU.value - else: - offset = 0 - # set the stop offset value to the integer or floating point equivalent - if int(offset) == offset: - stop.setAttribute('offset', str(int(offset))) - else: - stop.setAttribute('offset', str(offset)) + color = stop.getAttribute('stop-color') + opacity = stop.getAttribute('stop-opacity') + style = stop.getAttribute('style') + if stops.has_key(offset) : + oldStop = stops[offset] + if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: + stopsToRemove.append(stop) + stops[offset] = [color, opacity, style] - color = stop.getAttribute('stop-color') - opacity = stop.getAttribute('stop-opacity') - style = stop.getAttribute('style') - if offset in stops: - oldStop = stops[offset] - if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: - stopsToRemove.append(stop) - stops[offset] = [color, opacity, style] + for stop in stopsToRemove: + stop.parentNode.removeChild(stop) + num += 1 + numElemsRemoved += 1 - for stop in stopsToRemove: - stop.parentNode.removeChild(stop) - num += len(stopsToRemove) - stats.num_elements_removed += len(stopsToRemove) + # linear gradients + return num - return num +def collapseSinglyReferencedGradients(doc): + global numElemsRemoved + num = 0 + identifiedElements = findElementsWithId(doc.documentElement) -def collapse_singly_referenced_gradients(doc, stats): - num = 0 + # make sure to reset the ref'ed ids for when we are running this in testscour + for rid,nodeCount in findReferencedElements(doc.documentElement).iteritems(): + count = nodeCount[0] + nodes = nodeCount[1] + # Make sure that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + if count == 1 and rid in identifiedElements: + elem = identifiedElements[rid] + if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ + and elem.namespaceURI == NS['SVG']: + # found a gradient that is referenced by only 1 other element + refElem = nodes[0] + if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \ + and refElem.namespaceURI == NS['SVG']: + # elem is a gradient referenced by only one other gradient (refElem) - identifiedElements = findElementsWithId(doc.documentElement) + # add the stops to the referencing gradient (this removes them from elem) + if len(refElem.getElementsByTagName('stop')) == 0: + stopsToAdd = elem.getElementsByTagName('stop') + for stop in stopsToAdd: + refElem.appendChild(stop) - # make sure to reset the ref'ed ids for when we are running this in testscour - for rid, nodes in six.iteritems(findReferencedElements(doc.documentElement)): - # Make sure that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - if len(nodes) == 1 and rid in identifiedElements: - elem = identifiedElements[rid] - if ( - elem is not None and - elem.nodeType == Node.ELEMENT_NODE and - elem.nodeName in ['linearGradient', 'radialGradient'] and - elem.namespaceURI == NS['SVG'] - ): - # found a gradient that is referenced by only 1 other element - refElem = nodes.pop() - if refElem.nodeType == Node.ELEMENT_NODE and refElem.nodeName in ['linearGradient', 'radialGradient'] \ - and refElem.namespaceURI == NS['SVG']: - # elem is a gradient referenced by only one other gradient (refElem) + # adopt the gradientUnits, spreadMethod, gradientTransform attributes if + # they are unspecified on refElem + for attr in ['gradientUnits','spreadMethod','gradientTransform']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - # add the stops to the referencing gradient (this removes them from elem) - if len(refElem.getElementsByTagName('stop')) == 0: - stopsToAdd = elem.getElementsByTagName('stop') - for stop in stopsToAdd: - refElem.appendChild(stop) + # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if + # they are unspecified on refElem + if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': + for attr in ['fx','fy','cx','cy','r']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - # adopt the gradientUnits, spreadMethod, gradientTransform attributes if - # they are unspecified on refElem - for attr in ['gradientUnits', 'spreadMethod', 'gradientTransform']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if + # they are unspecified on refElem + if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': + for attr in ['x1','y1','x2','y2']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if - # they are unspecified on refElem - if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': - for attr in ['fx', 'fy', 'cx', 'cy', 'r']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - - # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if - # they are unspecified on refElem - if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': - for attr in ['x1', 'y1', 'x2', 'y2']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - - target_href = elem.getAttributeNS(NS['XLINK'], 'href') - if target_href: - # If the elem node had an xlink:href, then the - # refElem have to point to it as well to - # preserve the semantics of the image. - refElem.setAttributeNS(NS['XLINK'], 'href', target_href) - else: - # The elem node had no xlink:href reference, - # so we can simply remove the attribute. - refElem.removeAttributeNS(NS['XLINK'], 'href') - - # now delete elem - elem.parentNode.removeChild(elem) - stats.num_elements_removed += 1 - num += 1 - - return num - - -def computeGradientBucketKey(grad): - # Compute a key (hashable opaque value; here a string) from each - # gradient such that "key(grad1) == key(grad2)" is the same as - # saying that grad1 is a duplicate of grad2. - gradBucketAttr = ['gradientUnits', 'spreadMethod', 'gradientTransform', - 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy', 'r'] - gradStopBucketsAttr = ['offset', 'stop-color', 'stop-opacity', 'style'] - - # A linearGradient can never be a duplicate of a - # radialGradient (and vice versa) - subKeys = [grad.getAttribute(a) for a in gradBucketAttr] - subKeys.append(grad.getAttributeNS(NS['XLINK'], 'href')) - stops = grad.getElementsByTagName('stop') - if stops.length: - for i in range(stops.length): - stop = stops.item(i) - for attr in gradStopBucketsAttr: - stopKey = stop.getAttribute(attr) - subKeys.append(stopKey) - - # Use a raw ASCII "record separator" control character as it is - # not likely to be used in any of these values (without having to - # be escaped). - return "\x1e".join(subKeys) - - -def detect_duplicate_gradients(*grad_lists): - """Detects duplicate gradients from each iterable/generator given as argument - - Yields (master, master_id, duplicates_id, duplicates) tuples where: - * master_id: The ID attribute of the master element. This will always be non-empty - and not None as long at least one of the gradients have a valid ID. - * duplicates_id: List of ID attributes of the duplicate gradients elements (can be - empty where the gradient had no ID attribute) - * duplicates: List of elements that are duplicates of the `master` element. Will - never include the `master` element. Has the same order as `duplicates_id` - i.e. - `duplicates[X].getAttribute("id") == duplicates_id[X]`. - """ - for grads in grad_lists: - grad_buckets = defaultdict(list) - - for grad in grads: - key = computeGradientBucketKey(grad) - grad_buckets[key].append(grad) - - for bucket in six.itervalues(grad_buckets): - if len(bucket) < 2: - # The gradient must be unique if it is the only one in - # this bucket. - continue - master = bucket[0] - duplicates = bucket[1:] - duplicates_ids = [d.getAttribute('id') for d in duplicates] - master_id = master.getAttribute('id') - if not master_id: - # If our selected "master" copy does not have an ID, - # then replace it with one that does (assuming any of - # them has one). This avoids broken images like we - # saw in GH#203 - for i in range(len(duplicates_ids)): - dup_id = duplicates_ids[i] - if dup_id: - # We do not bother updating the master field - # as it is not used any more. - master_id = duplicates_ids[i] - duplicates[i] = master - # Clear the old id to avoid a redundant remapping - duplicates_ids[i] = "" - break - - yield master_id, duplicates_ids, duplicates - - -def dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids): - func_iri = None - for dup_id, dup_grad in zip(duplicates_ids, duplicates): - # if the duplicate gradient no longer has a parent that means it was - # already re-mapped to another master gradient - if not dup_grad.parentNode: - continue - - # With --keep-unreferenced-defs, we can end up with - # unreferenced gradients. See GH#156. - if dup_id in referenced_ids: - if func_iri is None: - # matches url(#<ANY_DUP_ID>), url('#<ANY_DUP_ID>') and url("#<ANY_DUP_ID>") - dup_id_regex = "|".join(duplicates_ids) - func_iri = re.compile('url\\([\'"]?#(?:' + dup_id_regex + ')[\'"]?\\)') - for elem in referenced_ids[dup_id]: - # find out which attribute referenced the duplicate gradient - for attr in ['fill', 'stroke']: - v = elem.getAttribute(attr) - (v_new, n) = func_iri.subn('url(#' + master_id + ')', v) - if n > 0: - elem.setAttribute(attr, v_new) - if elem.getAttributeNS(NS['XLINK'], 'href') == '#' + dup_id: - elem.setAttributeNS(NS['XLINK'], 'href', '#' + master_id) - styles = _getStyle(elem) - for style in styles: - v = styles[style] - (v_new, n) = func_iri.subn('url(#' + master_id + ')', v) - if n > 0: - styles[style] = v_new - _setStyle(elem, styles) - - # now that all referencing elements have been re-mapped to the master - # it is safe to remove this gradient from the document - dup_grad.parentNode.removeChild(dup_grad) - - # If the gradients have an ID, we update referenced_ids to match the newly remapped IDs. - # This enable us to avoid calling findReferencedElements once per loop, which is helpful as it is - # one of the slowest functions in scour. - if master_id: - try: - master_references = referenced_ids[master_id] - except KeyError: - master_references = set() - - for dup_id in duplicates_ids: - references = referenced_ids.pop(dup_id, None) - if references is None: - continue - master_references.update(references) - - # Only necessary but needed if the master gradient did - # not have any references originally - referenced_ids[master_id] = master_references + # now remove the xlink:href from refElem + refElem.removeAttributeNS(NS['XLINK'], 'href') + # now delete elem + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num def removeDuplicateGradients(doc): - prev_num = -1 - num = 0 + global numElemsRemoved + num = 0 - # get a collection of all elements that are referenced and their referencing elements - referenced_ids = findReferencedElements(doc.documentElement) + gradientsToRemove = {} + duplicateToMaster = {} - while prev_num != num: - prev_num = num + for gradType in ['linearGradient', 'radialGradient']: + grads = doc.getElementsByTagName(gradType) + for grad in grads: + # TODO: should slice grads from 'grad' here to optimize + for ograd in grads: + # do not compare gradient to itself + if grad == ograd: continue - linear_gradients = doc.getElementsByTagName('linearGradient') - radial_gradients = doc.getElementsByTagName('radialGradient') + # compare grad to ograd (all properties, then all stops) + # if attributes do not match, go to next gradient + someGradAttrsDoNotMatch = False + for attr in ['gradientUnits','spreadMethod','gradientTransform','x1','y1','x2','y2','cx','cy','fx','fy','r']: + if grad.getAttribute(attr) != ograd.getAttribute(attr): + someGradAttrsDoNotMatch = True + break; - for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): - dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids) - num += len(duplicates) + if someGradAttrsDoNotMatch: continue - return num + # compare xlink:href values too + if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): + continue + # all gradient properties match, now time to compare stops + stops = grad.getElementsByTagName('stop') + ostops = ograd.getElementsByTagName('stop') + + if stops.length != ostops.length: continue + + # now compare stops + stopsNotEqual = False + for i in xrange(stops.length): + if stopsNotEqual: break + stop = stops.item(i) + ostop = ostops.item(i) + for attr in ['offset', 'stop-color', 'stop-opacity', 'style']: + if stop.getAttribute(attr) != ostop.getAttribute(attr): + stopsNotEqual = True + break + if stopsNotEqual: continue + + # ograd is a duplicate of grad, we schedule it to be removed UNLESS + # ograd is ALREADY considered a 'master' element + if not gradientsToRemove.has_key(ograd): + if not duplicateToMaster.has_key(ograd): + if not gradientsToRemove.has_key(grad): + gradientsToRemove[grad] = [] + gradientsToRemove[grad].append( ograd ) + duplicateToMaster[ograd] = grad + + # get a collection of all elements that are referenced and their referencing elements + referencedIDs = findReferencedElements(doc.documentElement) + for masterGrad in gradientsToRemove.keys(): + master_id = masterGrad.getAttribute('id') +# print 'master='+master_id + for dupGrad in gradientsToRemove[masterGrad]: + # if the duplicate gradient no longer has a parent that means it was + # already re-mapped to another master gradient + if not dupGrad.parentNode: continue + dup_id = dupGrad.getAttribute('id') +# print 'dup='+dup_id +# print referencedIDs[dup_id] + # for each element that referenced the gradient we are going to remove + for elem in referencedIDs[dup_id][1]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + elem.setAttribute(attr, 'url(#'+master_id+')') + if elem.getAttributeNS(NS['XLINK'], 'href') == '#'+dup_id: + elem.setAttributeNS(NS['XLINK'], 'href', '#'+master_id) + styles = _getStyle(elem) + for style in styles: + v = styles[style] + if v == 'url(#'+dup_id+')' or v == 'url("#'+dup_id+'")' or v == "url('#"+dup_id+"')": + styles[style] = 'url(#'+master_id+')' + _setStyle(elem, styles) + + # now that all referencing elements have been re-mapped to the master + # it is safe to remove this gradient from the document + dupGrad.parentNode.removeChild(dupGrad) + numElemsRemoved += 1 + num += 1 + return num def _getStyle(node): - u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType != Node.ELEMENT_NODE: - return {} - style_attribute = node.getAttribute('style') - if style_attribute: - styleMap = {} - rawStyles = style_attribute.split(';') - for style in rawStyles: - propval = style.split(':') - if len(propval) == 2: - styleMap[propval[0].strip()] = propval[1].strip() - return styleMap - else: - return {} - + u"""Returns the style attribute of a node as a dictionary.""" + if node.nodeType == 1 and len(node.getAttribute('style')) > 0 : + styleMap = { } + rawStyles = node.getAttribute('style').split(';') + for style in rawStyles: + propval = style.split(':') + if len(propval) == 2 : + styleMap[propval[0].strip()] = propval[1].strip() + return styleMap + else: + return {} def _setStyle(node, styleMap): - u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join(prop + ':' + styleMap[prop] for prop in styleMap) - if fixedStyle != '': - node.setAttribute('style', fixedStyle) - elif node.getAttribute('style'): - node.removeAttribute('style') - return node - + u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" + fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap.keys()]) + if fixedStyle != '' : + node.setAttribute('style', fixedStyle) + elif node.getAttribute('style'): + node.removeAttribute('style') + return node def repairStyle(node, options): - num = 0 - styleMap = _getStyle(node) - if styleMap: + num = 0 + styleMap = _getStyle(node) + if styleMap: - # I've seen this enough to know that I need to correct it: - # fill: url(#linearGradient4918) rgb(0, 0, 0); - for prop in ['fill', 'stroke']: - if prop in styleMap: - chunk = styleMap[prop].split(') ') - if (len(chunk) == 2 - and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") - and chunk[1] == 'rgb(0, 0, 0)'): - styleMap[prop] = chunk[0] + ')' - num += 1 + # I've seen this enough to know that I need to correct it: + # fill: url(#linearGradient4918) rgb(0, 0, 0); + for prop in ['fill', 'stroke'] : + if styleMap.has_key(prop) : + chunk = styleMap[prop].split(') ') + if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)' : + styleMap[prop] = chunk[0] + ')' + num += 1 - # Here is where we can weed out unnecessary styles like: - # opacity:1 - if 'opacity' in styleMap: - opacity = float(styleMap['opacity']) - # if opacity='0' then all fill and stroke properties are useless, remove them - if opacity == 0.0: - for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', - 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', - 'stroke-dashoffset', 'stroke-opacity']: - if uselessStyle in styleMap and not styleInheritedByChild(node, uselessStyle): - del styleMap[uselessStyle] - num += 1 + # Here is where we can weed out unnecessary styles like: + # opacity:1 + if styleMap.has_key('opacity') : + opacity = float(styleMap['opacity']) + # if opacity='0' then all fill and stroke properties are useless, remove them + if opacity == 0.0 : + for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', + 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', + 'stroke-dashoffset', 'stroke-opacity'] : + if styleMap.has_key(uselessStyle): + del styleMap[uselessStyle] + num += 1 - # if stroke:none, then remove all stroke-related properties (stroke-width, etc) - # TODO: should also detect if the computed value of this element is stroke="none" - if 'stroke' in styleMap and styleMap['stroke'] == 'none': - for strokestyle in ['stroke-width', 'stroke-linejoin', 'stroke-miterlimit', - 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: - if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): - del styleMap[strokestyle] - num += 1 - # we need to properly calculate computed values - if not styleInheritedByChild(node, 'stroke'): - if styleInheritedFromParent(node, 'stroke') in [None, 'none']: - del styleMap['stroke'] - num += 1 + # if stroke:none, then remove all stroke-related properties (stroke-width, etc) + # TODO: should also detect if the computed value of this element is stroke="none" + if styleMap.has_key('stroke') and styleMap['stroke'] == 'none' : + for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] : + if styleMap.has_key(strokestyle) : + del styleMap[strokestyle] + num += 1 + # TODO: This is actually a problem if a parent element has a specified stroke + # we need to properly calculate computed values + del styleMap['stroke'] - # if fill:none, then remove all fill-related properties (fill-rule, etc) - if 'fill' in styleMap and styleMap['fill'] == 'none': - for fillstyle in ['fill-rule', 'fill-opacity']: - if fillstyle in styleMap and not styleInheritedByChild(node, fillstyle): - del styleMap[fillstyle] - num += 1 + # if fill:none, then remove all fill-related properties (fill-rule, etc) + if styleMap.has_key('fill') and styleMap['fill'] == 'none' : + for fillstyle in [ 'fill-rule', 'fill-opacity' ] : + if styleMap.has_key(fillstyle) : + del styleMap[fillstyle] + num += 1 - # fill-opacity: 0 - if 'fill-opacity' in styleMap: - fillOpacity = float(styleMap['fill-opacity']) - if fillOpacity == 0.0: - for uselessFillStyle in ['fill', 'fill-rule']: - if uselessFillStyle in styleMap and not styleInheritedByChild(node, uselessFillStyle): - del styleMap[uselessFillStyle] - num += 1 + # fill-opacity: 0 + if styleMap.has_key('fill-opacity') : + fillOpacity = float(styleMap['fill-opacity']) + if fillOpacity == 0.0 : + for uselessFillStyle in [ 'fill', 'fill-rule' ] : + if styleMap.has_key(uselessFillStyle): + del styleMap[uselessFillStyle] + num += 1 - # stroke-opacity: 0 - if 'stroke-opacity' in styleMap: - strokeOpacity = float(styleMap['stroke-opacity']) - if strokeOpacity == 0.0: - for uselessStrokeStyle in ['stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset']: - if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-opacity: 0 + if styleMap.has_key('stroke-opacity') : + strokeOpacity = float(styleMap['stroke-opacity']) + if strokeOpacity == 0.0 : + for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset' ] : + if styleMap.has_key(uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 - # stroke-width: 0 - if 'stroke-width' in styleMap: - strokeWidth = SVGLength(styleMap['stroke-width']) - if strokeWidth.value == 0.0: - for uselessStrokeStyle in ['stroke', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: - if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-width: 0 + if styleMap.has_key('stroke-width') : + strokeWidth = SVGLength(styleMap['stroke-width']) + if strokeWidth.value == 0.0 : + for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ] : + if styleMap.has_key(uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 - # remove font properties for non-text elements - # I've actually observed this in real SVG content - if not mayContainTextNodes(node): - for fontstyle in ['font-family', 'font-size', 'font-stretch', 'font-size-adjust', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', 'line-height', 'kerning', - 'text-align', 'text-anchor', 'text-decoration', - 'text-rendering', 'unicode-bidi', - 'word-spacing', 'writing-mode']: - if fontstyle in styleMap: - del styleMap[fontstyle] - num += 1 + # remove font properties for non-text elements + # I've actually observed this in real SVG content + if not mayContainTextNodes(node): + for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', 'line-height', 'kerning', + 'text-align', 'text-anchor', 'text-decoration', + 'text-rendering', 'unicode-bidi', + 'word-spacing', 'writing-mode'] : + if styleMap.has_key(fontstyle) : + del styleMap[fontstyle] + num += 1 - # remove inkscape-specific styles - # TODO: need to get a full list of these - for inkscapeStyle in ['-inkscape-font-specification']: - if inkscapeStyle in styleMap: - del styleMap[inkscapeStyle] - num += 1 + # remove inkscape-specific styles + # TODO: need to get a full list of these + for inkscapeStyle in ['-inkscape-font-specification']: + if styleMap.has_key(inkscapeStyle): + del styleMap[inkscapeStyle] + num += 1 - if 'overflow' in styleMap: - # remove overflow from elements to which it does not apply, - # see https://www.w3.org/TR/SVG/masking.html#OverflowProperty - if node.nodeName not in ['svg', 'symbol', 'image', 'foreignObject', 'marker', 'pattern']: - del styleMap['overflow'] - num += 1 - # if the node is not the root <svg> element the SVG's user agent style sheet - # overrides the initial (i.e. default) value with the value 'hidden', which can consequently be removed - # (see last bullet point in the link above) - elif node != node.ownerDocument.documentElement: - if styleMap['overflow'] == 'hidden': - del styleMap['overflow'] - num += 1 - # on the root <svg> element the CSS2 default overflow="visible" is the initial value and we can remove it - elif styleMap['overflow'] == 'visible': - del styleMap['overflow'] - num += 1 + if styleMap.has_key('overflow') : + # overflow specified on element other than svg, marker, pattern + if not node.nodeName in ['svg','marker','pattern']: + del styleMap['overflow'] + num += 1 + # it is a marker, pattern or svg + # as long as this node is not the document <svg>, then only + # remove overflow='hidden'. See + # http://www.w3.org/TR/2010/WD-SVG11-20100622/masking.html#OverflowProperty + elif node != node.ownerDocument.documentElement: + if styleMap['overflow'] == 'hidden': + del styleMap['overflow'] + num += 1 + # else if outer svg has a overflow="visible", we can remove it + elif styleMap['overflow'] == 'visible': + del styleMap['overflow'] + num += 1 - # now if any of the properties match known SVG attributes we prefer attributes - # over style so emit them and remove them from the style map - if options.style_to_xml: - for propName in list(styleMap): - if propName in svgAttributes: - node.setAttribute(propName, styleMap[propName]) - del styleMap[propName] + # now if any of the properties match known SVG attributes we prefer attributes + # over style so emit them and remove them from the style map + if options.style_to_xml: + for propName in styleMap.keys() : + if propName in svgAttributes : + node.setAttribute(propName, styleMap[propName]) + del styleMap[propName] - _setStyle(node, styleMap) + _setStyle(node, styleMap) - # recurse for our child elements - for child in node.childNodes: - num += repairStyle(child, options) - - return num - - -def styleInheritedFromParent(node, style): - """ - Returns the value of 'style' that is inherited from the parents of the passed-in node - - Warning: This method only considers presentation attributes and inline styles, - any style sheets are ignored! - """ - parentNode = node.parentNode - - # return None if we reached the Document element - if parentNode.nodeType == Node.DOCUMENT_NODE: - return None - - # check styles first (they take precedence over presentation attributes) - styles = _getStyle(parentNode) - if style in styles: - value = styles[style] - if not value == 'inherit': - return value - - # check attributes - value = parentNode.getAttribute(style) - if value not in ['', 'inherit']: - return parentNode.getAttribute(style) - - # check the next parent recursively if we did not find a value yet - return styleInheritedFromParent(parentNode, style) - - -def styleInheritedByChild(node, style, nodeIsChild=False): - """ - Returns whether 'style' is inherited by any children of the passed-in node - - If False is returned, it is guaranteed that 'style' can safely be removed - from the passed-in node without influencing visual output of it's children - - If True is returned, the passed-in node should not have its text-based - attributes removed. - - Warning: This method only considers presentation attributes and inline styles, - any style sheets are ignored! - """ - # Comment, text and CDATA nodes don't have attributes and aren't containers so they can't inherit attributes - if node.nodeType != Node.ELEMENT_NODE: - return False - - if nodeIsChild: - # if the current child node sets a new value for 'style' - # we can stop the search in the current branch of the DOM tree - - # check attributes - if node.getAttribute(style) not in ['', 'inherit']: - return False - # check styles - styles = _getStyle(node) - if (style in styles) and not (styles[style] == 'inherit'): - return False - else: - # if the passed-in node does not have any children 'style' can obviously not be inherited - if not node.childNodes: - return False - - # If we have child nodes recursively check those - if node.childNodes: - for child in node.childNodes: - if styleInheritedByChild(child, style, True): - return True - - # If the current element is a container element the inherited style is meaningless - # (since we made sure it's not inherited by any of its children) - if node.nodeName in ['a', 'defs', 'glyph', 'g', 'marker', 'mask', - 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol']: - return False - - # in all other cases we have to assume the inherited value of 'style' is meaningful and has to be kept - # (e.g nodes without children at the end of the DOM tree, text nodes, ...) - return True + # recurse for our child elements + for child in node.childNodes : + num += repairStyle(child,options) + return num def mayContainTextNodes(node): - """ - Returns True if the passed-in node is probably a text element, or at least - one of its descendants is probably a text element. + """ + Returns True if the passed-in node is probably a text element, or at least + one of its descendants is probably a text element. - If False is returned, it is guaranteed that the passed-in node has no - business having text-based attributes. + If False is returned, it is guaranteed that the passed-in node has no + business having text-based attributes. - If True is returned, the passed-in node should not have its text-based - attributes removed. - """ - # Cached result of a prior call? - try: - return node.mayContainTextNodes - except AttributeError: - pass + If True is returned, the passed-in node should not have its text-based + attributes removed. + """ + # Cached result of a prior call? + try: + return node.mayContainTextNodes + except AttributeError: + pass - result = True # Default value - # Comment, text and CDATA nodes don't have attributes and aren't containers - if node.nodeType != Node.ELEMENT_NODE: - result = False - # Non-SVG elements? Unknown elements! - elif node.namespaceURI != NS['SVG']: - result = True - # Blacklisted elements. Those are guaranteed not to be text elements. - elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', - 'polyline', 'path', 'image', 'stop']: - result = False - # Group elements. If we're missing any here, the default of True is used. - elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', - 'linearGradient', 'radialGradient', 'symbol']: - result = False - for child in node.childNodes: - if mayContainTextNodes(child): - result = True - # Everything else should be considered a future SVG-version text element - # at best, or an unknown element at worst. result will stay True. - - # Cache this result before returning it. - node.mayContainTextNodes = result - return result - - -# A list of default attributes that are safe to remove if all conditions are fulfilled -# -# Each default attribute is an object of type 'DefaultAttribute' with the following fields: -# name - name of the attribute to be matched -# value - default value of the attribute -# units - the unit(s) for which 'value' is valid (see 'Unit' class for possible specifications) -# elements - name(s) of SVG element(s) for which the attribute specification is valid -# conditions - additional conditions that have to be fulfilled for removal of the specified default attribute -# implemented as lambda functions with one argument (an xml.dom.minidom node) -# evaluating to either True or False -# When not specifying a field value, it will be ignored (i.e. always matches) -# -# Sources for this list: -# https://www.w3.org/TR/SVG/attindex.html (mostly implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (not yet implemented) -# https://www.w3.org/TR/SVG2/attindex.html (not yet implemented) -# -DefaultAttribute = namedtuple('DefaultAttribute', ['name', 'value', 'units', 'elements', 'conditions']) -DefaultAttribute.__new__.__defaults__ = (None,) * len(DefaultAttribute._fields) -default_attributes = [ - # unit systems - DefaultAttribute('clipPathUnits', 'userSpaceOnUse', elements=['clipPath']), - DefaultAttribute('filterUnits', 'objectBoundingBox', elements=['filter']), - DefaultAttribute('gradientUnits', 'objectBoundingBox', elements=['linearGradient', 'radialGradient']), - DefaultAttribute('maskUnits', 'objectBoundingBox', elements=['mask']), - DefaultAttribute('maskContentUnits', 'userSpaceOnUse', elements=['mask']), - DefaultAttribute('patternUnits', 'objectBoundingBox', elements=['pattern']), - DefaultAttribute('patternContentUnits', 'userSpaceOnUse', elements=['pattern']), - DefaultAttribute('primitiveUnits', 'userSpaceOnUse', elements=['filter']), - - DefaultAttribute('externalResourcesRequired', 'false', - elements=['a', 'altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'cursor', 'defs', 'ellipse', - 'feImage', 'filter', 'font', 'foreignObject', 'g', 'image', 'line', 'linearGradient', - 'marker', 'mask', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', - 'rect', 'script', 'set', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tref', 'tspan', - 'use', 'view']), - - # svg elements - DefaultAttribute('width', 100, Unit.PCT, elements=['svg']), - DefaultAttribute('height', 100, Unit.PCT, elements=['svg']), - DefaultAttribute('baseProfile', 'none', elements=['svg']), - DefaultAttribute('preserveAspectRatio', 'xMidYMid meet', - elements=['feImage', 'image', 'marker', 'pattern', 'svg', 'symbol', 'view']), - - # common attributes / basic types - DefaultAttribute('x', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', - 'image', 'pattern', 'rect', 'svg', 'text', 'use']), - DefaultAttribute('y', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', - 'image', 'pattern', 'rect', 'svg', 'text', 'use']), - DefaultAttribute('z', 0, elements=['fePointLight', 'feSpotLight']), - DefaultAttribute('x1', 0, elements=['line']), - DefaultAttribute('y1', 0, elements=['line']), - DefaultAttribute('x2', 0, elements=['line']), - DefaultAttribute('y2', 0, elements=['line']), - DefaultAttribute('cx', 0, elements=['circle', 'ellipse']), - DefaultAttribute('cy', 0, elements=['circle', 'ellipse']), - - # markers - DefaultAttribute('markerUnits', 'strokeWidth', elements=['marker']), - DefaultAttribute('refX', 0, elements=['marker']), - DefaultAttribute('refY', 0, elements=['marker']), - DefaultAttribute('markerHeight', 3, elements=['marker']), - DefaultAttribute('markerWidth', 3, elements=['marker']), - DefaultAttribute('orient', 0, elements=['marker']), - - # text / textPath / tspan / tref - DefaultAttribute('lengthAdjust', 'spacing', elements=['text', 'textPath', 'tref', 'tspan']), - DefaultAttribute('startOffset', 0, elements=['textPath']), - DefaultAttribute('method', 'align', elements=['textPath']), - DefaultAttribute('spacing', 'exact', elements=['textPath']), - - # filters and masks - DefaultAttribute('x', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('x', -0.1, Unit.NONE, ['filter', 'mask'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('y', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('y', -0.1, Unit.NONE, ['filter', 'mask'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('width', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('width', 1.2, Unit.NONE, ['filter', 'mask'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('height', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('height', 1.2, Unit.NONE, ['filter', 'mask'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - - # gradients - DefaultAttribute('x1', 0, elements=['linearGradient']), - DefaultAttribute('y1', 0, elements=['linearGradient']), - DefaultAttribute('y2', 0, elements=['linearGradient']), - DefaultAttribute('x2', 100, Unit.PCT, elements=['linearGradient']), - DefaultAttribute('x2', 1, Unit.NONE, elements=['linearGradient'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - # remove fx/fy before cx/cy to catch the case where fx = cx = 50% or fy = cy = 50% respectively - DefaultAttribute('fx', elements=['radialGradient'], - conditions=lambda node: node.getAttribute('fx') == node.getAttribute('cx')), - DefaultAttribute('fy', elements=['radialGradient'], - conditions=lambda node: node.getAttribute('fy') == node.getAttribute('cy')), - DefaultAttribute('r', 50, Unit.PCT, elements=['radialGradient']), - DefaultAttribute('r', 0.5, Unit.NONE, elements=['radialGradient'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cx', 50, Unit.PCT, elements=['radialGradient']), - DefaultAttribute('cx', 0.5, Unit.NONE, elements=['radialGradient'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cy', 50, Unit.PCT, elements=['radialGradient']), - DefaultAttribute('cy', 0.5, Unit.NONE, elements=['radialGradient'], - conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('spreadMethod', 'pad', elements=['linearGradient', 'radialGradient']), - - # filter effects - # TODO: Some numerical attributes allow an optional second value ("number-optional-number") - # and are currently handled as strings to avoid an exception in 'SVGLength', see - # https://github.com/scour-project/scour/pull/192 - DefaultAttribute('amplitude', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('azimuth', 0, elements=['feDistantLight']), - DefaultAttribute('baseFrequency', '0', elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('bias', 1, elements=['feConvolveMatrix']), - DefaultAttribute('diffuseConstant', 1, elements=['feDiffuseLighting']), - DefaultAttribute('edgeMode', 'duplicate', elements=['feConvolveMatrix']), - DefaultAttribute('elevation', 0, elements=['feDistantLight']), - DefaultAttribute('exponent', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('intercept', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('k1', 0, elements=['feComposite']), - DefaultAttribute('k2', 0, elements=['feComposite']), - DefaultAttribute('k3', 0, elements=['feComposite']), - DefaultAttribute('k4', 0, elements=['feComposite']), - DefaultAttribute('mode', 'normal', elements=['feBlend']), - DefaultAttribute('numOctaves', 1, elements=['feTurbulence']), - DefaultAttribute('offset', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('operator', 'over', elements=['feComposite']), - DefaultAttribute('operator', 'erode', elements=['feMorphology']), - DefaultAttribute('order', '3', elements=['feConvolveMatrix']), - DefaultAttribute('pointsAtX', 0, elements=['feSpotLight']), - DefaultAttribute('pointsAtY', 0, elements=['feSpotLight']), - DefaultAttribute('pointsAtZ', 0, elements=['feSpotLight']), - DefaultAttribute('preserveAlpha', 'false', elements=['feConvolveMatrix']), - DefaultAttribute('radius', '0', elements=['feMorphology']), - DefaultAttribute('scale', 0, elements=['feDisplacementMap']), - DefaultAttribute('seed', 0, elements=['feTurbulence']), - DefaultAttribute('specularConstant', 1, elements=['feSpecularLighting']), - DefaultAttribute('specularExponent', 1, elements=['feSpecularLighting', 'feSpotLight']), - DefaultAttribute('stdDeviation', '0', elements=['feGaussianBlur']), - DefaultAttribute('stitchTiles', 'noStitch', elements=['feTurbulence']), - DefaultAttribute('surfaceScale', 1, elements=['feDiffuseLighting', 'feSpecularLighting']), - DefaultAttribute('type', 'matrix', elements=['feColorMatrix']), - DefaultAttribute('type', 'turbulence', elements=['feTurbulence']), - DefaultAttribute('xChannelSelector', 'A', elements=['feDisplacementMap']), - DefaultAttribute('yChannelSelector', 'A', elements=['feDisplacementMap']) -] - -# split to increase lookup performance -# TODO: 'default_attributes_universal' is actually empty right now - will we ever need it? -default_attributes_universal = [] # list containing attributes valid for all elements -default_attributes_per_element = defaultdict(list) # dict containing lists of attributes valid for individual elements -for default_attribute in default_attributes: - if default_attribute.elements is None: - default_attributes_universal.append(default_attribute) - else: - for element in default_attribute.elements: - default_attributes_per_element[element].append(default_attribute) + result = True # Default value + # Comment, text and CDATA nodes don't have attributes and aren't containers + if node.nodeType != 1: + result = False + # Non-SVG elements? Unknown elements! + elif node.namespaceURI != NS['SVG']: + result = True + # Blacklisted elements. Those are guaranteed not to be text elements. + elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', + 'polyline', 'path', 'image', 'stop']: + result = False + # Group elements. If we're missing any here, the default of True is used. + elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', + 'linearGradient', 'radialGradient', 'symbol']: + result = False + for child in node.childNodes: + if mayContainTextNodes(child): + result = True + # Everything else should be considered a future SVG-version text element + # at best, or an unknown element at worst. result will stay True. + # Cache this result before returning it. + node.mayContainTextNodes = result + return result def taint(taintedSet, taintedAttribute): - u"""Adds an attribute to a set of attributes. + u"""Adds an attribute to a set of attributes. - Related attributes are also included.""" - taintedSet.add(taintedAttribute) - if taintedAttribute == 'marker': - taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) - if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: - taintedSet.add('marker') - return taintedSet + Related attributes are also included.""" + taintedSet.add(taintedAttribute) + if taintedAttribute == 'marker': + taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) + if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: + taintedSet.add('marker') + return taintedSet +def removeDefaultAttributeValues(node, options, tainted=set()): + u"""'tainted' keeps a set of attributes defined in parent nodes. -def removeDefaultAttributeValue(node, attribute): - """ - Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled + For such attributes, we don't delete attributes with default values.""" + num = 0 + if node.nodeType != 1: return 0 - Warning: Does NOT check if the attribute is actually valid for the passed element type for increased performance! - """ - if not node.hasAttribute(attribute.name): - return 0 + # gradientUnits: objectBoundingBox + if node.getAttribute('gradientUnits') == 'objectBoundingBox': + node.removeAttribute('gradientUnits') + num += 1 - # differentiate between text and numeric values - if isinstance(attribute.value, str): - if node.getAttribute(attribute.name) == attribute.value: - if (attribute.conditions is None) or attribute.conditions(node): - node.removeAttribute(attribute.name) - return 1 - else: - nodeValue = SVGLength(node.getAttribute(attribute.name)) - if ((attribute.value is None) - or ((nodeValue.value == attribute.value) and not (nodeValue.units == Unit.INVALID))): - if ((attribute.units is None) - or (nodeValue.units == attribute.units) - or (isinstance(attribute.units, list) and nodeValue.units in attribute.units)): - if (attribute.conditions is None) or attribute.conditions(node): - node.removeAttribute(attribute.name) - return 1 + # spreadMethod: pad + if node.getAttribute('spreadMethod') == 'pad': + node.removeAttribute('spreadMethod') + num += 1 - return 0 + # x1: 0% + if node.getAttribute('x1') != '': + x1 = SVGLength(node.getAttribute('x1')) + if x1.value == 0: + node.removeAttribute('x1') + num += 1 + # y1: 0% + if node.getAttribute('y1') != '': + y1 = SVGLength(node.getAttribute('y1')) + if y1.value == 0: + node.removeAttribute('y1') + num += 1 -def removeDefaultAttributeValues(node, options, tainted=None): - u"""'tainted' keeps a set of attributes defined in parent nodes. + # x2: 100% + if node.getAttribute('x2') != '': + x2 = SVGLength(node.getAttribute('x2')) + if (x2.value == 100 and x2.units == Unit.PCT) or (x2.value == 1 and x2.units == Unit.NONE): + node.removeAttribute('x2') + num += 1 - For such attributes, we don't delete attributes with default values.""" - num = 0 - if node.nodeType != Node.ELEMENT_NODE: - return 0 + # y2: 0% + if node.getAttribute('y2') != '': + y2 = SVGLength(node.getAttribute('y2')) + if y2.value == 0: + node.removeAttribute('y2') + num += 1 - if tainted is None: - tainted = set() + # fx: equal to rx + if node.getAttribute('fx') != '': + if node.getAttribute('fx') == node.getAttribute('cx'): + node.removeAttribute('fx') + num += 1 - # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) - # - # For increased performance do not iterate the whole list for each element but run only on valid subsets - # - 'default_attributes_universal' (attributes valid for all elements) - # - 'default_attributes_per_element' (attributes specific to one specific element type) - for attribute in default_attributes_universal: - num += removeDefaultAttributeValue(node, attribute) - if node.nodeName in default_attributes_per_element: - for attribute in default_attributes_per_element[node.nodeName]: - num += removeDefaultAttributeValue(node, attribute) + # fy: equal to ry + if node.getAttribute('fy') != '': + if node.getAttribute('fy') == node.getAttribute('cy'): + node.removeAttribute('fy') + num += 1 - # Summarily get rid of default properties - attributes = [node.attributes.item(i).nodeName for i in range(node.attributes.length)] - for attribute in attributes: - if attribute not in tainted: - if attribute in default_properties: - if node.getAttribute(attribute) == default_properties[attribute]: - node.removeAttribute(attribute) - num += 1 - else: - tainted = taint(tainted, attribute) - # Properties might also occur as styles, remove them too - styles = _getStyle(node) - for attribute in list(styles): - if attribute not in tainted: - if attribute in default_properties: - if styles[attribute] == default_properties[attribute]: - del styles[attribute] - num += 1 - else: - tainted = taint(tainted, attribute) - _setStyle(node, styles) + # cx: 50% + if node.getAttribute('cx') != '': + cx = SVGLength(node.getAttribute('cx')) + if (cx.value == 50 and cx.units == Unit.PCT) or (cx.value == 0.5 and cx.units == Unit.NONE): + node.removeAttribute('cx') + num += 1 - # recurse for our child elements - for child in node.childNodes: - num += removeDefaultAttributeValues(child, options, tainted.copy()) + # cy: 50% + if node.getAttribute('cy') != '': + cy = SVGLength(node.getAttribute('cy')) + if (cy.value == 50 and cy.units == Unit.PCT) or (cy.value == 0.5 and cy.units == Unit.NONE): + node.removeAttribute('cy') + num += 1 - return num + # r: 50% + if node.getAttribute('r') != '': + r = SVGLength(node.getAttribute('r')) + if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE): + node.removeAttribute('r') + num += 1 + # Summarily get rid of some more attributes + attributes = [node.attributes.item(i).nodeName + for i in range(node.attributes.length)] + for attribute in attributes: + if attribute not in tainted: + if attribute in default_attributes.keys(): + if node.getAttribute(attribute) == default_attributes[attribute]: + node.removeAttribute(attribute) + num += 1 + else: + tainted = taint(tainted, attribute) + # These attributes might also occur as styles + styles = _getStyle(node) + for attribute in styles.keys(): + if attribute not in tainted: + if attribute in default_attributes.keys(): + if styles[attribute] == default_attributes[attribute]: + del styles[attribute] + num += 1 + else: + tainted = taint(tainted, attribute) + _setStyle(node, styles) + + # recurse for our child elements + for child in node.childNodes : + num += removeDefaultAttributeValues(child, options, tainted.copy()) + + return num rgb = re.compile(r"\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*") rgbp = re.compile(r"\s*rgb\(\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*\)\s*") - - def convertColor(value): - """ - Converts the input color string and returns a #RRGGBB (or #RGB if possible) string - """ - s = value + """ + Converts the input color string and returns a #RRGGBB (or #RGB if possible) string + """ + s = value - if s in colors: - s = colors[s] + if s in colors.keys(): + s = colors[s] - rgbpMatch = rgbp.match(s) - if rgbpMatch is not None: - r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) - g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) - b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) - s = '#%02x%02x%02x' % (r, g, b) - else: - rgbMatch = rgb.match(s) - if rgbMatch is not None: - r = int(rgbMatch.group(1)) - g = int(rgbMatch.group(2)) - b = int(rgbMatch.group(3)) - s = '#%02x%02x%02x' % (r, g, b) + rgbpMatch = rgbp.match(s) + if rgbpMatch != None : + r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) + g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) + b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) + s = '#%02x%02x%02x' % (r, g, b) + else: + rgbMatch = rgb.match(s) + if rgbMatch != None : + r = int( rgbMatch.group(1) ) + g = int( rgbMatch.group(2) ) + b = int( rgbMatch.group(3) ) + s = '#%02x%02x%02x' % (r, g, b) - if s[0] == '#': - s = s.lower() - if len(s) == 7 and s[1] == s[2] and s[3] == s[4] and s[5] == s[6]: - s = '#' + s[1] + s[3] + s[5] + if s[0] == '#': + s = s.lower() + if len(s)==7 and s[1]==s[2] and s[3]==s[4] and s[5]==s[6]: + s = '#'+s[1]+s[3]+s[5] - return s + return s +def convertColors(element) : + """ + Recursively converts all color properties into #RRGGBB format if shorter + """ + numBytes = 0 -def convertColors(element): - """ - Recursively converts all color properties into #RRGGBB format if shorter - """ - numBytes = 0 + if element.nodeType != 1: return 0 - if element.nodeType != Node.ELEMENT_NODE: - return 0 + # set up list of color attributes for each element type + attrsToConvert = [] + if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', \ + 'line', 'polyline', 'path', 'g', 'a']: + attrsToConvert = ['fill', 'stroke'] + elif element.nodeName in ['stop']: + attrsToConvert = ['stop-color'] + elif element.nodeName in ['solidColor']: + attrsToConvert = ['solid-color'] - # set up list of color attributes for each element type - attrsToConvert = [] - if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', - 'line', 'polyline', 'path', 'g', 'a']: - attrsToConvert = ['fill', 'stroke'] - elif element.nodeName in ['stop']: - attrsToConvert = ['stop-color'] - elif element.nodeName in ['solidColor']: - attrsToConvert = ['solid-color'] + # now convert all the color formats + styles = _getStyle(element) + for attr in attrsToConvert: + oldColorValue = element.getAttribute(attr) + if oldColorValue != '': + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + element.setAttribute(attr, newColorValue) + numBytes += (oldBytes - len(element.getAttribute(attr))) + # colors might also hide in styles + if attr in styles.keys(): + oldColorValue = styles[attr] + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + styles[attr] = newColorValue + numBytes += (oldBytes - len(element.getAttribute(attr))) + _setStyle(element, styles) - # now convert all the color formats - styles = _getStyle(element) - for attr in attrsToConvert: - oldColorValue = element.getAttribute(attr) - if oldColorValue != '': - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - element.setAttribute(attr, newColorValue) - numBytes += (oldBytes - len(element.getAttribute(attr))) - # colors might also hide in styles - if attr in styles: - oldColorValue = styles[attr] - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - styles[attr] = newColorValue - numBytes += (oldBytes - newBytes) - _setStyle(element, styles) + # now recurse for our child elements + for child in element.childNodes : + numBytes += convertColors(child) - # now recurse for our child elements - for child in element.childNodes: - numBytes += convertColors(child) - - return numBytes + return numBytes # TODO: go over what this method does and see if there is a way to optimize it # TODO: go over the performance of this method and see if I can save memory/speed by # reusing data structures, etc +def cleanPath(element, options) : + """ + Cleans the path string (d attribute) of the element + """ + global numBytesSavedInPathData + global numPathSegmentsReduced + global numCurvesStraightened + # this gets the parser object from svg_regex.py + oldPathStr = element.getAttribute('d') + path = svg_parser.parse(oldPathStr) -def clean_path(element, options, stats): - """ - Cleans the path string (d attribute) of the element - """ + # This determines whether the stroke has round linecaps. If it does, + # we do not want to collapse empty segments, as they are actually rendered. + withRoundLineCaps = element.getAttribute('stroke-linecap') == 'round' - # this gets the parser object from svg_regex.py - oldPathStr = element.getAttribute('d') - path = svg_parser.parse(oldPathStr) - style = _getStyle(element) + # The first command must be a moveto, and whether it's relative (m) + # or absolute (M), the first set of coordinates *is* absolute. So + # the first iteration of the loop below will get x,y and startx,starty. - # This determines whether the stroke has round or square linecaps. If it does, we do not want to collapse empty - # segments, as they are actually rendered (as circles or squares with diameter/dimension matching the path-width). - has_round_or_square_linecaps = ( - element.getAttribute('stroke-linecap') in ['round', 'square'] - or 'stroke-linecap' in style and style['stroke-linecap'] in ['round', 'square'] - ) + # convert absolute coordinates into relative ones. + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists since we're not adding or removing any. + for pathIndex in xrange(0, len(path)): + cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure + i = 0 + # adjust abs to rel + # only the A command has some values that we don't want to adjust (radii, rotation, flags) + if cmd == 'A': + for i in xrange(i, len(data), 7): + data[i+5] -= x + data[i+6] -= y + x += data[i+5] + y += data[i+6] + path[pathIndex] = ('a', data) + elif cmd == 'a': + x += sum(data[5::7]) + y += sum(data[6::7]) + elif cmd == 'H': + for i in xrange(i, len(data)): + data[i] -= x + x += data[i] + path[pathIndex] = ('h', data) + elif cmd == 'h': + x += sum(data) + elif cmd == 'V': + for i in xrange(i, len(data)): + data[i] -= y + y += data[i] + path[pathIndex] = ('v', data) + elif cmd == 'v': + y += sum(data) + elif cmd == 'M': + startx, starty = data[0], data[1] + # If this is a path starter, don't convert its first + # coordinate to relative; that would just make it (0, 0) + if pathIndex != 0: + data[0] -= x + data[1] -= y - # This determines whether the stroke has intermediate markers. If it does, we do not want to collapse - # straight segments running in the same direction, as markers are rendered on the intermediate nodes. - has_intermediate_markers = ( - element.hasAttribute('marker') - or element.hasAttribute('marker-mid') - or 'marker' in style - or 'marker-mid' in style - ) - - # The first command must be a moveto, and whether it's relative (m) - # or absolute (M), the first set of coordinates *is* absolute. So - # the first iteration of the loop below will get x,y and startx,starty. - - # convert absolute coordinates into relative ones. - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists since we're not adding or removing any. - x = y = 0 - for pathIndex in range(len(path)): - cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure - i = 0 - # adjust abs to rel - # only the A command has some values that we don't want to adjust (radii, rotation, flags) - if cmd == 'A': - for i in range(i, len(data), 7): - data[i + 5] -= x - data[i + 6] -= y - x += data[i + 5] - y += data[i + 6] - path[pathIndex] = ('a', data) - elif cmd == 'a': - x += sum(data[5::7]) - y += sum(data[6::7]) - elif cmd == 'H': - for i in range(i, len(data)): - data[i] -= x - x += data[i] - path[pathIndex] = ('h', data) - elif cmd == 'h': - x += sum(data) - elif cmd == 'V': - for i in range(i, len(data)): - data[i] -= y - y += data[i] - path[pathIndex] = ('v', data) - elif cmd == 'v': - y += sum(data) - elif cmd == 'M': + x, y = startx, starty + i = 2 + for i in xrange(i, len(data), 2): + data[i] -= x + data[i+1] -= y + x += data[i] + y += data[i+1] + path[pathIndex] = ('m', data) + elif cmd in ['L','T']: + for i in xrange(i, len(data), 2): + data[i] -= x + data[i+1] -= y + x += data[i] + y += data[i+1] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['m']: + if pathIndex == 0: + # START OF PATH - this is an absolute moveto + # followed by relative linetos startx, starty = data[0], data[1] - # If this is a path starter, don't convert its first - # coordinate to relative; that would just make it (0, 0) - if pathIndex != 0: - data[0] -= x - data[1] -= y - x, y = startx, starty i = 2 - for i in range(i, len(data), 2): - data[i] -= x - data[i + 1] -= y - x += data[i] - y += data[i + 1] - path[pathIndex] = ('m', data) - elif cmd in ['L', 'T']: - for i in range(i, len(data), 2): - data[i] -= x - data[i + 1] -= y - x += data[i] - y += data[i + 1] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['m']: - if pathIndex == 0: - # START OF PATH - this is an absolute moveto - # followed by relative linetos - startx, starty = data[0], data[1] - x, y = startx, starty - i = 2 + else: + startx = x + data[0] + starty = y + data[1] + for i in xrange(i, len(data), 2): + x += data[i] + y += data[i+1] + elif cmd in ['l','t']: + x += sum(data[0::2]) + y += sum(data[1::2]) + elif cmd in ['S','Q']: + for i in xrange(i, len(data), 4): + data[i] -= x + data[i+1] -= y + data[i+2] -= x + data[i+3] -= y + x += data[i+2] + y += data[i+3] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['s','q']: + x += sum(data[2::4]) + y += sum(data[3::4]) + elif cmd == 'C': + for i in xrange(i, len(data), 6): + data[i] -= x + data[i+1] -= y + data[i+2] -= x + data[i+3] -= y + data[i+4] -= x + data[i+5] -= y + x += data[i+4] + y += data[i+5] + path[pathIndex] = ('c', data) + elif cmd == 'c': + x += sum(data[4::6]) + y += sum(data[5::6]) + elif cmd in ['z','Z']: + x, y = startx, starty + path[pathIndex] = ('z', data) + + # remove empty segments + # Reuse the data structure 'path' and the coordinate lists, even if we're + # deleting items, because these deletions are relatively cheap. + if not withRoundLineCaps: + for pathIndex in xrange(0, len(path)): + cmd, data = path[pathIndex] + i = 0 + if cmd in ['m','l','t']: + if cmd == 'm': + # remove m0,0 segments + if pathIndex > 0 and data[0] == data[i+1] == 0: + # 'm0,0 x,y' can be replaces with 'lx,y', + # except the first m which is a required absolute moveto + path[pathIndex] = ('l', data[2:]) + numPathSegmentsReduced += 1 + else: # else skip move coordinate + i = 2 + while i < len(data): + if data[i] == data[i+1] == 0: + del data[i:i+2] + numPathSegmentsReduced += 1 + else: + i += 2 + elif cmd == 'c': + while i < len(data): + if data[i] == data[i+1] == data[i+2] == data[i+3] == data[i+4] == data[i+5] == 0: + del data[i:i+6] + numPathSegmentsReduced += 1 + else: + i += 6 + elif cmd == 'a': + while i < len(data): + if data[i+5] == data[i+6] == 0: + del data[i:i+7] + numPathSegmentsReduced += 1 + else: + i += 7 + elif cmd == 'q': + while i < len(data): + if data[i] == data[i+1] == data[i+2] == data[i+3] == 0: + del data[i:i+4] + numPathSegmentsReduced += 1 + else: + i += 4 + elif cmd in ['h','v']: + oldLen = len(data) + path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) + numPathSegmentsReduced += len(path[pathIndex][1]) - oldLen + + # fixup: Delete subcommands having no coordinates. + path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] + + # convert straight curves into lines + newPath = [path[0]] + for (cmd,data) in path[1:]: + i = 0 + newData = data + if cmd == 'c': + newData = [] + while i < len(data): + # since all commands are now relative, we can think of previous point as (0,0) + # and new point (dx,dy) is (data[i+4],data[i+5]) + # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 + (p1x,p1y) = (data[i],data[i+1]) + (p2x,p2y) = (data[i+2],data[i+3]) + dx = data[i+4] + dy = data[i+5] + + foundStraightCurve = False + + if dx == 0: + if p1x == 0 and p2x == 0: + foundStraightCurve = True else: - startx = x + data[0] - starty = y + data[1] - for i in range(i, len(data), 2): - x += data[i] - y += data[i + 1] - elif cmd in ['l', 't']: - x += sum(data[0::2]) - y += sum(data[1::2]) - elif cmd in ['S', 'Q']: - for i in range(i, len(data), 4): - data[i] -= x - data[i + 1] -= y - data[i + 2] -= x - data[i + 3] -= y - x += data[i + 2] - y += data[i + 3] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['s', 'q']: - x += sum(data[2::4]) - y += sum(data[3::4]) - elif cmd == 'C': - for i in range(i, len(data), 6): - data[i] -= x - data[i + 1] -= y - data[i + 2] -= x - data[i + 3] -= y - data[i + 4] -= x - data[i + 5] -= y - x += data[i + 4] - y += data[i + 5] - path[pathIndex] = ('c', data) - elif cmd == 'c': - x += sum(data[4::6]) - y += sum(data[5::6]) - elif cmd in ['z', 'Z']: - x, y = startx, starty - path[pathIndex] = ('z', data) + m = dy/dx + if p1y == m*p1x and p2y == m*p2x: + foundStraightCurve = True - # remove empty segments and redundant commands - # Reuse the data structure 'path' and the coordinate lists, even if we're - # deleting items, because these deletions are relatively cheap. - if not has_round_or_square_linecaps: - # remove empty path segments - for pathIndex in range(len(path)): - cmd, data = path[pathIndex] - i = 0 - if cmd in ['m', 'l', 't']: - if cmd == 'm': - # It might be tempting to rewrite "m0 0 ..." into - # "l..." here. However, this is an unsound - # optimization in general as "m0 0 ... z" is - # different from "l...z". - # - # To do such a rewrite, we need to understand the - # full subpath. This logic happens after this - # loop. - i = 2 - while i < len(data): - if data[i] == data[i + 1] == 0: - del data[i:i + 2] - stats.num_path_segments_removed += 1 - else: - i += 2 - elif cmd == 'c': - while i < len(data): - if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == data[i + 4] == data[i + 5] == 0: - del data[i:i + 6] - stats.num_path_segments_removed += 1 - else: - i += 6 - elif cmd == 'a': - while i < len(data): - if data[i + 5] == data[i + 6] == 0: - del data[i:i + 7] - stats.num_path_segments_removed += 1 - else: - i += 7 - elif cmd == 'q': - while i < len(data): - if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == 0: - del data[i:i + 4] - stats.num_path_segments_removed += 1 - else: - i += 4 - elif cmd in ['h', 'v']: - oldLen = len(data) - path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) - stats.num_path_segments_removed += len(path[pathIndex][1]) - oldLen - - # remove no-op commands - pathIndex = len(path) - subpath_needs_anchor = False - # NB: We can never rewrite the first m/M command (expect if it - # is the only command) - while pathIndex > 1: - pathIndex -= 1 - cmd, data = path[pathIndex] - if cmd == 'z': - next_cmd, next_data = path[pathIndex - 1] - if next_cmd == 'm' and len(next_data) == 2: - # mX Yz -> mX Y - - # note the len check on next_data as it is not - # safe to rewrite "m0 0 1 1z" in general (it is a - # question of where the "pen" ends - you can - # continue a draw on the same subpath after a - # "z"). - del path[pathIndex] - stats.num_path_segments_removed += 1 - else: - # it is not safe to rewrite "m0 0 ..." to "l..." - # because of this "z" command. - subpath_needs_anchor = True - elif cmd == 'm': - if len(path) - 1 == pathIndex and len(data) == 2: - # Ends with an empty move (but no line/draw - # following it) - del path[pathIndex] - stats.num_path_segments_removed += 1 - continue - if subpath_needs_anchor: - subpath_needs_anchor = False - elif data[0] == data[1] == 0: - # unanchored, i.e. we can replace "m0 0 ..." with - # "l..." as there is no "z" after it. - path[pathIndex] = ('l', data[2:]) - stats.num_path_segments_removed += 1 - - # fixup: Delete subcommands having no coordinates. - path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] - - # convert straight curves into lines - newPath = [path[0]] - for (cmd, data) in path[1:]: - i = 0 - newData = data - if cmd == 'c': - newData = [] - while i < len(data): - # since all commands are now relative, we can think of previous point as (0,0) - # and new point (dx,dy) is (data[i+4],data[i+5]) - # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 - (p1x, p1y) = (data[i], data[i + 1]) - (p2x, p2y) = (data[i + 2], data[i + 3]) - dx = data[i + 4] - dy = data[i + 5] - - foundStraightCurve = False - - if dx == 0: - if p1x == 0 and p2x == 0: - foundStraightCurve = True - else: - m = dy / dx - if p1y == m * p1x and p2y == m * p2x: - foundStraightCurve = True - - if foundStraightCurve: - # flush any existing curve coords first - if newData: - newPath.append((cmd, newData)) - newData = [] - # now create a straight line segment - newPath.append(('l', [dx, dy])) - else: - newData.extend(data[i:i + 6]) - - i += 6 - if newData or cmd == 'z' or cmd == 'Z': - newPath.append((cmd, newData)) - path = newPath - - # collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [] - for (cmd, data) in path: - if prevCmd == '': - # initialize with current path cmd and data - prevCmd = cmd - prevData = data - else: - # collapse if - # - cmd is not moveto (explicit moveto commands are not drawn) - # - the previous and current commands are the same type, - # - the previous command is moveto and the current is lineto - # (subsequent moveto pairs are treated as implicit lineto commands) - if cmd != 'm' and (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')): - prevData.extend(data) - # else flush the previous command if it is not the same type as the current command + if foundStraightCurve: + # flush any existing curve coords first + if newData: + newPath.append( (cmd,newData) ) + newData = [] + # now create a straight line segment + newPath.append( ('l', [dx,dy]) ) + numCurvesStraightened += 1 else: - newPath.append((prevCmd, prevData)) - prevCmd = cmd - prevData = data - # flush last command and data - newPath.append((prevCmd, prevData)) - path = newPath + newData.extend(data[i:i+6]) - # convert to shorthand path segments where possible - newPath = [] - for (cmd, data) in path: - # convert line segments into h,v where possible - if cmd == 'l': - i = 0 - lineTuples = [] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing line command - newPath.append(('l', lineTuples)) - lineTuples = [] - # append the v and then the remaining line coords - newPath.append(('v', [data[i + 1]])) - stats.num_path_segments_removed += 1 - elif data[i + 1] == 0: - if lineTuples: - # flush the line command, then append the h and then the remaining line coords - newPath.append(('l', lineTuples)) - lineTuples = [] - newPath.append(('h', [data[i]])) - stats.num_path_segments_removed += 1 - else: - lineTuples.extend(data[i:i + 2]) - i += 2 - if lineTuples: - newPath.append(('l', lineTuples)) - # also handle implied relative linetos - elif cmd == 'm': - i = 2 - lineTuples = [data[0], data[1]] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing m/l command - newPath.append((cmd, lineTuples)) - lineTuples = [] - cmd = 'l' # dealing with linetos now - # append the v and then the remaining line coords - newPath.append(('v', [data[i + 1]])) - stats.num_path_segments_removed += 1 - elif data[i + 1] == 0: - if lineTuples: - # flush the m/l command, then append the h and then the remaining line coords - newPath.append((cmd, lineTuples)) - lineTuples = [] - cmd = 'l' # dealing with linetos now - newPath.append(('h', [data[i]])) - stats.num_path_segments_removed += 1 - else: - lineTuples.extend(data[i:i + 2]) - i += 2 - if lineTuples: - newPath.append((cmd, lineTuples)) - # convert Bézier curve segments into s where possible - elif cmd == 'c': - # set up the assumed bezier control point as the current point, - # i.e. (0,0) since we're using relative coords - bez_ctl_pt = (0, 0) - # however if the previous command was 's' - # the assumed control point is a reflection of the previous control point at the current point - if len(newPath): - (prevCmd, prevData) = newPath[-1] - if prevCmd == 's': - bez_ctl_pt = (prevData[-2] - prevData[-4], prevData[-1] - prevData[-3]) - i = 0 - curveTuples = [] - while i < len(data): - # rotate by 180deg means negate both coordinates - # if the previous control point is equal then we can substitute a - # shorthand bezier command - if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i + 1]: - if curveTuples: - newPath.append(('c', curveTuples)) - curveTuples = [] - # append the s command - newPath.append(('s', [data[i + 2], data[i + 3], data[i + 4], data[i + 5]])) - stats.num_path_segments_removed += 1 - else: - j = 0 - while j <= 5: - curveTuples.append(data[i + j]) - j += 1 + i += 6 + if newData or cmd == 'z' or cmd == 'Z': + newPath.append( (cmd,newData) ) + path = newPath - # set up control point for next curve segment - bez_ctl_pt = (data[i + 4] - data[i + 2], data[i + 5] - data[i + 3]) - i += 6 + # collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [] + for (cmd,data) in path: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append( (prevCmd, prevData) ) + prevCmd = '' + prevData = [] - if curveTuples: - newPath.append(('c', curveTuples)) - # convert quadratic curve segments into t where possible - elif cmd == 'q': - quad_ctl_pt = (0, 0) - i = 0 - curveTuples = [] - while i < len(data): - if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i + 1]: - if curveTuples: - newPath.append(('q', curveTuples)) - curveTuples = [] - # append the t command - newPath.append(('t', [data[i + 2], data[i + 3]])) - stats.num_path_segments_removed += 1 - else: - j = 0 - while j <= 3: - curveTuples.append(data[i + j]) - j += 1 + # if the previous and current commands are the same type, + # or the previous command is moveto and the current is lineto, collapse, + # but only if they are not move commands (since move can contain implicit lineto commands) + if (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')) and cmd != 'm': + prevData.extend(data) - quad_ctl_pt = (data[i + 2] - data[i], data[i + 3] - data[i + 1]) - i += 4 + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append( (prevCmd, prevData) ) + path = newPath - if curveTuples: - newPath.append(('q', curveTuples)) - else: - newPath.append((cmd, data)) - path = newPath + # convert to shorthand path segments where possible + newPath = [] + for (cmd,data) in path: + # convert line segments into h,v where possible + if cmd == 'l': + i = 0 + lineTuples = [] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing line command + newPath.append( ('l', lineTuples) ) + lineTuples = [] + # append the v and then the remaining line coords + newPath.append( ('v', [data[i+1]]) ) + numPathSegmentsReduced += 1 + elif data[i+1] == 0: + if lineTuples: + # flush the line command, then append the h and then the remaining line coords + newPath.append( ('l', lineTuples) ) + lineTuples = [] + newPath.append( ('h', [data[i]]) ) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i+2]) + i += 2 + if lineTuples: + newPath.append( ('l', lineTuples) ) + # also handle implied relative linetos + elif cmd == 'm': + i = 2 + lineTuples = [data[0], data[1]] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing m/l command + newPath.append( (cmd, lineTuples) ) + lineTuples = [] + cmd = 'l' # dealing with linetos now + # append the v and then the remaining line coords + newPath.append( ('v', [data[i+1]]) ) + numPathSegmentsReduced += 1 + elif data[i+1] == 0: + if lineTuples: + # flush the m/l command, then append the h and then the remaining line coords + newPath.append( (cmd, lineTuples) ) + lineTuples = [] + cmd = 'l' # dealing with linetos now + newPath.append( ('h', [data[i]]) ) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i+2]) + i += 2 + if lineTuples: + newPath.append( (cmd, lineTuples) ) + # convert Bézier curve segments into s where possible + elif cmd == 'c': + bez_ctl_pt = (0,0) + i = 0 + curveTuples = [] + while i < len(data): + # rotate by 180deg means negate both coordinates + # if the previous control point is equal then we can substitute a + # shorthand bezier command + if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i+1]: + if curveTuples: + newPath.append( ('c', curveTuples) ) + curveTuples = [] + # append the s command + newPath.append( ('s', [data[i+2], data[i+3], data[i+4], data[i+5]]) ) + numPathSegmentsReduced += 1 + else: + j = 0 + while j <= 5: + curveTuples.append(data[i+j]) + j += 1 - # For each m, l, h or v, collapse unnecessary coordinates that run in the same direction - # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change. - # If the path has intermediate markers we have to preserve intermediate nodes, though. - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists, even if we're deleting items, because these - # deletions are relatively cheap. - if not has_intermediate_markers: - for pathIndex in range(len(path)): - cmd, data = path[pathIndex] + # set up control point for next curve segment + bez_ctl_pt = (data[i+4]-data[i+2], data[i+5]-data[i+3]) + i += 6 - # h / v expects only one parameter and we start drawing with the first (so we need at least 2) - if cmd in ['h', 'v'] and len(data) >= 2: - coordIndex = 0 - while coordIndex+1 < len(data): - if is_same_sign(data[coordIndex], data[coordIndex+1]): - data[coordIndex] += data[coordIndex+1] - del data[coordIndex+1] - stats.num_path_segments_removed += 1 - else: - coordIndex += 1 + if curveTuples: + newPath.append( ('c', curveTuples) ) + # convert quadratic curve segments into t where possible + elif cmd == 'q': + quad_ctl_pt = (0,0) + i = 0 + curveTuples = [] + while i < len(data): + if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i+1]: + if curveTuples: + newPath.append( ('q', curveTuples) ) + curveTuples = [] + # append the t command + newPath.append( ('t', [data[i+2], data[i+3]]) ) + numPathSegmentsReduced += 1 + else: + j = 0; + while j <= 3: + curveTuples.append(data[i+j]) + j += 1 - # l expects two parameters and we start drawing with the first (so we need at least 4) - elif cmd == 'l' and len(data) >= 4: - coordIndex = 0 - while coordIndex+2 < len(data): - if is_same_direction(*data[coordIndex:coordIndex+4]): - data[coordIndex] += data[coordIndex+2] - data[coordIndex+1] += data[coordIndex+3] - del data[coordIndex+2] # delete the next two elements - del data[coordIndex+2] - stats.num_path_segments_removed += 1 - else: - coordIndex += 2 + quad_ctl_pt = (data[i+2]-data[i], data[i+3]-data[i+1]) + i += 4 - # m expects two parameters but we have to skip the first pair as it's not drawn (so we need at least 6) - elif cmd == 'm' and len(data) >= 6: - coordIndex = 2 - while coordIndex+2 < len(data): - if is_same_direction(*data[coordIndex:coordIndex+4]): - data[coordIndex] += data[coordIndex+2] - data[coordIndex+1] += data[coordIndex+3] - del data[coordIndex+2] # delete the next two elements - del data[coordIndex+2] - stats.num_path_segments_removed += 1 - else: - coordIndex += 2 + if curveTuples: + newPath.append( ('q', curveTuples) ) + else: + newPath.append( (cmd, data) ) + path = newPath - # it is possible that we have consecutive h, v, c, t commands now - # so again collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [path[0]] - for (cmd, data) in path[1:]: - # flush the previous command if it is not the same type as the current command - if prevCmd != '': - if cmd != prevCmd or cmd == 'm': - newPath.append((prevCmd, prevData)) - prevCmd = '' - prevData = [] + # for each h or v, collapse unnecessary coordinates that run in the same direction + # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists, even if we're deleting items, because these + # deletions are relatively cheap. + for pathIndex in xrange(1, len(path)): + cmd, data = path[pathIndex] + if cmd in ['h','v'] and len(data) > 1: + coordIndex = 1 + while coordIndex < len(data): + if isSameSign(data[coordIndex - 1], data[coordIndex]): + data[coordIndex - 1] += data[coordIndex] + del data[coordIndex] + numPathSegmentsReduced += 1 + else: + coordIndex += 1 - # if the previous and current commands are the same type, collapse - if cmd == prevCmd and cmd != 'm': + # it is possible that we have consecutive h, v, c, t commands now + # so again collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [path[0]] + for (cmd,data) in path[1:]: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append( (prevCmd, prevData) ) + prevCmd = '' + prevData = [] + + # if the previous and current commands are the same type, collapse + if cmd == prevCmd and cmd != 'm': prevData.extend(data) - # save last command and data - else: - prevCmd = cmd - prevData = data - # flush last command and data - if prevCmd != '': - newPath.append((prevCmd, prevData)) - path = newPath + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append( (prevCmd, prevData) ) + path = newPath - newPathStr = serializePath(path, options) + newPathStr = serializePath(path, options) + numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) + element.setAttribute('d', newPathStr) - # if for whatever reason we actually made the path longer don't use it - # TODO: maybe we could compare path lengths after each optimization step and use the shortest - if len(newPathStr) <= len(oldPathStr): - stats.num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr)) - element.setAttribute('d', newPathStr) def parseListOfPoints(s): - """ - Parse string into a list of points. + """ + Parse string into a list of points. - Returns a list containing an even number of coordinate strings - """ - i = 0 + Returns a list containing an even number of coordinate strings + """ + i = 0 - # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? - # coordinate-pair = coordinate comma-or-wsp coordinate - # coordinate = sign? integer - # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) - ws_nums = RE_COMMA_WSP.split(s.strip()) - nums = [] + # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? + # coordinate-pair = coordinate comma-or-wsp coordinate + # coordinate = sign? integer + # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) + ws_nums = re.split(r"\s*,?\s*", s.strip()) + nums = [] - # also, if 100-100 is found, split it into two also - # <polygon points="100,-100,100-100,100-100-100,-100-100" /> - for i in range(len(ws_nums)): - negcoords = ws_nums[i].split("-") + # also, if 100-100 is found, split it into two also + # <polygon points="100,-100,100-100,100-100-100,-100-100" /> + for i in xrange(len(ws_nums)): + negcoords = ws_nums[i].split("-") - # this string didn't have any negative coordinates - if len(negcoords) == 1: - nums.append(negcoords[0]) - # we got negative coords - else: - for j in range(len(negcoords)): - # first number could be positive - if j == 0: - if negcoords[0] != '': - nums.append(negcoords[0]) - # otherwise all other strings will be negative - else: - # unless we accidentally split a number that was in scientific notation - # and had a negative exponent (500.00e-1) - prev = "" - if len(nums): - prev = nums[len(nums) - 1] - if prev and prev[len(prev) - 1] in ['e', 'E']: - nums[len(nums) - 1] = prev + '-' + negcoords[j] - else: - nums.append('-' + negcoords[j]) + # this string didn't have any negative coordinates + if len(negcoords) == 1: + nums.append(negcoords[0]) + # we got negative coords + else: + for j in xrange(len(negcoords)): + # first number could be positive + if j == 0: + if negcoords[0] != '': + nums.append(negcoords[0]) + # otherwise all other strings will be negative + else: + # unless we accidentally split a number that was in scientific notation + # and had a negative exponent (500.00e-1) + prev = nums[len(nums)-1] + if prev[len(prev)-1] in ['e', 'E']: + nums[len(nums)-1] = prev + '-' + negcoords[j] + else: + nums.append( '-'+negcoords[j] ) - # if we have an odd number of points, return empty - if len(nums) % 2 != 0: - return [] + # if we have an odd number of points, return empty + if len(nums) % 2 != 0: return [] - # now resolve into Decimal values - i = 0 - while i < len(nums): - try: - nums[i] = getcontext().create_decimal(nums[i]) - nums[i + 1] = getcontext().create_decimal(nums[i + 1]) - except InvalidOperation: # one of the lengths had a unit or is an invalid number - return [] + # now resolve into Decimal values + i = 0 + while i < len(nums): + try: + nums[i] = getcontext().create_decimal(nums[i]) + nums[i + 1] = getcontext().create_decimal(nums[i + 1]) + except decimal.InvalidOperation: # one of the lengths had a unit or is an invalid number + return [] - i += 2 + i += 2 - return nums + return nums -def clean_polygon(elem, options): - """ - Remove unnecessary closing point of polygon points attribute - """ - num_points_removed_from_polygon = 0 - pts = parseListOfPoints(elem.getAttribute('points')) - N = len(pts) / 2 - if N >= 2: - (startx, starty) = pts[:2] - (endx, endy) = pts[-2:] - if startx == endx and starty == endy: - del pts[-2:] - num_points_removed_from_polygon += 1 - elem.setAttribute('points', scourCoordinates(pts, options, True)) - return num_points_removed_from_polygon +def cleanPolygon(elem, options): + """ + Remove unnecessary closing point of polygon points attribute + """ + global numPointsRemovedFromPolygon + + pts = parseListOfPoints(elem.getAttribute('points')) + N = len(pts)/2 + if N >= 2: + (startx,starty) = pts[:2] + (endx,endy) = pts[-2:] + if startx == endx and starty == endy: + del pts[-2:] + numPointsRemovedFromPolygon += 1 + elem.setAttribute('points', scourCoordinates(pts, options, True)) + def cleanPolyline(elem, options): - """ - Scour the polyline points attribute - """ - pts = parseListOfPoints(elem.getAttribute('points')) - elem.setAttribute('points', scourCoordinates(pts, options, True)) + """ + Scour the polyline points attribute + """ + pts = parseListOfPoints(elem.getAttribute('points')) + elem.setAttribute('points', scourCoordinates(pts, options, True)) -def controlPoints(cmd, data): - """ - Checks if there are control points in the path data - - Returns the indices of all values in the path data which are control points - """ - cmd = cmd.lower() - if cmd in ['c', 's', 'q']: - indices = range(len(data)) - if cmd == 'c': # c: (x1 y1 x2 y2 x y)+ - return [index for index in indices if (index % 6) < 4] - elif cmd in ['s', 'q']: # s: (x2 y2 x y)+ q: (x1 y1 x y)+ - return [index for index in indices if (index % 4) < 2] - - return [] - - -def flags(cmd, data): - """ - Checks if there are flags in the path data - - Returns the indices of all values in the path data which are flags - """ - if cmd.lower() == 'a': # a: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ - indices = range(len(data)) - return [index for index in indices if (index % 7) in [3, 4]] - - return [] - def serializePath(pathObj, options): - """ - Reserializes the path data with some cleanups. - """ - # elliptical arc commands must have comma/wsp separating the coordinates - # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 - return ''.join(cmd + scourCoordinates(data, options, - control_points=controlPoints(cmd, data), - flags=flags(cmd, data)) - for cmd, data in pathObj) + """ + Reserializes the path data with some cleanups. + """ + # elliptical arc commands must have comma/wsp separating the coordinates + # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 + return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a')) for cmd, data in pathObj]) + def serializeTransform(transformObj): - """ - Reserializes the transform data with some cleanups. - """ - return ' '.join(command + '(' + ' '.join(scourUnitlessLength(number) for number in numbers) + ')' - for command, numbers in transformObj) + """ + Reserializes the transform data with some cleanups. + """ + return ' '.join( + [command + '(' + ' '.join( + [scourUnitlessLength(number) for number in numbers] + ) + ')' + for command, numbers in transformObj] + ) -def scourCoordinates(data, options, force_whitespace=False, control_points=[], flags=[]): - """ - Serializes coordinate data with some cleanups: - - removes all trailing zeros after the decimal - - integerize coordinates if possible - - removes extraneous whitespace - - adds spaces between values in a subcommand if required (or if force_whitespace is True) - """ - if data is not None: - newData = [] - c = 0 - previousCoord = '' - for coord in data: - is_control_point = c in control_points - scouredCoord = scourUnitlessLength(coord, - nonsci_output=options.nonsci_output, - renderer_workaround=options.renderer_workaround, - is_control_point=is_control_point) - # don't output a space if this number starts with a dot (.) or minus sign (-); we only need a space if - # - this number starts with a digit - # - this number starts with a dot but the previous number had *no* dot or exponent - # i.e. '1.3 0.5' -> '1.3.5' or '1e3 0.5' -> '1e3.5' is fine but '123 0.5' -> '123.5' is obviously not - # - 'force_whitespace' is explicitly set to 'True' - # we never need a space after flags (occurring in elliptical arcs), but librsvg struggles without it - if (c > 0 - and (force_whitespace - or scouredCoord[0].isdigit() - or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord))) - and ((c-1 not in flags) or options.renderer_workaround)): - newData.append(' ') - # add the scoured coordinate to the path string - newData.append(scouredCoord) - previousCoord = scouredCoord - c += 1 +def scourCoordinates(data, options, forceCommaWsp = False): + """ + Serializes coordinate data with some cleanups: + - removes all trailing zeros after the decimal + - integerize coordinates if possible + - removes extraneous whitespace + - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) + """ + if data != None: + newData = [] + c = 0 + previousCoord = '' + for coord in data: + scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) + # only need the comma if the current number starts with a digit + # (numbers can start with - without needing a comma before) + # or if forceCommaWsp is True + # or if this number starts with a dot and the previous number + # had *no* dot or exponent (so we can go like -5.5.5 for -5.5,0.5 + # and 4e4.5 for 40000,0.5) + if c > 0 and (forceCommaWsp + or scouredCoord[0].isdigit() + or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) + ): + newData.append( ' ' ) - # What we need to do to work around GNOME bugs 548494, 563933 and 620565, is to make sure that a dot doesn't - # immediately follow a command (so 'h50' and 'h0.5' are allowed, but not 'h.5'). - # Then, we need to add a space character after any coordinates having an 'e' (scientific notation), - # so as to have the exponent separate from the next number. - # TODO: Check whether this is still required (bugs all marked as fixed, might be time to phase it out) - if options.renderer_workaround: - if len(newData) > 0: - for i in range(1, len(newData)): - if newData[i][0] == '-' and 'e' in newData[i - 1]: - newData[i - 1] += ' ' - return ''.join(newData) - else: + # add the scoured coordinate to the path string + newData.append( scouredCoord ) + previousCoord = scouredCoord + c += 1 + + # What we need to do to work around GNOME bugs 548494, 563933 and + # 620565, which are being fixed and unfixed in Ubuntu, is + # to make sure that a dot doesn't immediately follow a command + # (so 'h50' and 'h0.5' are allowed, but not 'h.5'). + # Then, we need to add a space character after any coordinates + # having an 'e' (scientific notation), so as to have the exponent + # separate from the next number. + if options.renderer_workaround: + if len(newData) > 0: + for i in xrange(1, len(newData)): + if newData[i][0] == '-' and 'e' in newData[i - 1]: + newData[i - 1] += ' ' return ''.join(newData) + else: + return ''.join(newData) + + return '' - return '' def scourLength(length): - """ - Scours a length. Accepts units. - """ - length = SVGLength(length) + """ + Scours a length. Accepts units. + """ + length = SVGLength(length) - return scourUnitlessLength(length.value) + Unit.str(length.units) + return scourUnitlessLength(length.value) + Unit.str(length.units) -def scourUnitlessLength(length, nonsci_output=False, renderer_workaround=False, is_control_point=False): # length is of a numeric type - """ - Scours the numeric part of a length only. Does not accept units. +def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type + """ + Scours the numeric part of a length only. Does not accept units. - This is faster than scourLength on elements guaranteed not to - contain units. - """ - if not isinstance(length, Decimal): - length = getcontext().create_decimal(str(length)) - initial_length = length + This is faster than scourLength on elements guaranteed not to + contain units. + """ + # reduce to the proper number of digits + if not isinstance(length, Decimal): + length = getcontext().create_decimal(str(length)) + # if the value is an integer, it may still have .0[...] attached to it for some reason + # remove those + if int(length) == length: + length = getcontext().create_decimal(int(length)) - # reduce numeric precision - # plus() corresponds to the unary prefix plus operator and applies context precision and rounding - if is_control_point: - length = scouringContextC.plus(length) - else: - length = scouringContext.plus(length) + # gather the non-scientific notation version of the coordinate. + # this may actually be in scientific notation if the value is + # sufficiently large or small, so this is a misnomer. + nonsci = unicode(length).lower().replace("e+", "e") + if not needsRendererWorkaround: + if len(nonsci) > 2 and nonsci[:2] == '0.': + nonsci = nonsci[1:] # remove the 0, leave the dot + elif len(nonsci) > 3 and nonsci[:3] == '-0.': + nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot - # remove trailing zeroes as we do not care for significance - intLength = length.to_integral_value() - if length == intLength: - length = Decimal(intLength) - else: - length = length.normalize() + if len(nonsci) > 3: # avoid calling normalize unless strictly necessary + # and then the scientific notation version, with E+NUMBER replaced with + # just eNUMBER, since SVG accepts this. + sci = unicode(length.normalize()).lower().replace("e+", "e") - # Gather the non-scientific notation version of the coordinate. - # Re-quantize from the initial value to prevent unnecessary loss of precision - # (e.g. 123.4 should become 123, not 120 or even 100) - nonsci = '{0:f}'.format(length) - nonsci = '{0:f}'.format(initial_length.quantize(Decimal(nonsci))) - if not renderer_workaround: - if len(nonsci) > 2 and nonsci[:2] == '0.': - nonsci = nonsci[1:] # remove the 0, leave the dot - elif len(nonsci) > 3 and nonsci[:3] == '-0.': - nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot - return_value = nonsci + if len(sci) < len(nonsci): return sci + else: return nonsci + else: return nonsci - # Prevent scientific notation, interferes with some label maker software - if nonsci_output: - return return_value +def reducePrecision(element) : + """ + Because opacities, letter spacings, stroke widths and all that don't need + to be preserved in SVG files with 9 digits of precision. - # Gather the scientific notation version of the coordinate which - # can only be shorter if the length of the number is at least 4 characters (e.g. 1000 = 1e3). - if len(nonsci) > 3: - # We have to implement this ourselves since both 'normalize()' and 'to_sci_string()' - # don't handle negative exponents in a reasonable way (e.g. 0.000001 remains unchanged) - exponent = length.adjusted() # how far do we have to shift the dot? - length = length.scaleb(-exponent).normalize() # shift the dot and remove potential trailing zeroes + Takes all of these attributes, in the given element node and its children, + and reduces their precision to the current Decimal context's precision. + Also checks for the attributes actually being lengths, not 'inherit', 'none' + or anything that isn't an SVGLength. - sci = six.text_type(length) + 'e' + six.text_type(exponent) + Returns the number of bytes saved after performing these reductions. + """ + num = 0 - if len(sci) < len(nonsci): - return_value = sci + styles = _getStyle(element) + for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', + 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', + 'stroke-dashoffset', 'letter-spacing', 'word-spacing', + 'kerning', 'font-size-adjust', 'font-size', + 'stroke-width']: + val = element.getAttribute(lengthAttr) + if val != '': + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + element.setAttribute(lengthAttr, newVal) + # repeat for attributes hidden in styles + if lengthAttr in styles.keys(): + val = styles[lengthAttr] + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + styles[lengthAttr] = newVal + _setStyle(element, styles) - return return_value + for child in element.childNodes: + if child.nodeType == 1: + num += reducePrecision(child) + return num -def reducePrecision(element): - """ - Because opacities, letter spacings, stroke widths and all that don't need - to be preserved in SVG files with 9 digits of precision. - - Takes all of these attributes, in the given element node and its children, - and reduces their precision to the current Decimal context's precision. - Also checks for the attributes actually being lengths, not 'inherit', 'none' - or anything that isn't an SVGLength. - - Returns the number of bytes saved after performing these reductions. - """ - num = 0 - - styles = _getStyle(element) - for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', - 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', - 'stroke-dashoffset', 'letter-spacing', 'word-spacing', - 'kerning', 'font-size-adjust', 'font-size', - 'stroke-width']: - val = element.getAttribute(lengthAttr) - if val != '': - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - element.setAttribute(lengthAttr, newVal) - # repeat for attributes hidden in styles - if lengthAttr in styles: - val = styles[lengthAttr] - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - styles[lengthAttr] = newVal - _setStyle(element, styles) - - for child in element.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - num += reducePrecision(child) - - return num def optimizeAngle(angle): - """ - Because any rotation can be expressed within 360 degrees - of any given number, and since negative angles sometimes - are one character longer than corresponding positive angle, - we shorten the number to one in the range to [-90, 270[. - """ - # First, we put the new angle in the range ]-360, 360[. - # The modulo operator yields results with the sign of the - # divisor, so for negative dividends, we preserve the sign - # of the angle. - if angle < 0: - angle %= -360 - else: - angle %= 360 - # 720 degrees is unnecessary, as 360 covers all angles. - # As "-x" is shorter than "35x" and "-xxx" one character - # longer than positive angles <= 260, we constrain angle - # range to [-90, 270[ (or, equally valid: ]-100, 260]). - if angle >= 270: - angle -= 360 - elif angle < -90: - angle += 360 - return angle + """ + Because any rotation can be expressed within 360 degrees + of any given number, and since negative angles sometimes + are one character longer than corresponding positive angle, + we shorten the number to one in the range to [-90, 270[. + """ + # First, we put the new angle in the range ]-360, 360[. + # The modulo operator yields results with the sign of the + # divisor, so for negative dividends, we preserve the sign + # of the angle. + if angle < 0: angle %= -360 + else: angle %= 360 + # 720 degrees is unneccessary, as 360 covers all angles. + # As "-x" is shorter than "35x" and "-xxx" one character + # longer than positive angles <= 260, we constrain angle + # range to [-90, 270[ (or, equally valid: ]-100, 260]). + if angle >= 270: angle -= 360 + elif angle < -90: angle += 360 + return angle + def optimizeTransform(transform): - """ - Optimises a series of transformations parsed from a single - transform="" attribute. + """ + Optimises a series of transformations parsed from a single + transform="" attribute. - The transformation list is modified in-place. - """ - # FIXME: reordering these would optimize even more cases: - # first: Fold consecutive runs of the same transformation - # extra: Attempt to cast between types to create sameness: - # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all - # are rotations (90, 180, 180) -- thus "rotate(90)" - # second: Simplify transforms where numbers are optional. - # third: Attempt to simplify any single remaining matrix() - # - # if there's only one transformation and it's a matrix, - # try to make it a shorter non-matrix transformation - # NOTE: as matrix(a b c d e f) in SVG means the matrix: - # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| - # | b d f | translating them | B1 B2 B3 | - # |_ 0 0 1 _| to more readable |_ 0 0 1 _| - if len(transform) == 1 and transform[0][0] == 'matrix': - matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] - # |¯ 1 0 0 ¯| - # | 0 1 0 | Identity matrix (no transformation) - # |_ 0 0 1 _| - if matrix == [1, 0, 0, 1, 0, 0]: - del transform[0] - # |¯ 1 0 X ¯| - # | 0 1 Y | Translation by (X, Y). - # |_ 0 0 1 _| - elif (A1 == 1 and A2 == 0 - and B1 == 0 and B2 == 1): - transform[0] = ('translate', [A3, B3]) - # |¯ X 0 0 ¯| - # | 0 Y 0 | Scaling by (X, Y). - # |_ 0 0 1 _| - elif (A2 == 0 and A3 == 0 - and B1 == 0 and B3 == 0): - transform[0] = ('scale', [A1, B2]) - # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, - # | sin(A) cos(A) 0 | clockwise, about the origin. - # |_ 0 0 1 _| A is in degrees, [-180...180]. - elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 - and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 - # as cos² A + sin² A == 1 and as decimal trig is approximate: - # FIXME: the "epsilon" term here should really be some function - # of the precision of the (sin|cos)_A terms, not 1e-15: - and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): - sin_A, cos_A = B1, A1 - # while asin(A) and acos(A) both only have an 180° range - # the sign of sin(A) and cos(A) varies across quadrants, - # letting us hone in on the angle the matrix represents: - # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 - # - # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg - # shows asin has the correct angle the middle quadrants: - A = Decimal(str(math.degrees(math.asin(float(sin_A))))) - if cos_A < 0: # otherwise needs adjusting from the edges - if sin_A < 0: - A = -180 - A - else: - A = 180 - A - transform[0] = ('rotate', [A]) + The transformation list is modified in-place. + """ + # FIXME: reordering these would optimize even more cases: + # first: Fold consecutive runs of the same transformation + # extra: Attempt to cast between types to create sameness: + # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all + # are rotations (90, 180, 180) -- thus "rotate(90)" + # second: Simplify transforms where numbers are optional. + # third: Attempt to simplify any single remaining matrix() + # + # if there's only one transformation and it's a matrix, + # try to make it a shorter non-matrix transformation + # NOTE: as matrix(a b c d e f) in SVG means the matrix: + # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| + # | b d f | translating them | B1 B2 B3 | + # |_ 0 0 1 _| to more readable |_ 0 0 1 _| + if len(transform) == 1 and transform[0][0] == 'matrix': + matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] + # |¯ 1 0 0 ¯| + # | 0 1 0 | Identity matrix (no transformation) + # |_ 0 0 1 _| + if matrix == [1, 0, 0, 1, 0, 0]: + del transform[0] + # |¯ 1 0 X ¯| + # | 0 1 Y | Translation by (X, Y). + # |_ 0 0 1 _| + elif (A1 == 1 and A2 == 0 + and B1 == 0 and B2 == 1): + transform[0] = ('translate', [A3, B3]) + # |¯ X 0 0 ¯| + # | 0 Y 0 | Scaling by (X, Y). + # |_ 0 0 1 _| + elif ( A2 == 0 and A3 == 0 + and B1 == 0 and B3 == 0): + transform[0] = ('scale', [A1, B2]) + # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, + # | sin(A) cos(A) 0 | clockwise, about the origin. + # |_ 0 0 1 _| A is in degrees, [-180...180]. + elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 + and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 + # as cos² A + sin² A == 1 and as decimal trig is approximate: + # FIXME: the "epsilon" term here should really be some function + # of the precision of the (sin|cos)_A terms, not 1e-15: + and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): + sin_A, cos_A = B1, A1 + # while asin(A) and acos(A) both only have an 180° range + # the sign of sin(A) and cos(A) varies across quadrants, + # letting us hone in on the angle the matrix represents: + # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 + # + # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg + # shows asin has the correct angle the middle quadrants: + A = Decimal(str(math.degrees(math.asin(float(sin_A))))) + if cos_A < 0: # otherwise needs adjusting from the edges + if sin_A < 0: + A = -180 - A + else: + A = 180 - A + transform[0] = ('rotate', [A]) - # Simplify transformations where numbers are optional. - for type, args in transform: - if type == 'translate': - # Only the X coordinate is required for translations. - # If the Y coordinate is unspecified, it's 0. - if len(args) == 2 and args[1] == 0: - del args[1] - elif type == 'rotate': - args[0] = optimizeAngle(args[0]) # angle - # Only the angle is required for rotations. - # If the coordinates are unspecified, it's the origin (0, 0). - if len(args) == 3 and args[1] == args[2] == 0: - del args[1:] - elif type == 'scale': - # Only the X scaling factor is required. - # If the Y factor is unspecified, it's the same as X. - if len(args) == 2 and args[0] == args[1]: - del args[1] + # Simplify transformations where numbers are optional. + for type, args in transform: + if type == 'translate': + # Only the X coordinate is required for translations. + # If the Y coordinate is unspecified, it's 0. + if len(args) == 2 and args[1] == 0: + del args[1] + elif type == 'rotate': + args[0] = optimizeAngle(args[0]) # angle + # Only the angle is required for rotations. + # If the coordinates are unspecified, it's the origin (0, 0). + if len(args) == 3 and args[1] == args[2] == 0: + del args[1:] + elif type == 'scale': + # Only the X scaling factor is required. + # If the Y factor is unspecified, it's the same as X. + if len(args) == 2 and args[0] == args[1]: + del args[1] - # Attempt to coalesce runs of the same transformation. - # Translations followed immediately by other translations, - # rotations followed immediately by other rotations, - # scaling followed immediately by other scaling, - # are safe to add. - # Identity skewX/skewY are safe to remove, but how do they accrete? - # |¯ 1 0 0 ¯| - # | tan(A) 1 0 | skews X coordinates by angle A - # |_ 0 0 1 _| - # - # |¯ 1 tan(A) 0 ¯| - # | 0 1 0 | skews Y coordinates by angle A - # |_ 0 0 1 _| - # - # FIXME: A matrix followed immediately by another matrix - # would be safe to multiply together, too. - i = 1 - while i < len(transform): - currType, currArgs = transform[i] - prevType, prevArgs = transform[i - 1] - if currType == prevType == 'translate': - prevArgs[0] += currArgs[0] # x - # for y, only add if the second translation has an explicit y - if len(currArgs) == 2: - if len(prevArgs) == 2: - prevArgs[1] += currArgs[1] # y - elif len(prevArgs) == 1: - prevArgs.append(currArgs[1]) # y + # Attempt to coalesce runs of the same transformation. + # Translations followed immediately by other translations, + # rotations followed immediately by other rotations, + # scaling followed immediately by other scaling, + # are safe to add. + # Identity skewX/skewY are safe to remove, but how do they accrete? + # |¯ 1 0 0 ¯| + # | tan(A) 1 0 | skews X coordinates by angle A + # |_ 0 0 1 _| + # + # |¯ 1 tan(A) 0 ¯| + # | 0 1 0 | skews Y coordinates by angle A + # |_ 0 0 1 _| + # + # FIXME: A matrix followed immediately by another matrix + # would be safe to multiply together, too. + i = 1 + while i < len(transform): + currType, currArgs = transform[i] + prevType, prevArgs = transform[i - 1] + if currType == prevType == 'translate': + prevArgs[0] += currArgs[0] # x + # for y, only add if the second translation has an explicit y + if len(currArgs) == 2: + if len(prevArgs) == 2: + prevArgs[1] += currArgs[1] # y + elif len(prevArgs) == 1: + prevArgs.append(currArgs[1]) # y + del transform[i] + if prevArgs[0] == prevArgs[1] == 0: + # Identity translation! + i -= 1 del transform[i] - if prevArgs[0] == prevArgs[1] == 0: - # Identity translation! - i -= 1 - del transform[i] - elif (currType == prevType == 'rotate' - and len(prevArgs) == len(currArgs) == 1): - # Only coalesce if both rotations are from the origin. - prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) + elif (currType == prevType == 'rotate' + and len(prevArgs) == len(currArgs) == 1): + # Only coalesce if both rotations are from the origin. + prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) + del transform[i] + elif currType == prevType == 'scale': + prevArgs[0] *= currArgs[0] # x + # handle an implicit y + if len(prevArgs) == 2 and len(currArgs) == 2: + # y1 * y2 + prevArgs[1] *= currArgs[1] + elif len(prevArgs) == 1 and len(currArgs) == 2: + # create y2 = uniformscalefactor1 * y2 + prevArgs.append(prevArgs[0] * currArgs[1]) + elif len(prevArgs) == 2 and len(currArgs) == 1: + # y1 * uniformscalefactor2 + prevArgs[1] *= currArgs[0] + del transform[i] + if prevArgs[0] == prevArgs[1] == 1: + # Identity scale! + i -= 1 del transform[i] - elif currType == prevType == 'scale': - prevArgs[0] *= currArgs[0] # x - # handle an implicit y - if len(prevArgs) == 2 and len(currArgs) == 2: - # y1 * y2 - prevArgs[1] *= currArgs[1] - elif len(prevArgs) == 1 and len(currArgs) == 2: - # create y2 = uniformscalefactor1 * y2 - prevArgs.append(prevArgs[0] * currArgs[1]) - elif len(prevArgs) == 2 and len(currArgs) == 1: - # y1 * uniformscalefactor2 - prevArgs[1] *= currArgs[0] - del transform[i] - # if prevArgs is [1] or [1, 1], then it is effectively an - # identity matrix and can be removed. - if prevArgs[0] == 1 and (len(prevArgs) == 1 or prevArgs[1] == 1): - # Identity scale! - i -= 1 - del transform[i] - else: - i += 1 + else: + i += 1 - # Some fixups are needed for single-element transformation lists, since - # the loop above was to coalesce elements with their predecessors in the - # list, and thus it required 2 elements. - i = 0 - while i < len(transform): - currType, currArgs = transform[i] - if ((currType == 'skewX' or currType == 'skewY') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity skew! - del transform[i] - elif ((currType == 'rotate') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity rotation! - del transform[i] - else: - i += 1 + # Some fixups are needed for single-element transformation lists, since + # the loop above was to coalesce elements with their predecessors in the + # list, and thus it required 2 elements. + i = 0 + while i < len(transform): + currType, currArgs = transform[i] + if ((currType == 'skewX' or currType == 'skewY') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity skew! + del transform[i] + elif ((currType == 'rotate') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity rotation! + del transform[i] + else: + i += 1 -def optimizeTransforms(element, options): - """ - Attempts to optimise transform specifications on the given node and its children. - Returns the number of bytes saved after performing these reductions. - """ - num = 0 +def optimizeTransforms(element, options) : + """ + Attempts to optimise transform specifications on the given node and its children. - for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: - val = element.getAttribute(transformAttr) - if val != '': - transform = svg_transform_parser.parse(val) + Returns the number of bytes saved after performing these reductions. + """ + num = 0 - optimizeTransform(transform) + for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: + val = element.getAttribute(transformAttr) + if val != '': + transform = svg_transform_parser.parse(val) - newVal = serializeTransform(transform) + optimizeTransform(transform) - if len(newVal) < len(val): - if len(newVal): - element.setAttribute(transformAttr, newVal) - else: - element.removeAttribute(transformAttr) - num += len(val) - len(newVal) + newVal = serializeTransform(transform) - for child in element.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - num += optimizeTransforms(child, options) + if len(newVal) < len(val): + if len(newVal): + element.setAttribute(transformAttr, newVal) + else: + element.removeAttribute(transformAttr) + num += len(val) - len(newVal) - return num + for child in element.childNodes: + if child.nodeType == 1: + num += optimizeTransforms(child, options) + + return num -def remove_comments(element, stats): - """ - Removes comments from the element and its children. - """ - if isinstance(element, xml.dom.minidom.Comment): - stats.num_bytes_saved_in_comments += len(element.data) - stats.num_comments_removed += 1 - element.parentNode.removeChild(element) - else: - for subelement in element.childNodes[:]: - remove_comments(subelement, stats) +def removeComments(element) : + """ + Removes comments from the element and its children. + """ + global numCommentBytes + + if isinstance(element, xml.dom.minidom.Document): + # must process the document object separately, because its + # documentElement's nodes have None as their parentNode + for subelement in element.childNodes: + if isinstance(element, xml.dom.minidom.Comment): + numCommentBytes += len(element.data) + element.documentElement.removeChild(subelement) + else: + removeComments(subelement) + elif isinstance(element, xml.dom.minidom.Comment): + numCommentBytes += len(element.data) + element.parentNode.removeChild(element) + else: + for subelement in element.childNodes: + removeComments(subelement) -def embed_rasters(element, options): - import base64 - """ + +def embedRasters(element, options) : + import base64 + import urllib + """ Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers - """ - num_rasters_embedded = 0 + """ + global numRastersEmbedded - href = element.getAttributeNS(NS['XLINK'], 'href') + href = element.getAttributeNS(NS['XLINK'],'href') - # if xlink:href is set, then grab the id - if href != '' and len(href) > 1: - ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] + # if xlink:href is set, then grab the id + if href != '' and len(href) > 1: + # find if href value has filename ext + ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] - # only operate on files with 'png', 'jpg', and 'gif' file extensions - if ext in ['png', 'jpg', 'gif']: - # fix common issues with file paths - # TODO: should we warn the user instead of trying to correct those invalid URIs? - # convert backslashes to slashes - href_fixed = href.replace('\\', '/') - # absolute 'file:' URIs have to use three slashes (unless specifying a host which I've never seen) - href_fixed = re.sub('file:/+', 'file:///', href_fixed) + # look for 'png', 'jpg', and 'gif' extensions + if ext == 'png' or ext == 'jpg' or ext == 'gif': - # parse the URI to get scheme and path - # in principle it would make sense to work only with this ParseResult and call 'urlunparse()' in the end - # however 'urlunparse(urlparse(file:raster.png))' -> 'file:///raster.png' which is nonsense - parsed_href = urllib.parse.urlparse(href_fixed) + # file:// URLs denote files on the local system too + if href[:7] == 'file://': + href = href[7:] + # does the file exist? + if os.path.isfile(href): + # if this is not an absolute path, set path relative + # to script file based on input arg + infilename = '.' + if options.infilename: infilename = options.infilename + href = os.path.join(os.path.dirname(infilename), href) - # assume locations without protocol point to local files (and should use the 'file:' protocol) - if parsed_href.scheme == '': - parsed_href = parsed_href._replace(scheme='file') - if href_fixed[0] == '/': - href_fixed = 'file://' + href_fixed - else: - href_fixed = 'file:' + href_fixed + rasterdata = '' + # test if file exists locally + if os.path.isfile(href): + # open raster file as raw binary + raster = open( href, "rb") + rasterdata = raster.read() + elif href[:7] == 'http://': + webFile = urllib.urlopen( href ) + rasterdata = webFile.read() + webFile.close() - # relative local paths are relative to the input file, therefore temporarily change the working dir - working_dir_old = None - if parsed_href.scheme == 'file' and parsed_href.path[0] != '/': - if options.infilename: - working_dir_old = os.getcwd() - working_dir_new = os.path.abspath(os.path.dirname(options.infilename)) - os.chdir(working_dir_new) + # ... should we remove all images which don't resolve? + if rasterdata != '' : + # base64-encode raster + b64eRaster = base64.b64encode( rasterdata ) - # open/download the file - try: - file = urllib.request.urlopen(href_fixed) - rasterdata = file.read() - file.close() - except Exception as e: - print("WARNING: Could not open file '" + href + "' for embedding. " - "The raster image will be kept as a reference but might be invalid. " - "(Exception details: " + str(e) + ")", file=options.ensure_value("stdout", sys.stdout)) - rasterdata = '' - finally: - # always restore initial working directory if we changed it above - if working_dir_old is not None: - os.chdir(working_dir_old) + # set href attribute to base64-encoded equivalent + if b64eRaster != '': + # PNG and GIF both have MIME Type 'image/[ext]', but + # JPEG has MIME Type 'image/jpeg' + if ext == 'jpg': + ext = 'jpeg' - # TODO: should we remove all images which don't resolve? - # then we also have to consider unreachable remote locations (i.e. if there is no internet connection) - if rasterdata != '': - # base64-encode raster - b64eRaster = base64.b64encode(rasterdata) + element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) + numRastersEmbedded += 1 + del b64eRaster - # set href attribute to base64-encoded equivalent - if b64eRaster != '': - # PNG and GIF both have MIME Type 'image/[ext]', but - # JPEG has MIME Type 'image/jpeg' - if ext == 'jpg': - ext = 'jpeg' - - element.setAttributeNS(NS['XLINK'], 'href', - 'data:image/' + ext + ';base64,' + b64eRaster.decode()) - num_rasters_embedded += 1 - del b64eRaster - return num_rasters_embedded def properlySizeDoc(docElement, options): - # get doc width and height - w = SVGLength(docElement.getAttribute('width')) - h = SVGLength(docElement.getAttribute('height')) + # get doc width and height + w = SVGLength(docElement.getAttribute('width')) + h = SVGLength(docElement.getAttribute('height')) - # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. - # well, it may be OK for Web browsers and vector editors, but not for librsvg. - if options.renderer_workaround: - if ((w.units != Unit.NONE and w.units != Unit.PX) or - (h.units != Unit.NONE and h.units != Unit.PX)): + # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. + # well, it may be OK for Web browsers and vector editors, but not for librsvg. + if options.renderer_workaround: + if ((w.units != Unit.NONE and w.units != Unit.PX) or + (h.units != Unit.NONE and h.units != Unit.PX)): + return + + # else we have a statically sized image and we should try to remedy that + + # parse viewBox attribute + vbSep = re.split("\\s*\\,?\\s*", docElement.getAttribute('viewBox'), 3) + # if we have a valid viewBox we need to check it + vbWidth,vbHeight = 0,0 + if len(vbSep) == 4: + try: + # if x or y are specified and non-zero then it is not ok to overwrite it + vbX = float(vbSep[0]) + vbY = float(vbSep[1]) + if vbX != 0 or vbY != 0: return - # else we have a statically sized image and we should try to remedy that + # if width or height are not equal to doc width/height then it is not ok to overwrite it + vbWidth = float(vbSep[2]) + vbHeight = float(vbSep[3]) + if vbWidth != w.value or vbHeight != h.value: + return + # if the viewBox did not parse properly it is invalid and ok to overwrite it + except ValueError: + pass - # parse viewBox attribute - vbSep = RE_COMMA_WSP.split(docElement.getAttribute('viewBox')) - # if we have a valid viewBox we need to check it - if len(vbSep) == 4: - try: - # if x or y are specified and non-zero then it is not ok to overwrite it - vbX = float(vbSep[0]) - vbY = float(vbSep[1]) - if vbX != 0 or vbY != 0: - return + # at this point it's safe to set the viewBox and remove width/height + docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) + docElement.removeAttribute('width') + docElement.removeAttribute('height') - # if width or height are not equal to doc width/height then it is not ok to overwrite it - vbWidth = float(vbSep[2]) - vbHeight = float(vbSep[3]) - if vbWidth != w.value or vbHeight != h.value: - return - # if the viewBox did not parse properly it is invalid and ok to overwrite it - except ValueError: - pass - - # at this point it's safe to set the viewBox and remove width/height - docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) - docElement.removeAttribute('width') - docElement.removeAttribute('height') def remapNamespacePrefix(node, oldprefix, newprefix): - if node is None or node.nodeType != Node.ELEMENT_NODE: - return + if node == None or node.nodeType != 1: return - if node.prefix == oldprefix: - localName = node.localName - namespace = node.namespaceURI - doc = node.ownerDocument - parent = node.parentNode + if node.prefix == oldprefix: + localName = node.localName + namespace = node.namespaceURI + doc = node.ownerDocument + parent = node.parentNode - # create a replacement node - if newprefix != '': - newNode = doc.createElementNS(namespace, newprefix + ":" + localName) - else: - newNode = doc.createElement(localName) + # create a replacement node + newNode = None + if newprefix != '': + newNode = doc.createElementNS(namespace, newprefix+":"+localName) + else: + newNode = doc.createElement(localName); - # add all the attributes - attrList = node.attributes - for i in range(attrList.length): - attr = attrList.item(i) - newNode.setAttributeNS(attr.namespaceURI, attr.name, attr.nodeValue) + # add all the attributes + attrList = node.attributes + for i in xrange(attrList.length): + attr = attrList.item(i) + newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) - # clone and add all the child nodes - for child in node.childNodes: - newNode.appendChild(child.cloneNode(True)) + # clone and add all the child nodes + for child in node.childNodes: + newNode.appendChild(child.cloneNode(True)) - # replace old node with new node - parent.replaceChild(newNode, node) - # set the node to the new node in the remapped namespace prefix - node = newNode + # replace old node with new node + parent.replaceChild( newNode, node ) + # set the node to the new node in the remapped namespace prefix + node = newNode - # now do all child nodes - for child in node.childNodes: - remapNamespacePrefix(child, oldprefix, newprefix) + # now do all child nodes + for child in node.childNodes : + remapNamespacePrefix(child, oldprefix, newprefix) -def make_well_formed(text, quote_dict=None): - if quote_dict is None: - quote_dict = XML_ENTS_NO_QUOTES - if not any(c in text for c in quote_dict): - # The quote-able characters are quite rare in SVG (they mostly only - # occur in text elements in practice). Therefore it make sense to - # optimize for this common case - return text - return ''.join(quote_dict[c] if c in quote_dict else c for c in text) +def makeWellFormed(str): + xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} -def choose_quote_character(value): - quot_count = value.count('"') - if quot_count == 0 or quot_count <= value.count("'"): - # Fewest "-symbols (if there are 0, we pick this to avoid spending - # time counting the '-symbols as it won't matter) - quote = '"' - xml_ent = XML_ENTS_ESCAPE_QUOT - else: - quote = "'" - xml_ent = XML_ENTS_ESCAPE_APOS - return quote, xml_ent +# starr = [] +# for c in str: +# if c in xml_ents: +# starr.append(xml_ents[c]) +# else: +# starr.append(c) + # this list comprehension is short-form for the above for-loop: + return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) -TEXT_CONTENT_ELEMENTS = ['text', 'tspan', 'tref', 'textPath', 'altGlyph', - 'flowDiv', 'flowPara', 'flowSpan', 'flowTref', 'flowLine'] - - -KNOWN_ATTRS = [ - # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html - # (but should be kept intuitively ordered) - 'id', 'xml:id', 'class', - 'transform', - 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', - 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', - 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', - 'd', 'points', - ] + sorted(svgAttributes) + [ - 'style', - ] - -KNOWN_ATTRS_ORDER_BY_NAME = defaultdict(lambda: len(KNOWN_ATTRS), - {name: order for order, name in enumerate(KNOWN_ATTRS)}) - - -# use custom order for known attributes and alphabetical order for the rest -def _attribute_sort_key_function(attribute): - name = attribute.name - order_value = KNOWN_ATTRS_ORDER_BY_NAME[name] - return order_value, name - - -def attributes_ordered_for_output(element): - if not element.hasAttributes(): - return [] - attribute = element.attributes - # The .item(i) call is painfully slow (bpo#40689). Therefore we ensure we - # call it at most once per attribute. - # - it would be many times faster to use `attribute.values()` but sadly - # that is an "experimental" interface. - return sorted((attribute.item(i) for i in range(attribute.length)), - key=_attribute_sort_key_function) # hand-rolled serialization function that has the following benefits: # - pretty printing # - somewhat judicious use of whitespace # - ensure id attributes are first -def serializeXML(element, options, indent_depth=0, preserveWhitespace=False): - outParts = [] +def serializeXML(element, options, ind = 0, preserveWhitespace = False): + outParts = [] - indent_type = '' - newline = '' - if options.newlines: - if options.indent_type == 'tab': - indent_type = '\t' - elif options.indent_type == 'space': - indent_type = ' ' - indent_type *= options.indent_depth - newline = '\n' + indent = ind + I='' + if options.indent_type == 'tab': I='\t' + elif options.indent_type == 'space': I=' ' - outParts.extend([(indent_type * indent_depth), '<', element.nodeName]) + outParts.extend([(I * ind), '<', element.nodeName]) - # now serialize the other attributes - attrs = attributes_ordered_for_output(element) - for attr in attrs: - attrValue = attr.nodeValue - quote, xml_ent = choose_quote_character(attrValue) - attrValue = make_well_formed(attrValue, xml_ent) + # always serialize the id or xml:id attributes first + if element.getAttribute('id') != '': + id = element.getAttribute('id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' id=', quot, id, quot]) + if element.getAttribute('xml:id') != '': + id = element.getAttribute('xml:id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' xml:id=', quot, id, quot]) - if attr.nodeName == 'style': - # sort declarations - attrValue = ';'.join(sorted(attrValue.split(';'))) + # now serialize the other attributes + attrList = element.attributes + for num in xrange(attrList.length) : + attr = attrList.item(num) + if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue + # if the attribute value contains a double-quote, use single-quotes + quot = '"' + if attr.nodeValue.find('"') != -1: + quot = "'" - outParts.append(' ') - # preserve xmlns: if it is a namespace prefix declaration - if attr.prefix is not None: - outParts.extend([attr.prefix, ':']) - elif attr.namespaceURI is not None: - if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: - outParts.append('xmlns:') - elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': - outParts.append('xlink:') - outParts.extend([attr.localName, '=', quote, attrValue, quote]) + attrValue = makeWellFormed( attr.nodeValue ) - if attr.nodeName == 'xml:space': - if attrValue == 'preserve': - preserveWhitespace = True - elif attrValue == 'default': - preserveWhitespace = False + outParts.append(' ') + # preserve xmlns: if it is a namespace prefix declaration + if attr.prefix != None: + outParts.extend([attr.prefix, ':']) + elif attr.namespaceURI != None: + if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: + outParts.append('xmlns:') + elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': + outParts.append('xlink:') + outParts.extend([attr.localName, '=', quot, attrValue, quot]) - children = element.childNodes - if children.length == 0: - outParts.append('/>') - else: - outParts.append('>') + if attr.nodeName == 'xml:space': + if attrValue == 'preserve': + preserveWhitespace = True + elif attrValue == 'default': + preserveWhitespace = False - onNewLine = False - for child in element.childNodes: - # element node - if child.nodeType == Node.ELEMENT_NODE: - # do not indent inside text content elements as in SVG there's a difference between - # "text1\ntext2" and - # "text1\n text2" - # see https://www.w3.org/TR/SVG/text.html#WhiteSpace - if preserveWhitespace or element.nodeName in TEXT_CONTENT_ELEMENTS: - outParts.append(serializeXML(child, options, 0, preserveWhitespace)) - else: - outParts.extend([newline, serializeXML(child, options, indent_depth + 1, preserveWhitespace)]) - onNewLine = True - # text node - elif child.nodeType == Node.TEXT_NODE: - text_content = child.nodeValue - if not preserveWhitespace: - # strip / consolidate whitespace according to spec, see - # https://www.w3.org/TR/SVG/text.html#WhiteSpace - if element.nodeName in TEXT_CONTENT_ELEMENTS: - text_content = text_content.replace('\n', '') - text_content = text_content.replace('\t', ' ') - if child == element.firstChild: - text_content = text_content.lstrip() - elif child == element.lastChild: - text_content = text_content.rstrip() - while ' ' in text_content: - text_content = text_content.replace(' ', ' ') - else: - text_content = text_content.strip() - outParts.append(make_well_formed(text_content)) - # CDATA node - elif child.nodeType == Node.CDATA_SECTION_NODE: - outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) - # Comment node - elif child.nodeType == Node.COMMENT_NODE: - outParts.extend([newline, indent_type * (indent_depth+1), '<!--', child.nodeValue, '-->']) - # TODO: entities, processing instructions, what else? - else: # ignore the rest - pass + # if no children, self-close + children = element.childNodes + if children.length > 0: + outParts.append('>') - if onNewLine: - outParts.append(newline) - outParts.append(indent_type * indent_depth) - outParts.extend(['</', element.nodeName, '>']) + onNewLine = False + for child in element.childNodes: + # element node + if child.nodeType == 1: + if preserveWhitespace: + outParts.append(serializeXML(child, options, 0, preserveWhitespace)) + else: + outParts.extend(['\n', serializeXML(child, options, indent + 1, preserveWhitespace)]) + onNewLine = True + # text node + elif child.nodeType == 3: + # trim it only in the case of not being a child of an element + # where whitespace might be important + if preserveWhitespace: + outParts.append(makeWellFormed(child.nodeValue)) + else: + outParts.append(makeWellFormed(child.nodeValue.strip())) + # CDATA node + elif child.nodeType == 4: + outParts.extend(['<![CDATA[', child.nodeValue, ']]>']) + # Comment node + elif child.nodeType == 8: + outParts.extend(['<!--', child.nodeValue, '-->']) + # TODO: entities, processing instructions, what else? + else: # ignore the rest + pass + + if onNewLine: outParts.append(I * ind) + outParts.extend(['</', element.nodeName, '>']) + if indent > 0: outParts.append('\n') + else: + outParts.append('/>') + if indent > 0: outParts.append('\n') + + return "".join(outParts) - return "".join(outParts) # this is the main method # input is a string representation of the input XML # returns a string representation of the output XML -def scourString(in_string, options=None, stats=None): - # sanitize options (take missing attributes from defaults, discard unknown attributes) - options = sanitizeOptions(options) +def scourString(in_string, options=None): + if options is None: + options = _options_parser.get_default_values() + getcontext().prec = options.digits + global numAttrsRemoved + global numStylePropsFixed + global numElemsRemoved + global numBytesSavedInColors + global numCommentsRemoved + global numBytesSavedInIDs + global numBytesSavedInLengths + global numBytesSavedInTransforms + doc = xml.dom.minidom.parseString(in_string) - if stats is None: - # This is easier than doing "if stats is not None:" checks all over the place - stats = ScourStats() + # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements + # on the first pass, so we do it multiple times + # does it have to do with removal of children affecting the childlist? + if options.keep_editor_data == False: + while removeNamespacedElements( doc.documentElement, unwanted_ns ) > 0 : + pass + while removeNamespacedAttributes( doc.documentElement, unwanted_ns ) > 0 : + pass - # default or invalid value - if(options.cdigits < 0): - options.cdigits = options.digits + # remove the xmlns: declarations now + xmlnsDeclsToRemove = [] + attrList = doc.documentElement.attributes + for num in xrange(attrList.length) : + if attrList.item(num).nodeValue in unwanted_ns : + xmlnsDeclsToRemove.append(attrList.item(num).nodeName) - # create decimal contexts with reduced precision for scouring numbers - # calculations should be done in the default context (precision defaults to 28 significant digits) - # to minimize errors - global scouringContext - global scouringContextC # even more reduced precision for control points - scouringContext = Context(prec=options.digits) - scouringContextC = Context(prec=options.cdigits) + for attr in xmlnsDeclsToRemove : + doc.documentElement.removeAttribute(attr) + numAttrsRemoved += 1 - doc = xml.dom.minidom.parseString(in_string) + # ensure namespace for SVG is declared + # TODO: what if the default namespace is something else (i.e. some valid namespace)? + if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': + doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + # TODO: throw error or warning? - # determine number of flowRoot elements in input document - # flowRoot elements don't render at all on current browsers (04/2016) - cnt_flowText_el = len(doc.getElementsByTagName('flowRoot')) - if cnt_flowText_el: - errmsg = "SVG input document uses {} flow text elements, " \ - "which won't render on browsers!".format(cnt_flowText_el) - if options.error_on_flowtext: - raise Exception(errmsg) - else: - print("WARNING: {}".format(errmsg), file=sys.stderr) + # check for redundant SVG namespace declaration + attrList = doc.documentElement.attributes + xmlnsDeclsToRemove = [] + redundantPrefixes = [] + for i in xrange(attrList.length): + attr = attrList.item(i) + name = attr.nodeName + val = attr.nodeValue + if name[0:6] == 'xmlns:' and val == 'http://www.w3.org/2000/svg': + redundantPrefixes.append(name[6:]) + xmlnsDeclsToRemove.append(name) - # remove descriptive elements - stats.num_elements_removed += remove_descriptive_elements(doc, options) + for attrName in xmlnsDeclsToRemove: + doc.documentElement.removeAttribute(attrName) - # remove unneeded namespaced elements/attributes added by common editors - if options.keep_editor_data is False: - stats.num_elements_removed += removeNamespacedElements(doc.documentElement, - unwanted_ns) - stats.num_attributes_removed += removeNamespacedAttributes(doc.documentElement, - unwanted_ns) + for prefix in redundantPrefixes: + remapNamespacePrefix(doc.documentElement, prefix, '') - # remove the xmlns: declarations now - xmlnsDeclsToRemove = [] - attrList = doc.documentElement.attributes - for index in range(attrList.length): - if attrList.item(index).nodeValue in unwanted_ns: - xmlnsDeclsToRemove.append(attrList.item(index).nodeName) + if options.strip_comments: + numCommentsRemoved = removeComments(doc) - for attr in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attr) - stats.num_attributes_removed += len(xmlnsDeclsToRemove) + # repair style (remove unnecessary style properties and change them into XML attributes) + numStylePropsFixed = repairStyle(doc.documentElement, options) - # ensure namespace for SVG is declared - # TODO: what if the default namespace is something else (i.e. some valid namespace)? - if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': - doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - # TODO: throw error or warning? + # convert colors to #RRGGBB format + if options.simple_colors: + numBytesSavedInColors = convertColors(doc.documentElement) - # check for redundant and unused SVG namespace declarations - def xmlnsUnused(prefix, namespace): - if doc.getElementsByTagNameNS(namespace, "*"): - return False - else: - for element in doc.getElementsByTagName("*"): - for attribute in element.attributes.values(): - if attribute.name.startswith(prefix): - return False - return True + # remove <metadata> if the user wants to + if options.remove_metadata: + removeMetadataElements(doc) - attrList = doc.documentElement.attributes - xmlnsDeclsToRemove = [] - redundantPrefixes = [] - for i in range(attrList.length): - attr = attrList.item(i) - name = attr.nodeName - val = attr.nodeValue - if name[0:6] == 'xmlns:': - if val == 'http://www.w3.org/2000/svg': - redundantPrefixes.append(name[6:]) - xmlnsDeclsToRemove.append(name) - elif xmlnsUnused(name[6:], val): - xmlnsDeclsToRemove.append(name) + # remove unreferenced gradients/patterns outside of defs + # and most unreferenced elements inside of defs + while removeUnreferencedElements(doc, options.keep_defs) > 0: + pass - for attrName in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attrName) - stats.num_attributes_removed += len(xmlnsDeclsToRemove) - - for prefix in redundantPrefixes: - remapNamespacePrefix(doc.documentElement, prefix, '') - - if options.strip_comments: - remove_comments(doc, stats) - - if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): - doc.documentElement.removeAttribute('xml:space') - stats.num_attributes_removed += 1 - - # repair style (remove unnecessary style properties and change them into XML attributes) - stats.num_style_properties_fixed = repairStyle(doc.documentElement, options) - - # convert colors to #RRGGBB format - if options.simple_colors: - stats.num_bytes_saved_in_colors = convertColors(doc.documentElement) - - # remove unreferenced gradients/patterns outside of defs - # and most unreferenced elements inside of defs - while remove_unreferenced_elements(doc, options.keep_defs, stats) > 0: - pass - - # remove empty defs, metadata, g - # NOTE: these elements will be removed if they just have whitespace-only text nodes - for tag in ['defs', 'title', 'desc', 'metadata', 'g']: - for elem in doc.documentElement.getElementsByTagName(tag): - removeElem = not elem.hasChildNodes() - if removeElem is False: - for child in elem.childNodes: - if child.nodeType in [Node.ELEMENT_NODE, Node.CDATA_SECTION_NODE, Node.COMMENT_NODE]: - break - elif child.nodeType == Node.TEXT_NODE and not child.nodeValue.isspace(): - break - else: - removeElem = True - if removeElem: - elem.parentNode.removeChild(elem) - stats.num_elements_removed += 1 - - if options.strip_ids: - referencedIDs = findReferencedElements(doc.documentElement) - identifiedElements = unprotected_ids(doc, options) - stats.num_ids_removed += remove_unreferenced_ids(referencedIDs, - identifiedElements) - - while remove_duplicate_gradient_stops(doc, stats) > 0: - pass - - # remove gradients that are only referenced by one other gradient - while collapse_singly_referenced_gradients(doc, stats) > 0: - pass - - # remove duplicate gradients - stats.num_elements_removed += removeDuplicateGradients(doc) - - if options.group_collapse: - stats.num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) - # create <g> elements if there are runs of elements with the same attributes. - # this MUST be before moveCommonAttributesToParentGroup. - if options.group_create: - create_groups_for_common_attributes(doc.documentElement, stats) - - # move common attributes to parent group - # NOTE: the if the <svg> element's immediate children - # all have the same value for an attribute, it must not - # get moved to the <svg> element. The <svg> element - # doesn't accept fill=, stroke= etc.! - referencedIds = findReferencedElements(doc.documentElement) - for child in doc.documentElement.childNodes: - stats.num_attributes_removed += moveCommonAttributesToParentGroup(child, referencedIds) - - # remove unused attributes from parent - stats.num_attributes_removed += removeUnusedAttributesOnParent(doc.documentElement) - - # Collapse groups LAST, because we've created groups. If done before - # moveAttributesToParentGroup, empty <g>'s may remain. - if options.group_collapse: - while remove_nested_groups(doc.documentElement, stats) > 0: - pass - - # remove unnecessary closing point of polygons and scour points - for polygon in doc.documentElement.getElementsByTagName('polygon'): - stats.num_points_removed_from_polygon += clean_polygon(polygon, options) - - # scour points of polyline - for polyline in doc.documentElement.getElementsByTagName('polyline'): - cleanPolyline(polyline, options) - - # clean path data - for elem in doc.documentElement.getElementsByTagName('path'): - if elem.getAttribute('d') == '': + # remove empty defs, metadata, g + # NOTE: these elements will be removed if they just have whitespace-only text nodes + for tag in ['defs', 'metadata', 'g'] : + for elem in doc.documentElement.getElementsByTagName(tag) : + removeElem = not elem.hasChildNodes() + if removeElem == False : + for child in elem.childNodes : + if child.nodeType in [1, 4, 8]: + break + elif child.nodeType == 3 and not child.nodeValue.isspace(): + break + else: + removeElem = True + if removeElem : elem.parentNode.removeChild(elem) - else: - clean_path(elem, options, stats) + numElemsRemoved += 1 - # shorten ID names as much as possible - if options.shorten_ids: - stats.num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, options) + if options.strip_ids: + bContinueLooping = True + while bContinueLooping: + identifiedElements = unprotected_ids(doc, options) + referencedIDs = findReferencedElements(doc.documentElement) + bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) - # scour lengths (including coordinates) - for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', - 'linearGradient', 'radialGradient', 'stop', 'filter']: - for elem in doc.getElementsByTagName(type): - for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', - 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: - if elem.getAttribute(attr) != '': - elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) - viewBox = doc.documentElement.getAttribute('viewBox') - if viewBox: - lengths = RE_COMMA_WSP.split(viewBox) - lengths = [scourUnitlessLength(length) for length in lengths] - doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) + while removeDuplicateGradientStops(doc) > 0: + pass - # more length scouring in this function - stats.num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) + # remove gradients that are only referenced by one other gradient + while collapseSinglyReferencedGradients(doc) > 0: + pass - # remove default values of attributes - stats.num_attributes_removed += removeDefaultAttributeValues(doc.documentElement, options) + # remove duplicate gradients + while removeDuplicateGradients(doc) > 0: + pass - # reduce the length of transformation attributes - stats.num_bytes_saved_in_transforms = optimizeTransforms(doc.documentElement, options) + # create <g> elements if there are runs of elements with the same attributes. + # this MUST be before moveCommonAttributesToParentGroup. + if options.group_create: + createGroupsForCommonAttributes(doc.documentElement) - # convert rasters references to base64-encoded strings - if options.embed_rasters: - for elem in doc.documentElement.getElementsByTagName('image'): - stats.num_rasters_embedded += embed_rasters(elem, options) + # move common attributes to parent group + # NOTE: the if the <svg> element's immediate children + # all have the same value for an attribute, it must not + # get moved to the <svg> element. The <svg> element + # doesn't accept fill=, stroke= etc.! + referencedIds = findReferencedElements(doc.documentElement) + for child in doc.documentElement.childNodes: + numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) - # properly size the SVG document (ideally width/height should be 100% with a viewBox) - if options.enable_viewboxing: - properlySizeDoc(doc.documentElement, options) + # remove unused attributes from parent + numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) - # output the document as a pretty string with a single space for indent - # NOTE: removed pretty printing because of this problem: - # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ - # rolled our own serialize function here to save on space, put id first, customize indentation, etc + # Collapse groups LAST, because we've created groups. If done before + # moveAttributesToParentGroup, empty <g>'s may remain. + if options.group_collapse: + while removeNestedGroups(doc.documentElement) > 0: + pass + + # remove unnecessary closing point of polygons and scour points + for polygon in doc.documentElement.getElementsByTagName('polygon') : + cleanPolygon(polygon, options) + + # scour points of polyline + for polyline in doc.documentElement.getElementsByTagName('polyline') : + cleanPolyline(polyline, options) + + # clean path data + for elem in doc.documentElement.getElementsByTagName('path') : + if elem.getAttribute('d') == '': + elem.parentNode.removeChild(elem) + else: + cleanPath(elem, options) + + # shorten ID names as much as possible + if options.shorten_ids: + numBytesSavedInIDs += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) + + # scour lengths (including coordinates) + for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: + for elem in doc.getElementsByTagName(type): + for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', + 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: + if elem.getAttribute(attr) != '': + elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) + + # more length scouring in this function + numBytesSavedInLengths = reducePrecision(doc.documentElement) + + # remove default values of attributes + numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) + + # reduce the length of transformation attributes + numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) + + # convert rasters references to base64-encoded strings + if options.embed_rasters: + for elem in doc.documentElement.getElementsByTagName('image') : + embedRasters(elem, options) + + # properly size the SVG document (ideally width/height should be 100% with a viewBox) + if options.enable_viewboxing: + properlySizeDoc(doc.documentElement, options) + + # output the document as a pretty string with a single space for indent + # NOTE: removed pretty printing because of this problem: + # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ + # rolled our own serialize function here to save on space, put id first, customize indentation, etc # out_string = doc.documentElement.toprettyxml(' ') - out_string = serializeXML(doc.documentElement, options) + '\n' + out_string = serializeXML(doc.documentElement, options) + '\n' - # return the string with its XML prolog and surrounding comments - if options.strip_xml_prolog is False: - total_output = '<?xml version="1.0" encoding="UTF-8"' - if doc.standalone: - total_output += ' standalone="yes"' - total_output += '?>\n' - else: - total_output = "" + # now strip out empty lines + lines = [] + # Get rid of empty lines + for line in out_string.splitlines(True): + if line.strip(): + lines.append(line) - for child in doc.childNodes: - if child.nodeType == Node.ELEMENT_NODE: - total_output += out_string - else: # doctypes, entities, comments - total_output += child.toxml() + '\n' + # return the string with its XML prolog and surrounding comments + if options.strip_xml_prolog == False: + total_output = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + else: + total_output = "" + + for child in doc.childNodes: + if child.nodeType == 1: + total_output += "".join(lines) + else: # doctypes, entities, comments + total_output += child.toxml() + '\n' + + return total_output - return total_output # used mostly by unit tests # input is a filename # returns the minidom doc representation of the SVG -def scourXmlFile(filename, options=None, stats=None): - # sanitize options (take missing attributes from defaults, discard unknown attributes) - options = sanitizeOptions(options) - # we need to make sure infilename is set correctly (otherwise relative references in the SVG won't work) - options.ensure_value("infilename", filename) +def scourXmlFile(filename, options=None): + in_string = open(filename).read() + out_string = scourString(in_string, options) + return xml.dom.minidom.parseString(out_string.encode('utf-8')) - # open the file and scour it - with open(filename, "rb") as f: - in_string = f.read() - out_string = scourString(in_string, options, stats=stats) - - # prepare the output xml.dom.minidom object - doc = xml.dom.minidom.parseString(out_string.encode('utf-8')) - - # since minidom does not seem to parse DTDs properly - # manually declare all attributes with name "id" to be of type ID - # (otherwise things like doc.getElementById() won't work) - all_nodes = doc.getElementsByTagName("*") - for node in all_nodes: - try: - node.setIdAttribute('id') - except NotFoundErr: - pass - - return doc # GZ: Seems most other commandline tools don't do this, is it really wanted? class HeaderedFormatter(optparse.IndentedHelpFormatter): - """ - Show application name, version number, and copyright statement - above usage information. - """ + """ + Show application name, version number, and copyright statement + above usage information. + """ + def format_usage(self, usage): + return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, + optparse.IndentedHelpFormatter.format_usage(self, usage)) - def format_usage(self, usage): - return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, - optparse.IndentedHelpFormatter.format_usage(self, usage)) # GZ: would prefer this to be in a function or class scope, but tests etc need # access to the defaults anyway _options_parser = optparse.OptionParser( - usage="%prog [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]", - description=("If the input/output files are not specified, stdin/stdout are used. " - "If the input/output files are specified with a svgz extension, " - "then compressed SVG is assumed."), - formatter=HeaderedFormatter(max_help_position=33), - version=VER) + usage="%prog [-i input.svg] [-o output.svg] [OPTIONS]", + description=("If the input/output files are specified with a svgz" + " extension, then compressed SVG is assumed. If the input file is not" + " specified, stdin is used. If the output file is not specified, " + " stdout is used."), + formatter=HeaderedFormatter(max_help_position=30), + version=VER) -# legacy options (kept around for backwards compatibility, should not be used in new code) -_options_parser.add_option("-p", action="store", type=int, dest="digits", help=optparse.SUPPRESS_HELP) +_options_parser.add_option("--disable-simplify-colors", + action="store_false", dest="simple_colors", default=True, + help="won't convert all colors to #RRGGBB format") +_options_parser.add_option("--disable-style-to-xml", + action="store_false", dest="style_to_xml", default=True, + help="won't convert styles into XML attributes") +_options_parser.add_option("--disable-group-collapsing", + action="store_false", dest="group_collapse", default=True, + help="won't collapse <g> elements") +_options_parser.add_option("--create-groups", + action="store_true", dest="group_create", default=False, + help="create <g> elements for runs of elements with identical attributes") +_options_parser.add_option("--enable-id-stripping", + action="store_true", dest="strip_ids", default=False, + help="remove all un-referenced ID attributes") +_options_parser.add_option("--enable-comment-stripping", + action="store_true", dest="strip_comments", default=False, + help="remove all <!-- --> comments") +_options_parser.add_option("--shorten-ids", + action="store_true", dest="shorten_ids", default=False, + help="shorten all ID attributes to the least number of letters possible") +_options_parser.add_option("--shorten-ids-prefix", + action="store", type="string", dest="shorten_ids_prefix", default="", + help="shorten all ID attributes with a custom prefix") +_options_parser.add_option("--disable-embed-rasters", + action="store_false", dest="embed_rasters", default=True, + help="won't embed rasters as base64-encoded data") +_options_parser.add_option("--keep-unreferenced-defs", + action="store_true", dest="keep_defs", default=False, + help="won't remove elements within the defs container that are unreferenced") +_options_parser.add_option("--keep-editor-data", + action="store_true", dest="keep_editor_data", default=False, + help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes") +_options_parser.add_option("--remove-metadata", + action="store_true", dest="remove_metadata", default=False, + help="remove <metadata> elements (which may contain license metadata etc.)") +_options_parser.add_option("--renderer-workaround", + action="store_true", dest="renderer_workaround", default=True, + help="work around various renderer bugs (currently only librsvg) (default)") +_options_parser.add_option("--no-renderer-workaround", + action="store_false", dest="renderer_workaround", default=True, + help="do not work around various renderer bugs (currently only librsvg)") +_options_parser.add_option("--strip-xml-prolog", + action="store_true", dest="strip_xml_prolog", default=False, + help="won't output the <?xml ?> prolog") +_options_parser.add_option("--enable-viewboxing", + action="store_true", dest="enable_viewboxing", default=False, + help="changes document width/height to 100%/100% and creates viewbox coordinates") -# general options -_options_parser.add_option("-q", "--quiet", - action="store_true", dest="quiet", default=False, - help="suppress non-error output") -_options_parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", default=False, - help="verbose output (statistics, etc.)") +# GZ: this is confusing, most people will be thinking in terms of +# decimal places, which is not what decimal precision is doing +_options_parser.add_option("-p", "--set-precision", + action="store", type=int, dest="digits", default=5, + help="set number of significant digits (default: %default)") _options_parser.add_option("-i", - action="store", dest="infilename", metavar="INPUT.SVG", - help="alternative way to specify input filename") + action="store", dest="infilename", help=optparse.SUPPRESS_HELP) _options_parser.add_option("-o", - action="store", dest="outfilename", metavar="OUTPUT.SVG", - help="alternative way to specify output filename") + action="store", dest="outfilename", help=optparse.SUPPRESS_HELP) +_options_parser.add_option("-q", "--quiet", + action="store_true", dest="quiet", default=False, + help="suppress non-error output") +_options_parser.add_option("--indent", + action="store", type="string", dest="indent_type", default="space", + help="indentation of the output: none, space, tab (default: %default)") +_options_parser.add_option("--protect-ids-noninkscape", + action="store_true", dest="protect_ids_noninkscape", default=False, + help="Don't change IDs not ending with a digit") +_options_parser.add_option("--protect-ids-list", + action="store", type="string", dest="protect_ids_list", default=None, + help="Don't change IDs given in a comma-separated list") +_options_parser.add_option("--protect-ids-prefix", + action="store", type="string", dest="protect_ids_prefix", default=None, + help="Don't change IDs starting with the given prefix") -_option_group_optimization = optparse.OptionGroup(_options_parser, "Optimization") -_option_group_optimization.add_option("--set-precision", - action="store", type=int, dest="digits", default=5, metavar="NUM", - help="set number of significant digits (default: %default)") -_option_group_optimization.add_option("--set-c-precision", - action="store", type=int, dest="cdigits", default=-1, metavar="NUM", - help="set number of significant digits for control points " - "(default: same as '--set-precision')") -_option_group_optimization.add_option("--disable-simplify-colors", - action="store_false", dest="simple_colors", default=True, - help="won't convert colors to #RRGGBB format") -_option_group_optimization.add_option("--disable-style-to-xml", - action="store_false", dest="style_to_xml", default=True, - help="won't convert styles into XML attributes") -_option_group_optimization.add_option("--disable-group-collapsing", - action="store_false", dest="group_collapse", default=True, - help="won't collapse <g> elements") -_option_group_optimization.add_option("--create-groups", - action="store_true", dest="group_create", default=False, - help="create <g> elements for runs of elements with identical attributes") -_option_group_optimization.add_option("--keep-editor-data", - action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi, Adobe Illustrator " - "or Sketch elements and attributes") -_option_group_optimization.add_option("--nonsci-output", - action="store_true", dest="nonsci_output", default=False, - help="Remove scientific notation from path data") -_option_group_optimization.add_option("--keep-unreferenced-defs", - action="store_true", dest="keep_defs", default=False, - help="won't remove elements within the defs container that are unreferenced") -_option_group_optimization.add_option("--renderer-workaround", - action="store_true", dest="renderer_workaround", default=True, - help="work around various renderer bugs (currently only librsvg) (default)") -_option_group_optimization.add_option("--no-renderer-workaround", - action="store_false", dest="renderer_workaround", default=True, - help="do not work around various renderer bugs (currently only librsvg)") -_options_parser.add_option_group(_option_group_optimization) - -_option_group_document = optparse.OptionGroup(_options_parser, "SVG document") -_option_group_document.add_option("--strip-xml-prolog", - action="store_true", dest="strip_xml_prolog", default=False, - help="won't output the XML prolog (<?xml ?>)") -_option_group_document.add_option("--remove-titles", - action="store_true", dest="remove_titles", default=False, - help="remove <title> elements") -_option_group_document.add_option("--remove-descriptions", - action="store_true", dest="remove_descriptions", default=False, - help="remove <desc> elements") -_option_group_document.add_option("--remove-metadata", - action="store_true", dest="remove_metadata", default=False, - help="remove <metadata> elements " - "(which may contain license/author information etc.)") -_option_group_document.add_option("--remove-descriptive-elements", - action="store_true", dest="remove_descriptive_elements", default=False, - help="remove <title>, <desc> and <metadata> elements") -_option_group_document.add_option("--enable-comment-stripping", - action="store_true", dest="strip_comments", default=False, - help="remove all comments (<!-- -->)") -_option_group_document.add_option("--disable-embed-rasters", - action="store_false", dest="embed_rasters", default=True, - help="won't embed rasters as base64-encoded data") -_option_group_document.add_option("--enable-viewboxing", - action="store_true", dest="enable_viewboxing", default=False, - help="changes document width/height to 100%/100% and creates viewbox coordinates") -_options_parser.add_option_group(_option_group_document) - -_option_group_formatting = optparse.OptionGroup(_options_parser, "Output formatting") -_option_group_formatting.add_option("--indent", - action="store", type="string", dest="indent_type", default="space", metavar="TYPE", - help="indentation of the output: none, space, tab (default: %default)") -_option_group_formatting.add_option("--nindent", - action="store", type=int, dest="indent_depth", default=1, metavar="NUM", - help="depth of the indentation, i.e. number of spaces/tabs: (default: %default)") -_option_group_formatting.add_option("--no-line-breaks", - action="store_false", dest="newlines", default=True, - help="do not create line breaks in output" - "(also disables indentation; might be overridden by xml:space=\"preserve\")") -_option_group_formatting.add_option("--strip-xml-space", - action="store_true", dest="strip_xml_space_attribute", default=False, - help="strip the xml:space=\"preserve\" attribute from the root SVG element") -_options_parser.add_option_group(_option_group_formatting) - -_option_group_ids = optparse.OptionGroup(_options_parser, "ID attributes") -_option_group_ids.add_option("--enable-id-stripping", - action="store_true", dest="strip_ids", default=False, - help="remove all unreferenced IDs") -_option_group_ids.add_option("--shorten-ids", - action="store_true", dest="shorten_ids", default=False, - help="shorten all IDs to the least number of letters possible") -_option_group_ids.add_option("--shorten-ids-prefix", - action="store", type="string", dest="shorten_ids_prefix", default="", metavar="PREFIX", - help="add custom prefix to shortened IDs") -_option_group_ids.add_option("--protect-ids-noninkscape", - action="store_true", dest="protect_ids_noninkscape", default=False, - help="don't remove IDs not ending with a digit") -_option_group_ids.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", metavar="LIST", - help="don't remove IDs given in this comma-separated list") -_option_group_ids.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", metavar="PREFIX", - help="don't remove IDs starting with the given prefix") -_options_parser.add_option_group(_option_group_ids) - -_option_group_compatibility = optparse.OptionGroup(_options_parser, "SVG compatibility checks") -_option_group_compatibility.add_option("--error-on-flowtext", - action="store_true", dest="error_on_flowtext", default=False, - help="exit with error if the input SVG uses non-standard flowing text " - "(only warn by default)") -_options_parser.add_option_group(_option_group_compatibility) - - -def parse_args(args=None, ignore_additional_args=False): - options, rargs = _options_parser.parse_args(args) - - if rargs: - if not options.infilename: - options.infilename = rargs.pop(0) - if not options.outfilename and rargs: - options.outfilename = rargs.pop(0) - if not ignore_additional_args and rargs: - _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) - if options.digits < 1: - _options_parser.error("Number of significant digits has to be larger than zero, see --help") - if options.cdigits > options.digits: - options.cdigits = -1 - print("WARNING: The value for '--set-c-precision' should be lower than the value for '--set-precision'. " - "Number of significant digits for control points reset to default value, see --help", file=sys.stderr) - if options.indent_type not in ['tab', 'space', 'none']: - _options_parser.error("Invalid value for --indent, see --help") - if options.indent_depth < 0: - _options_parser.error("Value for --nindent should be positive (or zero), see --help") - if options.infilename and options.outfilename and options.infilename == options.outfilename: - _options_parser.error("Input filename is the same as output filename") - - return options - - -# this function was replaced by 'sanitizeOptions()' and is only kept for backwards compatibility -# TODO: delete this at some point or continue to keep it around? -def generateDefaultOptions(): - return sanitizeOptions() - - -# sanitizes options by updating attributes in a set of defaults options while discarding unknown attributes -def sanitizeOptions(options=None): - optionsDict = dict((key, getattr(options, key)) for key in dir(options) if not key.startswith('__')) - - sanitizedOptions = _options_parser.get_default_values() - sanitizedOptions._update_careful(optionsDict) - - return sanitizedOptions def maybe_gziped_file(filename, mode="r"): - if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): - import gzip - return gzip.GzipFile(filename, mode) - return open(filename, mode) + if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): + import gzip + return gzip.GzipFile(filename, mode) + return file(filename, mode) -def getInOut(options): - if options.infilename: - infile = maybe_gziped_file(options.infilename, "rb") - # GZ: could catch a raised IOError here and report - else: - # GZ: could sniff for gzip compression here - # - # open the binary buffer of stdin and let XML parser handle decoding - try: - infile = sys.stdin.buffer - except AttributeError: - infile = sys.stdin - # the user probably does not want to manually enter SVG code into the terminal... - if sys.stdin.isatty(): - _options_parser.error("No input file specified, see --help for detailed usage information") - if options.outfilename: - outfile = maybe_gziped_file(options.outfilename, "wb") - else: - # open the binary buffer of stdout as the output is already encoded - try: - outfile = sys.stdout.buffer - except AttributeError: - outfile = sys.stdout - # redirect informational output to stderr when SVG is output to stdout - options.stdout = sys.stderr +def parse_args(args=None, ignore_additional_args=False): + options, rargs = _options_parser.parse_args(args) - return [infile, outfile] + if rargs and not ignore_additional_args: + _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) + if options.digits < 0: + _options_parser.error("Can't have negative significant digits, see --help") + if not options.indent_type in ["tab", "space", "none"]: + _options_parser.error("Invalid value for --indent, see --help") + if options.infilename and options.outfilename and options.infilename == options.outfilename: + _options_parser.error("Input filename is the same as output filename") + + if options.infilename: + infile = maybe_gziped_file(options.infilename) + # GZ: could catch a raised IOError here and report + else: + # GZ: could sniff for gzip compression here + infile = sys.stdin + if options.outfilename: + outfile = maybe_gziped_file(options.outfilename, "wb") + else: + outfile = sys.stdout + + return options, [infile, outfile] -def generate_report(stats): - return ( - ' Number of elements removed: ' + str(stats.num_elements_removed) + os.linesep + - ' Number of attributes removed: ' + str(stats.num_attributes_removed) + os.linesep + - ' Number of unreferenced IDs removed: ' + str(stats.num_ids_removed) + os.linesep + - ' Number of comments removed: ' + str(stats.num_comments_removed) + os.linesep + - ' Number of style properties fixed: ' + str(stats.num_style_properties_fixed) + os.linesep + - ' Number of raster images embedded: ' + str(stats.num_rasters_embedded) + os.linesep + - ' Number of path segments reduced/removed: ' + str(stats.num_path_segments_removed) + os.linesep + - ' Number of points removed from polygons: ' + str(stats.num_points_removed_from_polygon) + os.linesep + - ' Number of bytes saved in path data: ' + str(stats.num_bytes_saved_in_path_data) + os.linesep + - ' Number of bytes saved in colors: ' + str(stats.num_bytes_saved_in_colors) + os.linesep + - ' Number of bytes saved in comments: ' + str(stats.num_bytes_saved_in_comments) + os.linesep + - ' Number of bytes saved in IDs: ' + str(stats.num_bytes_saved_in_ids) + os.linesep + - ' Number of bytes saved in lengths: ' + str(stats.num_bytes_saved_in_lengths) + os.linesep + - ' Number of bytes saved in transformations: ' + str(stats.num_bytes_saved_in_transforms) - ) + +def getReport(): + return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ + ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ + ' Number of unreferenced id attributes removed: ' + str(numIDsRemoved) + os.linesep + \ + ' Number of style properties fixed: ' + str(numStylePropsFixed) + os.linesep + \ + ' Number of raster images embedded inline: ' + str(numRastersEmbedded) + os.linesep + \ + ' Number of path segments reduced/removed: ' + str(numPathSegmentsReduced) + os.linesep + \ + ' Number of bytes saved in path data: ' + str(numBytesSavedInPathData) + os.linesep + \ + ' Number of bytes saved in colors: ' + str(numBytesSavedInColors) + os.linesep + \ + ' Number of points removed from polygons: ' + str(numPointsRemovedFromPolygon) + os.linesep + \ + ' Number of bytes saved in comments: ' + str(numCommentBytes) + os.linesep + \ + ' Number of bytes saved in id attributes: ' + str(numBytesSavedInIDs) + os.linesep + \ + ' Number of bytes saved in lengths: ' + str(numBytesSavedInLengths) + os.linesep + \ + ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) + + + +def generateDefaultOptions(): + ## FIXME: clean up this mess/hack and refactor arg parsing to argparse + class Struct: + def __init__(self, **entries): + self.__dict__.update(entries) + + d = parse_args(ignore_additional_args = True)[0].__dict__.copy() + + return Struct(**d) + def start(options, input, output): - # sanitize options (take missing attributes from defaults, discard unknown attributes) - options = sanitizeOptions(options) + if sys.platform == "win32": + from time import clock as get_tick + else: + # GZ: is this different from time.time() in any way? + def get_tick(): + return os.times()[0] - start = time.time() - stats = ScourStats() + start = get_tick() - # do the work - in_string = input.read() - out_string = scourString(in_string, options, stats=stats).encode("UTF-8") - output.write(out_string) + if not options.quiet: + print >>sys.stderr, "%s %s\n%s" % (APP, VER, COPYRIGHT) - # Close input and output files (but do not attempt to close stdin/stdout!) - if not ((input is sys.stdin) or (hasattr(sys.stdin, 'buffer') and input is sys.stdin.buffer)): - input.close() - if not ((output is sys.stdout) or (hasattr(sys.stdout, 'buffer') and output is sys.stdout.buffer)): - output.close() + # do the work + in_string = input.read() + out_string = scourString(in_string, options).encode("UTF-8") + output.write(out_string) - end = time.time() + # Close input and output files + input.close() + output.close() - # run-time in ms - duration = int(round((end - start) * 1000.)) + end = get_tick() - oldsize = len(in_string) - newsize = len(out_string) - sizediff = (newsize / oldsize) * 100. + # GZ: not using globals would be good too + if not options.quiet: + print >>sys.stderr, ' File:', input.name, \ + os.linesep + ' Time taken:', str(end-start) + 's' + os.linesep, \ + getReport() + + oldsize = len(in_string) + newsize = len(out_string) + sizediff = (newsize / oldsize) * 100 + print >>sys.stderr, ' Original file size:', oldsize, 'bytes;', \ + 'new file size:', newsize, 'bytes (' + str(sizediff)[:5] + '%)' - if not options.quiet: - print('Scour processed file "{}" in {} ms: {}/{} bytes new/orig -> {:.1f}%'.format( - input.name, - duration, - newsize, - oldsize, - sizediff), file=options.ensure_value("stdout", sys.stdout)) - if options.verbose: - print(generate_report(stats), file=options.ensure_value("stdout", sys.stdout)) def run(): - options = parse_args() - (input, output) = getInOut(options) - start(options, input, output) + options, (input, output) = parse_args() + start(options, input, output) + if __name__ == '__main__': - run() + run() diff --git a/scour/stats.py b/scour/stats.py deleted file mode 100644 index 2762b92..0000000 --- a/scour/stats.py +++ /dev/null @@ -1,28 +0,0 @@ -class ScourStats(object): - - __slots__ = ( - 'num_elements_removed', - 'num_attributes_removed', - 'num_style_properties_fixed', - 'num_bytes_saved_in_colors', - 'num_ids_removed', - 'num_comments_removed', - 'num_style_properties_fixed', - 'num_rasters_embedded', - 'num_path_segments_removed', - 'num_points_removed_from_polygon', - 'num_bytes_saved_in_path_data', - 'num_bytes_saved_in_colors', - 'num_bytes_saved_in_comments', - 'num_bytes_saved_in_ids', - 'num_bytes_saved_in_lengths', - 'num_bytes_saved_in_transforms', - ) - - def __init__(self): - self.reset() - - def reset(self): - # Set all stats to 0 - for attr in self.__slots__: - setattr(self, attr, 0) diff --git a/scour/svg_regex.py b/scour/svg_regex.py index c62ba2a..ce83c7b 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -41,22 +41,15 @@ Out[4]: [('M', [(0.60509999999999997, 0.5)])] In [5]: svg_parser.parse('M 100-200') # Another edge case Out[5]: [('M', [(100.0, -200.0)])] """ -from __future__ import absolute_import import re -from decimal import Decimal, getcontext -from functools import partial +from decimal import * # Sentinel. - - class _EOF(object): - def __repr__(self): return 'EOF' - - EOF = _EOF() lexicon = [ @@ -76,7 +69,6 @@ class Lexer(object): http://www.gooli.org/blog/a-simple-lexer-in-python/ """ - def __init__(self, lexicon): self.lexicon = lexicon parts = [] @@ -99,7 +91,6 @@ class Lexer(object): break yield (EOF, None) - svg_lexer = Lexer(lexicon) @@ -154,148 +145,140 @@ class SVGPathParser(object): def parse(self, text): """ Parse a string of SVG <path> data. """ - gen = self.lexer.lex(text) - next_val_fn = partial(next, *(gen,)) - token = next_val_fn() - return self.rule_svg_path(next_val_fn, token) + next = self.lexer.lex(text).next + token = next() + return self.rule_svg_path(next, token) - def rule_svg_path(self, next_val_fn, token): + def rule_svg_path(self, next, token): commands = [] while token[0] is not EOF: if token[0] != 'command': raise SyntaxError("expecting a command; got %r" % (token,)) rule = self.command_dispatch[token[1]] - command_group, token = rule(next_val_fn, token) + command_group, token = rule(next, token) commands.append(command_group) return commands - def rule_closepath(self, next_val_fn, token): + def rule_closepath(self, next, token): command = token[1] - token = next_val_fn() + token = next() return (command, []), token - def rule_moveto_or_lineto(self, next_val_fn, token): + def rule_moveto_or_lineto(self, next, token): command = token[1] - token = next_val_fn() + token = next() coordinates = [] while token[0] in self.number_tokens: - pair, token = self.rule_coordinate_pair(next_val_fn, token) + pair, token = self.rule_coordinate_pair(next, token) coordinates.extend(pair) return (command, coordinates), token - def rule_orthogonal_lineto(self, next_val_fn, token): + def rule_orthogonal_lineto(self, next, token): command = token[1] - token = next_val_fn() + token = next() coordinates = [] while token[0] in self.number_tokens: - coord, token = self.rule_coordinate(next_val_fn, token) + coord, token = self.rule_coordinate(next, token) coordinates.append(coord) return (command, coordinates), token - def rule_curveto3(self, next_val_fn, token): + def rule_curveto3(self, next, token): command = token[1] - token = next_val_fn() + token = next() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next_val_fn, token) - pair2, token = self.rule_coordinate_pair(next_val_fn, token) - pair3, token = self.rule_coordinate_pair(next_val_fn, token) + pair1, token = self.rule_coordinate_pair(next, token) + pair2, token = self.rule_coordinate_pair(next, token) + pair3, token = self.rule_coordinate_pair(next, token) coordinates.extend(pair1) coordinates.extend(pair2) coordinates.extend(pair3) return (command, coordinates), token - def rule_curveto2(self, next_val_fn, token): + def rule_curveto2(self, next, token): command = token[1] - token = next_val_fn() + token = next() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next_val_fn, token) - pair2, token = self.rule_coordinate_pair(next_val_fn, token) + pair1, token = self.rule_coordinate_pair(next, token) + pair2, token = self.rule_coordinate_pair(next, token) coordinates.extend(pair1) coordinates.extend(pair2) return (command, coordinates), token - def rule_curveto1(self, next_val_fn, token): + def rule_curveto1(self, next, token): command = token[1] - token = next_val_fn() + token = next() coordinates = [] while token[0] in self.number_tokens: - pair1, token = self.rule_coordinate_pair(next_val_fn, token) + pair1, token = self.rule_coordinate_pair(next, token) coordinates.extend(pair1) return (command, coordinates), token - def rule_elliptical_arc(self, next_val_fn, token): + def rule_elliptical_arc(self, next, token): command = token[1] - token = next_val_fn() + token = next() arguments = [] while token[0] in self.number_tokens: rx = Decimal(token[1]) * 1 if rx < Decimal("0.0"): raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - token = next_val_fn() + token = next() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) ry = Decimal(token[1]) * 1 if ry < Decimal("0.0"): raise SyntaxError("expecting a nonnegative number; got %r" % (token,)) - token = next_val_fn() + token = next() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) axis_rotation = Decimal(token[1]) * 1 - token = next_val_fn() - if token[1][0] not in ('0', '1'): + token = next() + if token[1] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - large_arc_flag = Decimal(token[1][0]) * 1 + large_arc_flag = Decimal(token[1]) * 1 - if len(token[1]) > 1: - token = list(token) - token[1] = token[1][1:] - else: - token = next_val_fn() - if token[1][0] not in ('0', '1'): + token = next() + if token[1] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) - sweep_flag = Decimal(token[1][0]) * 1 + sweep_flag = Decimal(token[1]) * 1 - if len(token[1]) > 1: - token = list(token) - token[1] = token[1][1:] - else: - token = next_val_fn() + token = next() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = Decimal(token[1]) * 1 - token = next_val_fn() + token = next() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) y = Decimal(token[1]) * 1 - token = next_val_fn() + token = next() arguments.extend([rx, ry, axis_rotation, large_arc_flag, sweep_flag, x, y]) return (command, arguments), token - def rule_coordinate(self, next_val_fn, token): + def rule_coordinate(self, next, token): if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = getcontext().create_decimal(token[1]) - token = next_val_fn() + token = next() return x, token - def rule_coordinate_pair(self, next_val_fn, token): + + def rule_coordinate_pair(self, next, token): # Inline these since this rule is so common. if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = getcontext().create_decimal(token[1]) - token = next_val_fn() + token = next() if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) y = getcontext().create_decimal(token[1]) - token = next_val_fn() + token = next() return [x, y], token diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 83454b3..72fd06f 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -56,22 +56,15 @@ Multiple transformations are supported: In [12]: svg_transform_parser.parse('translate(30 -30) rotate(36)') Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])] """ -from __future__ import absolute_import import re -from decimal import Decimal -from functools import partial - -from six.moves import range +from decimal import * # Sentinel. class _EOF(object): - def __repr__(self): return 'EOF' - - EOF = _EOF() lexicon = [ @@ -93,7 +86,6 @@ class Lexer(object): http://www.gooli.org/blog/a-simple-lexer-in-python/ """ - def __init__(self, lexicon): self.lexicon = lexicon parts = [] @@ -116,7 +108,6 @@ class Lexer(object): break yield (EOF, None) - svg_lexer = Lexer(lexicon) @@ -154,90 +145,88 @@ class SVGTransformationParser(object): def parse(self, text): """ Parse a string of SVG transform="" data. """ - gen = self.lexer.lex(text) - next_val_fn = partial(next, *(gen,)) - + next = self.lexer.lex(text).next commands = [] - token = next_val_fn() + token = next() while token[0] is not EOF: - command, token = self.rule_svg_transform(next_val_fn, token) - commands.append(command) + command, token = self.rule_svg_transform(next, token) + commands.append(command) return commands - def rule_svg_transform(self, next_val_fn, token): + def rule_svg_transform(self, next, token): if token[0] != 'command': raise SyntaxError("expecting a transformation type; got %r" % (token,)) command = token[1] rule = self.command_dispatch[command] - token = next_val_fn() + token = next() if token[0] != 'coordstart': raise SyntaxError("expecting '('; got %r" % (token,)) - numbers, token = rule(next_val_fn, token) + numbers, token = rule(next, token) if token[0] != 'coordend': raise SyntaxError("expecting ')'; got %r" % (token,)) - token = next_val_fn() + token = next() return (command, numbers), token - def rule_1or2numbers(self, next_val_fn, token): + def rule_1or2numbers(self, next, token): numbers = [] # 1st number is mandatory - token = next_val_fn() - number, token = self.rule_number(next_val_fn, token) + token = next() + number, token = self.rule_number(next, token) numbers.append(number) # 2nd number is optional - number, token = self.rule_optional_number(next_val_fn, token) + number, token = self.rule_optional_number(next, token) if number is not None: numbers.append(number) return numbers, token - def rule_1number(self, next_val_fn, token): + def rule_1number(self, next, token): # this number is mandatory - token = next_val_fn() - number, token = self.rule_number(next_val_fn, token) + token = next() + number, token = self.rule_number(next, token) numbers = [number] return numbers, token - def rule_1or3numbers(self, next_val_fn, token): + def rule_1or3numbers(self, next, token): numbers = [] # 1st number is mandatory - token = next_val_fn() - number, token = self.rule_number(next_val_fn, token) + token = next() + number, token = self.rule_number(next, token) numbers.append(number) # 2nd number is optional - number, token = self.rule_optional_number(next_val_fn, token) + number, token = self.rule_optional_number(next, token) if number is not None: # but, if the 2nd number is provided, the 3rd is mandatory. # we can't have just 2. numbers.append(number) - number, token = self.rule_number(next_val_fn, token) + number, token = self.rule_number(next, token) numbers.append(number) return numbers, token - def rule_6numbers(self, next_val_fn, token): + def rule_6numbers(self, next, token): numbers = [] - token = next_val_fn() + token = next() # all numbers are mandatory - for i in range(6): - number, token = self.rule_number(next_val_fn, token) + for i in xrange(6): + number, token = self.rule_number(next, token) numbers.append(number) return numbers, token - def rule_number(self, next_val_fn, token): + def rule_number(self, next, token): if token[0] not in self.number_tokens: raise SyntaxError("expecting a number; got %r" % (token,)) x = Decimal(token[1]) * 1 - token = next_val_fn() + token = next() return x, token - def rule_optional_number(self, next_val_fn, token): + def rule_optional_number(self, next, token): if token[0] not in self.number_tokens: return None, token else: x = Decimal(token[1]) * 1 - token = next_val_fn() + token = next() return x, token diff --git a/scour/yocto_css.py b/scour/yocto_css.py index 0aaac5a..3efeeda 100644 --- a/scour/yocto_css.py +++ b/scour/yocto_css.py @@ -48,29 +48,25 @@ # | DASHMATCH | FUNCTION S* any* ')' # | '(' S* any* ')' | '[' S* any* ']' ] S*; - def parseCssString(str): - rules = [] - # first, split on } to get the rule chunks - chunks = str.split('}') - for chunk in chunks: - # second, split on { to get the selector and the list of properties - bits = chunk.split('{') - if len(bits) != 2: - continue - rule = {} - rule['selector'] = bits[0].strip() - # third, split on ; to get the property declarations - bites = bits[1].strip().split(';') - if len(bites) < 1: - continue - props = {} - for bite in bites: - # fourth, split on : to get the property name and value - nibbles = bite.strip().split(':') - if len(nibbles) != 2: - continue - props[nibbles[0].strip()] = nibbles[1].strip() - rule['properties'] = props - rules.append(rule) - return rules + rules = [] + # first, split on } to get the rule chunks + chunks = str.split('}') + for chunk in chunks: + # second, split on { to get the selector and the list of properties + bits = chunk.split('{') + if len(bits) != 2: continue + rule = {} + rule['selector'] = bits[0].strip() + # third, split on ; to get the property declarations + bites = bits[1].strip().split(';') + if len(bites) < 1: continue + props = {} + for bite in bites: + # fourth, split on : to get the property name and value + nibbles = bite.strip().split(':') + if len(nibbles) != 2: continue + props[nibbles[0].strip()] = nibbles[1].strip() + rule['properties'] = props + rules.append(rule) + return rules diff --git a/setup.py b/setup.py index 990b596..97d11c2 100644 --- a/setup.py +++ b/setup.py @@ -1,87 +1,64 @@ ############################################################################### -# -# Copyright (C) 2013-2014 Tavendo GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# +## +## Copyright (C) 2013-2014 Tavendo GmbH +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## ############################################################################### -import os -import re - -from setuptools import find_packages, setup +from setuptools import setup, find_packages LONGDESC = """ -Scour is an SVG optimizer/cleaner that reduces the size of scalable -vector graphics by optimizing structure and removing unnecessary data. - -It can be used to create streamlined vector graphics suitable for web -deployment, publishing/sharing or further processing. - -The goal of Scour is to output a file that renders identically at a -fraction of the size by removing a lot of redundant information created -by most SVG editors. Optimization options are typically lossless but can -be tweaked for more aggressive cleaning. +Scour is a SVG optimizer/sanitizer that can be used to produce SVGs for Web deployment. Website - http://www.codedread.com/scour/ (original website) - - https://github.com/scour-project/scour (today) + - https://github.com/oberstet/scour (today) Authors: - Jeff Schiller, Louis Simard (original authors) - Tobias Oberstein (maintainer) - - Patrick Storz (maintainer) """ -VERSIONFILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "scour", "__init__.py") -verstrline = open(VERSIONFILE, "rt").read() -VSRE = r"^__version__ = u['\"]([^'\"]*)['\"]" -mo = re.search(VSRE, verstrline, re.M) -if mo: - verstr = mo.group(1) -else: - raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) - - -setup( - name='scour', - version=verstr, - description='Scour SVG Optimizer', - # long_description = open("README.md").read(), - long_description=LONGDESC, - license='Apache License 2.0', - author='Jeff Schiller', - author_email='codedread@gmail.com', - url='https://github.com/scour-project/scour', - platforms=('Any'), - install_requires=['six>=1.9.0'], - packages=find_packages(), - zip_safe=True, - entry_points={ - 'console_scripts': [ - 'scour = scour.scour:run' - ]}, - classifiers=["License :: OSI Approved :: Apache Software License", - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Pre-processors", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Utilities"], - keywords='svg optimizer' +setup ( + name = 'scour', + version = '0.29', + description = 'Scour SVG Optimizer', +# long_description = open("README.md").read(), + long_description = LONGDESC, + license = 'Apache License 2.0', + author = 'Jeff Schiller', + author_email = 'codedread@gmail.com', + url = 'https://github.com/oberstet/scour', + platforms = ('Any'), + install_requires = [], + packages = find_packages(), + zip_safe = True, + entry_points = { + 'console_scripts': [ + 'scour = scour.scour:run' + ]}, + classifiers = ["License :: OSI Approved :: Apache Software License", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Pre-processors", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Utilities"], + keywords = 'svg optimizer' ) diff --git a/test_css.py b/test_css.py deleted file mode 100755 index d7fd3e2..0000000 --- a/test_css.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Test Harness for Scour -# -# Copyright 2010 Jeff Schiller -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import - -import unittest - -from scour.yocto_css import parseCssString - - -class Blank(unittest.TestCase): - - def runTest(self): - r = parseCssString('') - self.assertEqual(len(r), 0, 'Blank string returned non-empty list') - self.assertEqual(type(r), type([]), 'Blank string returned non list') - - -class ElementSelector(unittest.TestCase): - - def runTest(self): - r = parseCssString('foo {}') - self.assertEqual(len(r), 1, 'Element selector not returned') - self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual(len(r[0]['properties']), 0, 'Property list for foo not empty') - - -class ElementSelectorWithProperty(unittest.TestCase): - - def runTest(self): - r = parseCssString('foo { bar: baz}') - self.assertEqual(len(r), 1, 'Element selector not returned') - self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual(len(r[0]['properties']), 1, 'Property list for foo did not have 1') - self.assertEqual(r[0]['properties']['bar'], 'baz', 'Property bar did not have baz value') - - -if __name__ == '__main__': - unittest.main() diff --git a/test_scour.py b/test_scour.py deleted file mode 100755 index 549333f..0000000 --- a/test_scour.py +++ /dev/null @@ -1,2796 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Test Harness for Scour -# -# Copyright 2010 Jeff Schiller -# Copyright 2010 Louis Simard -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import print_function # use print() as a function in Python 2 (see PEP 3105) -from __future__ import absolute_import # use absolute imports by default in Python 2 (see PEP 328) - -import os -import sys -import unittest - -import six -from six.moves import map, range - -from scour.scour import (make_well_formed, parse_args, scourString, scourXmlFile, start, run, - XML_ENTS_ESCAPE_APOS, XML_ENTS_ESCAPE_QUOT) -from scour.svg_regex import svg_parser -from scour import __version__ - - -SVGNS = 'http://www.w3.org/2000/svg' - - -# I couldn't figure out how to get ElementTree to work with the following XPath -# "//*[namespace-uri()='http://example.com']" -# so I decided to use minidom and this helper function that performs a test on a given node -# and all its children -# func must return either True (if pass) or False (if fail) -def walkTree(elem, func): - if func(elem) is False: - return False - for child in elem.childNodes: - if walkTree(child, func) is False: - return False - return True - - -class ScourOptions: - pass - - -class EmptyOptions(unittest.TestCase): - - MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ - '<svg xmlns="http://www.w3.org/2000/svg"/>\n' - - def test_scourString(self): - options = ScourOptions - try: - scourString(self.MINIMAL_SVG, options) - fail = False - except Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling "scourString" with empty options object') - - def test_scourXmlFile(self): - options = ScourOptions - try: - scourXmlFile('unittests/minimal.svg', options) - fail = False - except Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling "scourXmlFile" with empty options object') - - def test_start(self): - options = ScourOptions - input = open('unittests/minimal.svg', 'rb') - output = open('testscour_temp.svg', 'wb') - - stdout_temp = sys.stdout - sys.stdout = None - try: - start(options, input, output) - fail = False - except Exception: - fail = True - sys.stdout = stdout_temp - - os.remove('testscour_temp.svg') - - self.assertEqual(fail, False, - 'Exception when calling "start" with empty options object') - - -class InvalidOptions(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.invalidOption = "invalid value" - try: - scourXmlFile('unittests/ids-to-strip.svg', options) - fail = False - except Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling Scour with invalid options') - - -class GetElementById(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids.svg') - self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') - self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') - self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') - self.assertIsNotNone(doc.getElementById('rect1'), 'rect not found by ID') - self.assertIsNone(doc.getElementById('rect2'), 'Non-existing element found by ID') - - -class NoInkscapeElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), - False, - 'Found Inkscape elements') - - -class NoSodipodiElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), - False, - 'Found Sodipodi elements') - - -class NoAdobeIllustratorElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), - False, - 'Found Adobe Illustrator elements') - - -class NoAdobeGraphsElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), - False, - 'Found Adobe Graphs elements') - - -class NoAdobeSVGViewerElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), - False, - 'Found Adobe SVG Viewer elements') - - -class NoAdobeVariablesElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), - False, - 'Found Adobe Variables elements') - - -class NoAdobeSaveForWebElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), - False, - 'Found Adobe Save For Web elements') - - -class NoAdobeExtensibilityElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), - False, - 'Found Adobe Extensibility elements') - - -class NoAdobeFlowsElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), - False, - 'Found Adobe Flows elements') - - -class NoAdobeImageReplacementElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), - False, - 'Found Adobe Image Replacement elements') - - -class NoAdobeCustomElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), - False, - 'Found Adobe Custom elements') - - -class NoAdobeXPathElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), - False, - 'Found Adobe XPath elements') - - -class DoNotRemoveTitleWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child') - - -class RemoveEmptyTitleElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element') - - -class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child') - - -class RemoveEmptyDescriptionElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element') - - -class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child') - - -class RemoveEmptyMetadataElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element') - - -class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child') - - -class RemoveEmptyDescriptiveElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element') - - -class RemoveEmptyGElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-g.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Did not remove empty g element') - - -class RemoveUnreferencedPattern(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-pattern.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern not removed') - - -class RemoveUnreferencedLinearGradient(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-linearGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Unreferenced linearGradient not removed') - - -class RemoveUnreferencedRadialGradient(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-radialGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, - 'Unreferenced radialGradient not removed') - - -class RemoveUnreferencedElementInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Unreferenced rect left in defs') - - -class RemoveUnreferencedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-defs.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, - 'Unreferenced radialGradient left in defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern left in defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 0, - 'Unreferenced circle left in defs') - - -class KeepUnreferencedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-defs.svg', - parse_args(['--keep-unreferenced-defs'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, - 'Unreferenced radialGradient removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 1, - 'Unreferenced pattern removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 1, - 'Unreferenced circle removed from defs with `--keep-unreferenced-defs`') - - -class DoNotRemoveChainedRefsInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/refs-in-defs.svg') - g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] - self.assertEqual(g.childNodes.length >= 2, True, - 'Chained references not honored in defs') - - -class KeepTitleInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Title removed from in defs') - - -class RemoveNestedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-defs.svg') - allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') - self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') - - -class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids-to-strip.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', - '<svg> ID stripped when it should be disabled') - - -class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids-to-strip.svg', - parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', - '<svg> ID not stripped') - - -class ProtectIDs(unittest.TestCase): - - def test_protect_none(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' not stripped when none of the '--protect-ids-_' options was specified") - - def test_protect_ids_noninkscape(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-noninkscape'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), 'text_custom', - "ID 'text_custom' should NOT have been stripped because of '--protect-ids-noninkscape'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' should have been stripped despite '--protect-ids-noninkscape' being specified") - - def test_protect_ids_list(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-list=text2,text3'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-list' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), 'text2', - "ID 'text2' should NOT have been stripped because of '--protect-ids-list'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), 'text3', - "ID 'text3' should NOT have been stripped because of '--protect-ids-list'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' should have been stripped despite '--protect-ids-list' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' should have been stripped despite '--protect-ids-list' being specified") - - def test_protect_ids_prefix(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-prefix=my'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), 'my_text1', - "ID 'my_text1' should NOT have been stripped because of '--protect-ids-prefix'") - - -class RemoveUselessNestedGroups(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-useless-groups.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Useless nested groups not removed') - - -class DoNotRemoveUselessNestedGroups(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-useless-groups.svg', - parse_args(['--disable-group-collapsing'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Useless nested groups were removed despite --disable-group-collapsing') - - -class DoNotRemoveNestedGroupsWithTitle(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with title was removed') - - -class DoNotRemoveNestedGroupsWithDesc(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with desc was removed') - - -class RemoveDuplicateLinearGradientStops(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops not removed') - - -class RemoveDuplicateLinearGradientStopsPct(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops with percentages not removed') - - -class RemoveDuplicateRadialGradientStops(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate radial gradient stops not removed') - - -class NoSodipodiNamespaceDecl(unittest.TestCase): - - def runTest(self): - attrs = scourXmlFile('unittests/sodipodi.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'Sodipodi namespace declaration found') - - -class NoInkscapeNamespaceDecl(unittest.TestCase): - - def runTest(self): - attrs = scourXmlFile('unittests/inkscape.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://www.inkscape.org/namespaces/inkscape', - 'Inkscape namespace declaration found') - - -class NoSodipodiAttributes(unittest.TestCase): - - def runTest(self): - def findSodipodiAttr(elem): - attrs = elem.attributes - if attrs is None: - return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - return False - return True - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), - False, - 'Found Sodipodi attributes') - - -class NoInkscapeAttributes(unittest.TestCase): - - def runTest(self): - def findInkscapeAttr(elem): - attrs = elem.attributes - if attrs is None: - return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': - return False - return True - self.assertNotEqual(walkTree(scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), - False, - 'Found Inkscape attributes') - - -class KeepInkscapeNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Inkscape namespace declaration when using --keep-editor-data") - return False - - -class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Sodipodi namespace declaration when using --keep-editor-data") - return False - - -class KeepReferencedFonts(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-font.svg') - fonts = doc.documentElement.getElementsByTagNameNS(SVGNS, 'font') - self.assertEqual(len(fonts), 1, - 'Font wrongly removed from <defs>') - - -class ConvertStyleToAttrs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', - 'style attribute not emptied') - - -class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when stroke opacity zero') - - -class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when stroke opacity zero') - - -class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when stroke opacity zero') - - -class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when stroke opacity zero') - - -class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when stroke opacity zero') - - -class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when stroke opacity zero') - - -class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when width zero') - - -class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when width zero') - - -class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when width zero') - - -class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when width zero') - - -class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when width zero') - - -class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when width zero') - - -class RemoveStrokeWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when no stroke') - - -class KeepStrokeWhenInheritedFromParent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', - 'stroke attribute removed despite a different value being inherited from a parent') - - -class KeepStrokeWhenInheritedByChild(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', - 'stroke attribute removed despite it being inherited by a child') - - -class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when no stroke') - - -class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', - 'stroke-width attribute removed despite it being inherited by a child') - - -class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when no stroke') - - -class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when no stroke') - - -class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when no stroke') - - -class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when no stroke') - - -class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when no stroke') - - -class RemoveFillRuleWhenFillNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', - 'fill-rule attribute not emptied when no fill') - - -class RemoveFillOpacityWhenFillNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', - 'fill-opacity attribute not emptied when no fill') - - -class ConvertFillPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg', - parse_args(['--disable-simplify-colors'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', - 'fill property not converted to XML attribute') - - -class ConvertFillOpacityPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', - 'fill-opacity property not converted to XML attribute') - - -class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', - 'fill-rule property not converted to XML attribute') - - -class CollapseSinglyReferencedGradients(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Singly-referenced linear gradient not collapsed') - - -class InheritGradientUnitsUponCollapsing(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), - 'userSpaceOnUse', - 'gradientUnits not properly inherited when collapsing gradients') - - -class OverrideGradientUnitsUponCollapsing(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', - 'gradientUnits not properly overrode when collapsing gradients') - - -class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/dont-collapse-gradients.svg') - self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Multiply-referenced linear gradient collapsed') - - -class PreserveXLinkHrefWhenCollapsingReferencedGradients(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients-preserve-xlink-href.svg') - g1 = doc.getElementById("g1") - g2 = doc.getElementById("g2") - g3 = doc.getElementById("g3") - self.assertTrue(g1, 'g1 is still present') - self.assertTrue(g2 is None, 'g2 was removed') - self.assertTrue(g3, 'g3 is still present') - self.assertEqual(g3.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#g1', - 'g3 has a xlink:href to g1') - - -class RemoveTrailingZerosFromPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[:4] == 'm300' and path[4] != '.', True, - 'Trailing zeros not removed from path data') - - -class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros-calc.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path, 'm5.81 0h0.1', - 'Trailing zeros not removed from path data after calculation') - - -class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[4], '-', - 'Delimiters not removed before negative coordinates in path data') - - -class UseScientificNotationToShortenCoordsInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-use-scientific-notation.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path, 'm1e4 0', - 'Not using scientific notation for path coord when representation is shorter') - - -class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-abs-to-rel.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[1][0], 'v', - 'Absolute V command not converted to relative v command') - self.assertEqual(float(path[1][1][0]), -20.0, - 'Absolute V value not converted to relative v value') - - -class RoundPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(float(path[0][1][0]), 100.0, - 'Not rounding down') - self.assertEqual(float(path[0][1][1]), 100.0, - 'Not rounding up') - - -class LimitPrecisionInPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(float(path[1][1][0]), 100.01, - 'Not correctly limiting precision on path data') - - -class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=1'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4e3 5e4 7e5", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4e3 -5e4 -7e5", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 6.5e5", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-6.5e5", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=4'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123.5 101-123.5-101", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - -class LimitPrecisionInControlPointPathData(unittest.TestCase): - - def runTest(self): - path_data = ("m1.1 2.2 3.3 4.4m-4.4-6.7" - "c1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7m-17-20" - "s1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4m-10-13" - "q1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4") - doc = scourXmlFile('unittests/path-precision-control-points.svg', - parse_args(['--set-precision=2', '--set-c-precision=1'])) - path_data2 = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path_data2, path_data, - 'Not correctly limiting precision on path data with --set-c-precision') - - -class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[4][0], 'z', - 'Did not remove an empty line segment from path') - - -class RemoveEmptySegmentsFromPathWithButtLineCaps(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) - for id in ['none', 'attr_butt', 'style_butt']: - path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) - self.assertEqual(len(path), 1, - 'Did not remove empty segments when path had butt linecaps') - - -class DoNotRemoveEmptySegmentsFromPathWithRoundSquareLineCaps(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) - for id in ['attr_round', 'attr_square', 'style_round', 'style_square']: - path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) - self.assertEqual(len(path), 2, - 'Did remove empty segments when path had round or square linecaps') - - -class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[1][0], 'h', - 'Did not change line to horizontal line segment in path') - self.assertEqual(float(path[1][1][0]), 200.0, - 'Did not calculate horizontal line segment in path correctly') - - -class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[2][0], 'v', - 'Did not change line to vertical line segment in path') - self.assertEqual(float(path[2][1][0]), 100.0, - 'Did not calculate vertical line segment in path correctly') - - -class ChangeBezierToShorthandInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-bez-optimize.svg') - self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', - 'Did not change bezier curves into shorthand curve segments in path') - self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', - 'Did not change bezier curve into shorthand curve segment when first control point ' - 'is the current point and previous command was not a bezier curve') - self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', - 'Did change bezier curve into shorthand curve segment when first control point ' - 'is the current point but previous command was a bezier curve with a different control point') - - -class ChangeQuadToShorthandInPath(unittest.TestCase): - - def runTest(self): - path = scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', - 'Did not change quadratic curves into shorthand curve segments in path') - - -class BooleanFlagsInEllipticalPath(unittest.TestCase): - - def test_omit_spaces(self): - doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--no-renderer-workaround'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths: - self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 00100 50', - 'Did not ommit spaces after boolean flags in elliptical arg path command') - - def test_output_spaces_with_renderer_workaround(self): - doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--renderer-workaround'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths: - self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 0 0 100 50', - 'Did not output spaces after boolean flags in elliptical arg path command ' - 'with renderer workaround') - - -class DoNotOptimzePathIfLarger(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertTrue(len(p.getAttribute('d')) <= - # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): - # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" - len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), - 'Made path data longer during optimization') - - -class HandleEncodingUTF8(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/encoding-utf8.svg') - text = u'Hello in many languages:\n' \ - u'ar: أهلا\n' \ - u'bn: হ্যালো\n' \ - u'el: Χαίρετε\n' \ - u'en: Hello\n' \ - u'hi: नमस्ते\n' \ - u'iw: שלום\n' \ - u'ja: こんにちは\n' \ - u'km: ជំរាបសួរ\n' \ - u'ml: ഹലോ\n' \ - u'ru: Здравствуйте\n' \ - u'ur: ہیلو\n' \ - u'zh: 您好' - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual(desc, text, - 'Did not handle international UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() - self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', - 'Did not handle common UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() - self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', - 'Did not handle mathematical UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() - self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', - 'Did not handle superscript/subscript UTF8 characters') - - -class HandleEncodingISO_8859_15(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/encoding-iso-8859-15.svg') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual(desc, u'áèîäöüß€ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters') - - -class HandleSciNoInPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-sn.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, - 'Did not handle scientific notation in path data') - - -class TranslateRGBIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(elem.getAttribute('fill'), '#0f1011', - 'Not converting rgb into hex') - - -class TranslateRGBPctIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] - self.assertEqual(elem.getAttribute('stop-color'), '#7f0000', - 'Not converting rgb pct into hex') - - -class TranslateColorNamesIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(elem.getAttribute('stroke'), '#a9a9a9', - 'Not converting standard color names into hex') - - -class TranslateExtendedColorNamesIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] - self.assertEqual(elem.getAttribute('solid-color'), '#fafad2', - 'Not converting extended color names into hex') - - -class TranslateLongHexColorIntoShortHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] - self.assertEqual(elem.getAttribute('fill'), '#fff', - 'Not converting long hex color into short hex') - - -class DoNotConvertShortColorNames(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/dont-convert-short-color-names.svg') \ - .getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual('red', elem.getAttribute('fill'), - 'Converted short color name to longer hex string') - - -class AllowQuotEntitiesInUrl(unittest.TestCase): - - def runTest(self): - grads = scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grads), 1, - 'Removed referenced gradient when " was in the url') - - -class RemoveFontStylesFromNonTextShapes(unittest.TestCase): - - def runTest(self): - r = scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(r.getAttribute('font-size'), '', - 'font-size not removed from rect') - - -class CollapseStraightPathSegments(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-straight-path-segments.svg', parse_args(['--disable-style-to-xml'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - path_data = [path.getAttribute('d') for path in paths] - path_data_expected = ['m0 0h30', - 'm0 0v30', - 'm0 0h10.5v10.5', - 'm0 0h10-1v10-1', - 'm0 0h30', - 'm0 0h30', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0 20 40v1l10 20', - 'm0 0 10 10-20-20 10 10-20-20', - 'm0 0 1 2m1 2 2 4m1 2 2 4', - 'm6.3228 7.1547 81.198 45.258'] - - self.assertEqual(path_data[0:3], path_data_expected[0:3], - 'Did not collapse h/v commands into a single h/v commands') - self.assertEqual(path_data[3], path_data_expected[3], - 'Collapsed h/v commands with different direction') - self.assertEqual(path_data[4:6], path_data_expected[4:6], - 'Did not collapse h/v commands with only start/end markers present') - self.assertEqual(path_data[6:10], path_data_expected[6:10], - 'Did not preserve h/v commands with intermediate markers present') - - self.assertEqual(path_data[10], path_data_expected[10], - 'Did not collapse lineto commands into a single (implicit) lineto command') - self.assertEqual(path_data[11], path_data_expected[11], - 'Collapsed lineto commands with different direction') - self.assertEqual(path_data[12], path_data_expected[12], - 'Collapsed first parameter pair of a moveto subpath') - self.assertEqual(path_data[13], path_data_expected[13], - 'Did not collapse the nodes of a straight real world path') - - -class ConvertStraightCurvesToLines(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10 40 40 40-40z', - 'Did not convert straight curves into lines') - - -class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - self.assertEqual(p.getAttribute('points'), '50 50 150 50 150 150 50 150', - 'Unnecessary polygon end point not removed') - - -class DoNotRemovePolgonLastPoint(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] - self.assertEqual(p.getAttribute('points'), '200 50 300 50 300 150 200 150', - 'Last point of polygon removed') - - -class ScourPolygonCoordsSciNo(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - self.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polygon coordinates not scoured') - - -class ScourPolylineCoordsSciNo(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polyline coordinates not scoured') - - -class ScourPolygonNegativeCoords(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - # points="100,-100,100-100,100-100-100,-100-100,200" /> - self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') - - -class ScourPolylineNegativeCoords(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') - - -class ScourPolygonNegativeCoordFirst(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - # points="-100,-100,100-100,100-100-100,-100-100,200" /> - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') - - -class ScourPolylineNegativeCoordFirst(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') - - -class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): - - def runTest(self): - f = scourXmlFile('unittests/important-groups-in-defs.svg') - self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Group in defs with id\'ed element removed') - - -class AlwaysKeepClosePathSegments(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', - 'Path with closepath not preserved') - - -class RemoveDuplicateLinearGradients(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(1, lingrads.length, - 'Duplicate linear gradient not removed') - - -class RereferenceForLinearGradient(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), - 'Reference not updated after removing duplicate linear gradient') - self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), - 'Reference not updated after removing duplicate linear gradient') - - -class RemoveDuplicateRadialGradients(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(1, radgrads.length, - 'Duplicate radial gradient not removed') - - -class RemoveDuplicateRadialGradientsEnsureMasterHasID(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients-master-without-id.svg') - lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') - rect = svgdoc.getElementById('r1') - self.assertEqual(1, lingrads.length, - 'Duplicate linearGradient not removed') - self.assertEqual(lingrads[0].getAttribute("id"), "g1", - "linearGradient has a proper ID") - self.assertNotEqual(rect.getAttribute("fill"), "url(#)", - "linearGradient has a proper ID") - - -class RereferenceForRadialGradient(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), - 'Reference not updated after removing duplicate radial gradient') - - -class RereferenceForGradientWithFallback(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[0].getAttribute('fill') + ' #fff', rects[5].getAttribute('fill'), - 'Reference (with fallback) not updated after removing duplicate linear gradient') - - -class CollapseSamePathPoints(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), "m100 100 100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", - 'Did not collapse same path points') - - -class ScourUnitlessLengths(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/scour-lengths.svg') - r = doc.getElementsByTagNameNS(SVGNS, 'rect')[0] - svg = doc.documentElement - self.assertEqual(svg.getAttribute('x'), '1', - 'Did not scour x attribute of svg element with unitless number') - self.assertEqual(r.getAttribute('x'), '123.46', - 'Did not scour x attribute of rect with unitless number') - self.assertEqual(r.getAttribute('y'), '123', - 'Did not scour y attribute of rect unitless number') - self.assertEqual(r.getAttribute('width'), '300', - 'Did not scour width attribute of rect with unitless number') - self.assertEqual(r.getAttribute('height'), '100', - 'Did not scour height attribute of rect with unitless number') - - -class ScourLengthsWithUnits(unittest.TestCase): - - def runTest(self): - r = scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1] - self.assertEqual(r.getAttribute('x'), '123.46px', - 'Did not scour x attribute with unit') - self.assertEqual(r.getAttribute('y'), '35ex', - 'Did not scour y attribute with unit') - self.assertEqual(r.getAttribute('width'), '300pt', - 'Did not scour width attribute with unit') - self.assertEqual(r.getAttribute('height'), '50%', - 'Did not scour height attribute with unit') - - -class RemoveRedundantSvgNamespaceDeclaration(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement - self.assertNotEqual(doc.getAttribute('xmlns:svg'), 'http://www.w3.org/2000/svg', - 'Redundant svg namespace declaration not removed') - - -class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement - r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] - self.assertEqual(r.tagName, 'rect', - 'Redundant svg: prefix not removed from rect') - t = doc.getElementsByTagNameNS(SVGNS, 'text')[0] - self.assertEqual(t.tagName, 'text', - 'Redundant svg: prefix not removed from text') - - # Regression test for #239 - self.assertEqual(t.getAttribute('xml:space'), 'preserve', - 'Required xml: prefix removed in error') - self.assertEqual(t.getAttribute("space"), '', - 'Required xml: prefix removed in error') - - -class RemoveDefaultGradX1Value(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual(g.getAttribute('x1'), '', - 'x1="0" not removed') - - -class RemoveDefaultGradY1Value(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual(g.getAttribute('y1'), '', - 'y1="0" not removed') - - -class RemoveDefaultGradX2Value(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/gradient-default-attrs.svg') - self.assertEqual(doc.getElementById('grad1').getAttribute('x2'), '', - 'x2="100%" not removed') - self.assertEqual(doc.getElementById('grad1b').getAttribute('x2'), '', - 'x2="1" not removed, ' - 'which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') - self.assertNotEqual(doc.getElementById('grad1c').getAttribute('x2'), '', - 'x2="1" removed, ' - 'which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') - - -class RemoveDefaultGradY2Value(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual(g.getAttribute('y2'), '', - 'y2="0" not removed') - - -class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual(g.getAttribute('gradientUnits'), '', - 'gradientUnits="objectBoundingBox" not removed') - - -class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual(g.getAttribute('spreadMethod'), '', - 'spreadMethod="pad" not removed') - - -class RemoveDefaultGradCXValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual(g.getAttribute('cx'), '', - 'cx="50%" not removed') - - -class RemoveDefaultGradCYValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual(g.getAttribute('cy'), '', - 'cy="50%" not removed') - - -class RemoveDefaultGradRValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual(g.getAttribute('r'), '', - 'r="50%" not removed') - - -class RemoveDefaultGradFXValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual(g.getAttribute('fx'), '', - 'fx matching cx not removed') - - -class RemoveDefaultGradFYValue(unittest.TestCase): - - def runTest(self): - g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual(g.getAttribute('fy'), '', - 'fy matching cy not removed') - - -class RemoveDefaultAttributeOrderSVGLengthCrash(unittest.TestCase): - - # Triggered a crash in v0.36 - def runTest(self): - try: - scourXmlFile('unittests/remove-default-attr-order.svg') - except AttributeError: - self.fail("Processing the order attribute triggered an AttributeError") - - -class RemoveDefaultAttributeStdDeviationSVGLengthCrash(unittest.TestCase): - - # Triggered a crash in v0.36 - def runTest(self): - try: - scourXmlFile('unittests/remove-default-attr-std-deviation.svg') - except AttributeError: - self.fail("Processing the order attribute triggered an AttributeError") - - -class CDATAInXml(unittest.TestCase): - - def runTest(self): - with open('unittests/cdata.svg') as f: - lines = scourString(f.read()).splitlines() - self.assertEqual(lines[3], - " alert('pb&j');", - 'CDATA did not come out correctly') - - -class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('unicode="<"') != -1, - "Improperly serialized < in attribute value") - - -class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('unicode="&"') != -1, - 'Improperly serialized & in attribute value') - - -class WellFormedXMLLesserThanInTextContent(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('<title>2 < 5') != -1, - 'Improperly serialized < in text content') - - -class WellFormedXMLAmpersandInTextContent(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('Peanut Butter & Jelly') != -1, - 'Improperly serialized & in text content') - - -class WellFormedXMLNamespacePrefixRemoveUnused(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:foo=') == -1, - 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') - - -class WellFormedXMLNamespacePrefixKeepUsedElementPrefix(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:bar=') != -1, - 'Improperly serialized namespace prefix declarations: Used element prefix removed') - - -class WellFormedXMLNamespacePrefixKeepUsedAttributePrefix(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:baz=') != -1, - 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') - - -class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scourString(f.read()) - self.assertTrue(xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, - 'Improperly serialized namespace prefix declarations when not in default namespace') - - -class MoveSVGElementsToDefaultNamespace(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scourString(f.read()) - self.assertTrue(xmlstring.find(' does not inherit xml:space="preserve" of parent text element') - text = self.doc.getElementById('txt_c2') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="default" of does not overwrite xml:space="preserve" of parent text element') - text = self.doc.getElementById('txt_c3') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="preserve" of does not overwrite xml:space="default" of parent text element') - text = self.doc.getElementById('txt_c4') - self.assertIn('text1 text2', text.toxml(), - ' does not inherit xml:space="preserve" of parent group') - text = self.doc.getElementById('txt_c5') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="default" of text element does not overwrite xml:space="preserve" of parent group') - text = self.doc.getElementById('txt_c6') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="preserve" of text element does not overwrite xml:space="default" of parent group') - - def test_important_whitespace(self): - text = self.doc.getElementById('txt_d1') - self.assertIn('text1 text2', text.toxml(), - 'Newline with whitespace collapsed in text element') - text = self.doc.getElementById('txt_d2') - self.assertIn('text1 tspan1 text2', text.toxml(), - 'Whitespace stripped from the middle of a text element') - text = self.doc.getElementById('txt_d3') - self.assertIn('text1 tspan1 tspan2 text2', text.toxml(), - 'Whitespace stripped from the middle of a text element') - - def test_incorrect_whitespace(self): - text = self.doc.getElementById('txt_e1') - self.assertIn('text1text2', text.toxml(), - 'Whitespace introduced in text element with newline') - text = self.doc.getElementById('txt_e2') - self.assertIn('text1tspantext2', text.toxml(), - 'Whitespace introduced in text element with ') - text = self.doc.getElementById('txt_e3') - self.assertIn('text1tspantext2', text.toxml(), - 'Whitespace introduced in text element with and newlines') - - -class GetAttrPrefixRight(unittest.TestCase): - - def runTest(self): - grad = scourXmlFile('unittests/xml-namespace-attrs.svg') \ - .getElementsByTagNameNS(SVGNS, 'linearGradient')[1] - self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', - 'Did not get xlink:href prefix right') - - -class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): - - def runTest(self): - with open('unittests/no-collapse-lines.svg') as f: - s = scourString(f.read()) - self.assertEqual(len(s.splitlines()), 6, - 'Did not properly preserve whitespace on elements even if they were not textual') - - -class HandleEmptyStyleElement(unittest.TestCase): - - def runTest(self): - try: - styles = scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') - fail = len(styles) != 1 - except AttributeError: - fail = True - self.assertEqual(fail, False, - 'Could not handle an empty style element') - - -class EnsureLineEndings(unittest.TestCase): - - def runTest(self): - with open('unittests/newlines.svg') as f: - s = scourString(f.read()) - self.assertEqual(len(s.splitlines()), 24, - 'Did handle reading or outputting line ending characters correctly') - - -class XmlEntities(unittest.TestCase): - - def runTest(self): - self.assertEqual(make_well_formed('<>&'), '<>&', - 'Incorrectly translated unquoted XML entities') - self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_APOS), '<>&', - 'Incorrectly translated single-quoted XML entities') - self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_QUOT), '<>&', - 'Incorrectly translated double-quoted XML entities') - - self.assertEqual(make_well_formed("'"), "'", - 'Incorrectly translated unquoted single quote') - self.assertEqual(make_well_formed('"'), '"', - 'Incorrectly translated unquoted double quote') - - self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_QUOT), "'", - 'Incorrectly translated double-quoted single quote') - self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_APOS), '"', - 'Incorrectly translated single-quoted double quote') - - self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_APOS), ''', - 'Incorrectly translated single-quoted single quote') - self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_QUOT), '"', - 'Incorrectly translated double-quoted double quote') - - -class HandleQuotesInAttributes(unittest.TestCase): - - def runTest(self): - with open('unittests/entities.svg', "rb") as f: - output = scourString(f.read()) - self.assertTrue('a="\'"' in output, - 'Failed on attribute value with non-double quote') - self.assertTrue("b='\"'" in output, - 'Failed on attribute value with non-single quote') - self.assertTrue("c=\"''"\"" in output, - 'Failed on attribute value with more single quotes than double quotes') - self.assertTrue('d=\'""'\'' in output, - 'Failed on attribute value with more double quotes than single quotes') - self.assertTrue("e=\"''""\"" in output, - 'Failed on attribute value with the same number of double quotes as single quotes') - - -class PreserveQuotesInStyles(unittest.TestCase): - - def runTest(self): - with open('unittests/quotes-in-styles.svg', "rb") as f: - output = scourString(f.read()) - self.assertTrue('use[id="t"]' in output, - 'Failed to preserve quote characters in a style element') - self.assertTrue("'Times New Roman'" in output, - 'Failed to preserve quote characters in a style attribute') - - -class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/comments.svg') - self.assertEqual(doc.childNodes.length, 4, - 'Did not include all comment children outside of root') - self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual(doc.childNodes[1].nodeType, 8, 'Second node not a comment') - self.assertEqual(doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') - - -class DoNotStripDoctype(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/doctype.svg') - self.assertEqual(doc.childNodes.length, 3, - 'Did not include the DOCROOT') - self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual(doc.childNodes[1].nodeType, 10, 'Second node not a doctype') - self.assertEqual(doc.childNodes[2].nodeType, 1, 'Third node not the root node') - - -class PathImplicitLineWithMoveCommands(unittest.TestCase): - - def runTest(self): - path = scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", - "Implicit line segments after move not preserved") - - -class RemoveTitlesOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-titles'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove tag with --remove-titles') - - -class RemoveDescriptionsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-descriptions'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <desc> tag with --remove-descriptions') - - -class RemoveMetadataOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-metadata'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <metadata> tag with --remove-metadata') - - -class RemoveDescriptiveElementsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-descriptive-elements'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <title>, <desc> and <metadata> tags with --remove-descriptive-elements') - - -class EnableCommentStrippingOption(unittest.TestCase): - - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scourString(docStr, - parse_args(['--enable-comment-stripping'])) - self.assertEqual(docStr.find('<!--'), -1, - 'Did not remove document-level comment with --enable-comment-stripping') - - -class StripXmlPrologOption(unittest.TestCase): - - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scourString(docStr, - parse_args(['--strip-xml-prolog'])) - self.assertEqual(docStr.find('<?xml'), -1, - 'Did not remove <?xml?> with --strip-xml-prolog') - - -class ShortenIDsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/shorten-ids.svg', - parse_args(['--shorten-ids'])) - gradientTag = doc.getElementsByTagName('linearGradient')[0] - self.assertEqual(gradientTag.getAttribute('id'), 'a', - "Did not shorten a linear gradient's ID with --shorten-ids") - rectTag = doc.getElementsByTagName('rect')[0] - self.assertEqual(rectTag.getAttribute('fill'), 'url(#a)', - 'Did not update reference to shortened ID') - - -class ShortenIDsStableOutput(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/shorten-ids-stable-output.svg', - parse_args(['--shorten-ids'])) - use_tags = doc.getElementsByTagName('use') - hrefs_ordered = [x.getAttributeNS('http://www.w3.org/1999/xlink', 'href') - for x in use_tags] - expected = ['#a', '#b', '#b'] - self.assertEqual(hrefs_ordered, expected, - '--shorten-ids pointlessly reassigned ids') - - -class MustKeepGInSwitch(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-in-switch.svg') - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Erroneously removed a <g> in a <switch>') - - -class MustKeepGInSwitch2(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-in-switch-with-id.svg', - parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Erroneously removed a <g> in a <switch>') - - -class GroupSiblingMerge(unittest.TestCase): - - def test_sibling_merge(self): - doc = scourXmlFile('unittests/group-sibling-merge.svg', - parse_args([])) - self.assertEqual(doc.getElementsByTagName('g').length, 5, - 'Merged sibling <g> tags with similar values') - - def test_sibling_merge_disabled(self): - doc = scourXmlFile('unittests/group-sibling-merge.svg', - parse_args(['--disable-group-collapsing'])) - self.assertEqual(doc.getElementsByTagName('g').length, 8, - 'Sibling merging is disabled by --disable-group-collapsing') - - def test_sibling_merge_crash(self): - doc = scourXmlFile('unittests/group-sibling-merge-crash.svg', - parse_args([''])) - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Sibling merge should work without causing crashes') - - -class GroupCreation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/group-creation.svg', - parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Did not create a <g> for a run of elements having similar attributes') - - -class GroupCreationForInheritableAttributesOnly(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/group-creation.svg', - parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').item(0).getAttribute('y'), '', - 'Promoted the uninheritable attribute y to a <g>') - - -class GroupNoCreation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/group-no-creation.svg', - parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 0, - 'Created a <g> for a run of elements having dissimilar attributes') - - -class GroupNoCreationForTspan(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/group-no-creation-tspan.svg', - parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 0, - 'Created a <g> for a run of <tspan>s ' - 'that are not allowed as children according to content model') - - -class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/commonized-referenced-elements.svg') - self.assertEqual(doc.getElementsByTagName('circle')[0].getAttribute('fill'), '#0f0', - 'Grouped an element referenced elsewhere into a <g>') - - -class DoNotRemoveOverflowVisibleOnMarker(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/overflow-marker.svg') - self.assertEqual(doc.getElementById('m1').getAttribute('overflow'), 'visible', - 'Removed the overflow attribute when it was not using the default value') - self.assertEqual(doc.getElementById('m2').getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') - - -class DoNotRemoveOrientAutoOnMarker(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/orient-marker.svg') - self.assertEqual(doc.getElementById('m1').getAttribute('orient'), 'auto', - 'Removed the orient attribute when it was not using the default value') - self.assertEqual(doc.getElementById('m2').getAttribute('orient'), '', - 'Did not remove the orient attribute when it was using the default value') - - -class MarkerOnSvgElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/overflow-svg.svg') - self.assertEqual(doc.getElementsByTagName('svg')[0].getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') - self.assertEqual(doc.getElementsByTagName('svg')[1].getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') - self.assertEqual(doc.getElementsByTagName('svg')[2].getAttribute('overflow'), 'visible', - 'Removed the overflow attribute when it was not using the default value') - - -class GradientReferencedByStyleCDATA(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/style-cdata.svg') - self.assertEqual(len(doc.getElementsByTagName('linearGradient')), 1, - 'Removed a gradient referenced by an internal stylesheet') - - -class ShortenIDsInStyleCDATA(unittest.TestCase): - - def runTest(self): - with open('unittests/style-cdata.svg') as f: - docStr = f.read() - docStr = scourString(docStr, - parse_args(['--shorten-ids'])) - self.assertEqual(docStr.find('somethingreallylong'), -1, - 'Did not shorten IDs in the internal stylesheet') - - -class StyleToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/style-to-attr.svg') - line = doc.getElementsByTagName('line')[0] - self.assertEqual(line.getAttribute('stroke'), '#000') - self.assertEqual(line.getAttribute('marker-start'), 'url(#m)') - self.assertEqual(line.getAttribute('marker-mid'), 'url(#m)') - self.assertEqual(line.getAttribute('marker-end'), 'url(#m)') - - -class PathCommandRewrites(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-command-rewrites.svg') - paths = doc.getElementsByTagName('path') - expected_paths = [ - ('m100 100 200 100', "Trailing m0 0z not removed"), - ('m100 100v200m0 0 100 100z', "Mangled m0 0 100 100"), - ("m100 100v200m0 0 2-1-2 1z", "Should have removed empty m0 0"), - ("m100 100v200l3-5-5 3m0 0 2-1-2 1z", "Rewrite m0 0 3-5-5 3 ... -> l3-5-5 3 ..."), - ("m100 100v200m0 0 3-5-5 3zm0 0 2-1-2 1z", "No rewrite of m0 0 3-5-5 3z"), - ] - self.assertEqual(len(paths), len(expected_paths), "len(actual_paths) != len(expected_paths)") - for i in range(len(paths)): - actual_path = paths[i].getAttribute('d') - expected_path, message = expected_paths[i] - self.assertEqual(actual_path, - expected_path, - '%s: "%s" != "%s"' % (message, actual_path, expected_path)) - - -class DefaultsRemovalToplevel(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed') - - -class DefaultsRemovalToplevelInverse(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed') - - -class DefaultsRemovalToplevelFormat(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed') - - -class DefaultsRemovalInherited(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed in child') - - -class DefaultsRemovalInheritedInverse(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed in child') - - -class DefaultsRemovalInheritedFormat(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed in child') - - -class DefaultsRemovalOverwrite(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', - 'Default attribute removed, although it overwrites parent element') - - -class DefaultsRemovalOverwriteMarker(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', - 'Default marker attribute removed, although it overwrites parent element') - - -class DefaultsRemovalNonOverwrite(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', - 'Default attribute not removed, although its parent used default') - - -class RemoveDefsWithUnreferencedElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/useless-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only unreferenced elements') - - -class RemoveDefsWithWhitespace(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/whitespace-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only whitespace or is <defs/>') - - -class TransformIdentityMatrix(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity matrix not removed') - - -class TransformRotate135(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', - 'Rotation matrix not converted to rotate(135)') - - -class TransformRotate45(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', - 'Rotation matrix not converted to rotate(45)') - - -class TransformRotate90(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Rotation matrix not converted to rotate(90)') - - -class TransformRotateCCW135(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', - 'Counter-clockwise rotation matrix not converted to rotate(225)') - - -class TransformRotateCCW45(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', - 'Counter-clockwise rotation matrix not converted to rotate(-45)') - - -class TransformRotateCCW90(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', - 'Counter-clockwise rotation matrix not converted to rotate(-90)') - - -class TransformScale2by3(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', - 'Scaling matrix not converted to scale(2 3)') - - -class TransformScaleMinus1(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', - 'Scaling matrix not converted to scale(-1)') - - -class TransformTranslate(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-translate.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', - 'Translation matrix not converted to translate(2 3)') - - -class TransformRotationRange719_5(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', - 'Transform containing rotate(719.5) not shortened to rotate(-.5)') - - -class TransformRotationRangeCCW540_0(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', - 'Transform containing rotate(-540.0) not shortened to rotate(180)') - - -class TransformRotation3Args(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-fold-3args.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Optional zeroes in rotate(angle 0 0) not removed') - - -class TransformIdentityRotation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity rotation not removed') - - -class TransformIdentitySkewX(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-skewX-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity X-axis skew not removed') - - -class TransformIdentitySkewY(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-skewY-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity Y-axis skew not removed') - - -class TransformIdentityTranslate(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-translate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity translation not removed') - - -class TransformIdentityScale(unittest.TestCase): - - def runTest(self): - try: - doc = scourXmlFile('unittests/transform-scale-is-identity.svg') - except IndexError: - self.fail("scour failed to handled scale(1) [See GH#190]") - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('scale'), '', - 'Transform containing identity translation not removed') - - -class DuplicateGradientsUpdateStyle(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradients-update-style.svg', - parse_args(['--disable-style-to-xml'])) - gradient = doc.getElementsByTagName('linearGradient')[0] - rects = doc.getElementsByTagName('rect') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" (with fallback) was not updated to reflect this') - - -class DocWithFlowtext(unittest.TestCase): - - def runTest(self): - with self.assertRaises(Exception): - scourXmlFile('unittests/flowtext.svg', - parse_args(['--error-on-flowtext'])) - - -class DocWithNoFlowtext(unittest.TestCase): - - def runTest(self): - try: - scourXmlFile('unittests/flowtext-less.svg', - parse_args(['--error-on-flowtext'])) - except Exception as e: - self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) - - -class ParseStyleAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/style.svg') - self.assertEqual(doc.documentElement.getAttribute('style'), - 'property1:value1;property2:value2;property3:value3', - "Style attribute not properly parsed and/or serialized") - - -class StripXmlSpaceAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/xml-space.svg', - parse_args(['--strip-xml-space'])) - self.assertEqual(doc.documentElement.getAttribute('xml:space'), '', - "'xml:space' attribute not removed from root SVG element" - "when '--strip-xml-space' was specified") - self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element " - "when '--strip-xml-space' was specified (should only operate on root SVG element)") - - -class DoNotStripXmlSpaceAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/xml-space.svg') - self.assertNotEqual(doc.documentElement.getAttribute('xml:space'), '', - "'xml:space' attribute removed from root SVG element" - "when '--strip-xml-space' was NOT specified") - self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element " - "when '--strip-xml-space' was NOT specified (should never be removed!)") - - -class CommandLineUsage(unittest.TestCase): - - USAGE_STRING = "Usage: scour [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]" - MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ - '<svg xmlns="http://www.w3.org/2000/svg"/>\n' - TEMP_SVG_FILE = 'testscour_temp.svg' - - # wrapper function for scour.run() to emulate command line usage - # - # returns an object with the following attributes: - # status: the exit status - # stdout: a string representing the combined output to 'stdout' - # stderr: a string representing the combined output to 'stderr' - def _run_scour(self): - class Result(object): - pass - - result = Result() - try: - run() - result.status = 0 - except SystemExit as exception: # catch any calls to sys.exit() - result.status = exception.code - result.stdout = self.temp_stdout.getvalue() - result.stderr = self.temp_stderr.getvalue() - - return result - - def setUp(self): - # store current values of 'argv', 'stdin', 'stdout' and 'stderr' - self.argv = sys.argv - self.stdin = sys.stdin - self.stdout = sys.stdout - self.stderr = sys.stderr - - # start with a fresh 'argv' - sys.argv = ['scour'] # TODO: Do we need a (more) valid 'argv[0]' for anything? - - # create 'stdin', 'stdout' and 'stderr' with behavior close to the original - # TODO: can we create file objects that behave *exactly* like the original? - # this is a mess since we have to ensure compatibility across Python 2 and 3 and it seems impossible - # to replicate all the details of 'stdin', 'stdout' and 'stderr' - class InOutBuffer(six.StringIO, object): - def write(self, string): - try: - return super(InOutBuffer, self).write(string) - except TypeError: - return super(InOutBuffer, self).write(string.decode()) - - sys.stdin = self.temp_stdin = InOutBuffer() - sys.stdout = self.temp_stdout = InOutBuffer() - sys.stderr = self.temp_stderr = InOutBuffer() - - self.temp_stdin.name = '<stdin>' # Scour wants to print the name of the input file... - - def tearDown(self): - # restore previous values of 'argv', 'stdin', 'stdout' and 'stderr' - sys.argv = self.argv - sys.stdin = self.stdin - sys.stdout = self.stdout - sys.stderr = self.stderr - - # clean up - self.temp_stdin.close() - self.temp_stdout.close() - self.temp_stderr.close() - - def test_no_arguments(self): - # we have to pretend that our input stream is a TTY, otherwise Scour waits for input from stdin - self.temp_stdin.isatty = lambda: True - - result = self._run_scour() - - self.assertEqual(result.status, 2, "Execution of 'scour' without any arguments should exit with status '2'") - self.assertTrue(self.USAGE_STRING in result.stderr, - "Usage information not displayed when calling 'scour' without any arguments") - - def test_version(self): - sys.argv.append('--version') - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Execution of 'scour --version' erorred'") - self.assertEqual(__version__ + "\n", result.stdout, "Unexpected output of 'scour --version'") - - def test_help(self): - sys.argv.append('--help') - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Execution of 'scour --help' erorred'") - self.assertTrue(self.USAGE_STRING in result.stdout and 'Options:' in result.stdout, - "Unexpected output of 'scour --help'") - - def test_stdin_stdout(self): - sys.stdin.write(self.MINIMAL_SVG) - sys.stdin.seek(0) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour via 'stdin' / 'stdout' erorred'") - self.assertEqual(result.stdout, self.MINIMAL_SVG, "Unexpected SVG output via 'stdout'") - - def test_filein_fileout_named(self): - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as named parameters errored'") - with open(self.TEMP_SVG_FILE) as file: - file_content = file.read() - self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") - os.remove(self.TEMP_SVG_FILE) - - def test_filein_fileout_positional(self): - sys.argv.extend(['unittests/minimal.svg', self.TEMP_SVG_FILE]) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as positional parameters errored'") - with open(self.TEMP_SVG_FILE) as file: - file_content = file.read() - self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") - os.remove(self.TEMP_SVG_FILE) - - def test_quiet(self): - sys.argv.append('-q') - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - os.remove(self.TEMP_SVG_FILE) - - self.assertEqual(result.status, 0, "Execution of 'scour -q ...' erorred'") - self.assertEqual(result.stdout, '', "Output writtent to 'stdout' when '--quiet' options was used") - self.assertEqual(result.stderr, '', "Output writtent to 'stderr' when '--quiet' options was used") - - def test_verbose(self): - sys.argv.append('-v') - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - os.remove(self.TEMP_SVG_FILE) - - self.assertEqual(result.status, 0, "Execution of 'scour -v ...' erorred'") - self.assertEqual(result.stdout.count('Number'), 14, - "Statistics output not as expected when '--verbose' option was used") - self.assertEqual(result.stdout.count(': 0'), 14, - "Statistics output not as expected when '--verbose' option was used") - - -class EmbedRasters(unittest.TestCase): - - # quick way to ping a host using the OS 'ping' command and return the execution result - def _ping(host): - import os - import platform - - # work around https://github.com/travis-ci/travis-ci/issues/3080 as pypy throws if 'ping' can't be executed - import distutils.spawn - if not distutils.spawn.find_executable('ping'): - return -1 - - system = platform.system().lower() - ping_count = '-n' if system == 'windows' else '-c' - dev_null = 'NUL' if system == 'windows' else '/dev/null' - - return os.system('ping ' + ping_count + ' 1 ' + host + ' > ' + dev_null) - - def test_disable_embed_rasters(self): - doc = scourXmlFile('unittests/raster-formats.svg', - parse_args(['--disable-embed-rasters'])) - self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), 'raster.png', - "Raster image embedded when '--disable-embed-rasters' was specified") - - def test_raster_formats(self): - doc = scourXmlFile('unittests/raster-formats.svg') - self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAABAgMAAABmjvwnAAAAC' - 'VBMVEUAAP//AAAA/wBmtfVOAAAACklEQVQI12NIAAAAYgBhGxZhsAAAAABJRU5ErkJggg==', - "Raster image (PNG) not correctly embedded.") - self.assertEqual(doc.getElementById('gif').getAttribute('xlink:href'), - 'data:image/gif;base64,R0lGODdhAwABAKEDAAAA//8AAAD/AP///ywAAAAAAwABAAACAoxQADs=', - "Raster image (GIF) not correctly embedded.") - self.assertEqual(doc.getElementById('jpg').getAttribute('xlink:href'), - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/' - '2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' - '2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' - 'wAARCAABAAMDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABoQAAEFAQAAAAAAAAAAAAAAAAgABQc3d7j/' - 'xAAVAQEBAAAAAAAAAAAAAAAAAAAHCv/EABwRAAEDBQAAAAAAAAAAAAAAAAgAB7gJODl2eP/aAAwDAQACEQMRAD8AMeaF' - '/u2aj5z1Fqp7oN4rxx2kn5cPuhV6LkzG7qOyYL2r/9k=', - "Raster image (JPG) not correctly embedded.") - - def test_raster_paths_local(self): - doc = scourXmlFile('unittests/raster-paths-local.svg') - images = doc.getElementsByTagName('image') - for image in images: - href = image.getAttribute('xlink:href') - self.assertTrue(href.startswith('data:image/'), - "Raster image from local path '" + href + "' not embedded.") - - def test_raster_paths_local_absolute(self): - with open('unittests/raster-formats.svg', 'r') as f: - svg = f.read() - - # create a reference string by scouring the original file with relative links - options = ScourOptions - options.infilename = 'unittests/raster-formats.svg' - reference_svg = scourString(svg, options) - - # this will not always create formally valid paths but it'll check how robust our implementation is - # (the third path is invalid for sure because file: needs three slashes according to URI spec) - svg = svg.replace('raster.png', - '/' + os.path.abspath(os.path.dirname(__file__)) + '\\unittests\\raster.png') - svg = svg.replace('raster.gif', - 'file:///' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.gif') - svg = svg.replace('raster.jpg', - 'file:/' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.jpg') - - svg = scourString(svg) - - self.assertEqual(svg, reference_svg, - "Raster images from absolute local paths not properly embedded.") - - @unittest.skipIf(_ping('raw.githubusercontent.com') != 0, "Remote server not reachable.") - def test_raster_paths_remote(self): - doc = scourXmlFile('unittests/raster-paths-remote.svg') - images = doc.getElementsByTagName('image') - for image in images: - href = image.getAttribute('xlink:href') - self.assertTrue(href.startswith('data:image/'), - "Raster image from remote path '" + href + "' not embedded.") - - -class ViewBox(unittest.TestCase): - - def test_viewbox_create(self): - doc = scourXmlFile('unittests/viewbox-create.svg', parse_args(['--enable-viewboxing'])) - viewBox = doc.documentElement.getAttribute('viewBox') - self.assertEqual(viewBox, '0 0 123.46 654.32', "viewBox not properly created with '--enable-viewboxing'.") - - def test_viewbox_remove_width_and_height(self): - doc = scourXmlFile('unittests/viewbox-remove.svg', parse_args(['--enable-viewboxing'])) - width = doc.documentElement.getAttribute('width') - height = doc.documentElement.getAttribute('height') - self.assertEqual(width, '', "width not removed with '--enable-viewboxing'.") - self.assertEqual(height, '', "height not removed with '--enable-viewboxing'.") - - -# TODO: write tests for --keep-editor-data - -if __name__ == '__main__': - testcss = __import__('test_css') - scour = __import__('__main__') - suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) - unittest.main(defaultTest="suite") diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 82420b6..0000000 --- a/tox.ini +++ /dev/null @@ -1,31 +0,0 @@ -[tox] -envlist = - pypy - py27 - py34 - py35 - py36 - py37 - py38 - py39 - py310 - flake8 - - - -[testenv] -deps = - six - coverage - -commands = - scour --version - coverage run --parallel-mode --source=scour test_scour.py - - -[testenv:flake8] -deps = - flake8 - -commands = - flake8 --max-line-length=119 diff --git a/unittests/adobe.svg b/unittests/adobe.svg deleted file mode 100644 index 7dd7e73..0000000 --- a/unittests/adobe.svg +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" - xmlns:x="http://ns.adobe.com/Extensibility/1.0/" - xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" - xmlns:graph="http://ns.adobe.com/Graphs/1.0/" - xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" - xmlns:f="http://ns.adobe.com/Flows/1.0/" - xmlns:ir="http://ns.adobe.com/ImageReplacement/1.0/" - xmlns:custom="http://ns.adobe.com/GenericCustomNamespace/1.0/" - xmlns:xpath="http://ns.adobe.com/XPath/1.0/" - xmlns:ok="A.namespace.we.want.left.in" - i:viewOrigin="190.2959 599.1841" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0"> -<x:foo>bar</x:foo> -<i:foo>bar</i:foo> -<graph:foo>bar</graph:foo> -<a:foo>bar</a:foo> -<f:foo>bar</f:foo> -<ir:foo>bar</ir:foo> -<custom:foo>bar</custom:foo> -<xpath:foo>bar</xpath:foo> -<variableSets xmlns="http://ns.adobe.com/Variables/1.0/"> - <variableSet varSetName="binding1" locked="none"> - <variables/> - <v:sampleDataSets xmlns="http://ns.adobe.com/GenericCustomNamespace/1.0/" xmlns:v="http://ns.adobe.com/Variables/1.0/"/> - </variableSet> -</variableSets> -<sfw xmlns="http://ns.adobe.com/SaveForWeb/1.0/"> - <slices/> - <sliceSourceBounds y="191.664" x="190.296" width="225.72" height="407.52" bottomLeftOrigin="true"/> -</sfw> -<rect width="300" height="200" fill="green" - x:baz="1" - i:baz="1" - graph:baz="1" - a:baz="1" - f:baz="1" - ir:baz="1" - custom:baz='1' - xpath:baz="1" - xmlns:v="http://ns.adobe.Variables/1.0/" - v:baz="1" - xmlns:sfw="http://ns.adobe.com/SaveForWeb/1.0/" - sfw:baz="1" - ok:baz="1" /> -</svg> diff --git a/unittests/cascading-default-attribute-removal.svg b/unittests/cascading-default-attribute-removal.svg deleted file mode 100644 index dbc3698..0000000 --- a/unittests/cascading-default-attribute-removal.svg +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <path style="fill-rule:evenodd;stroke-linecap:butt;stroke-width:1.00;stroke:#000" d="m1,1z"/> - <path style="fill-rule:nonzero;stroke-linecap:butt;stroke:#000" d="m1,1z"/> - <g style="stroke:#f00;marker:none"> - <path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/> - <path style="fill-rule:nonzero" d="m1,1z"/> - <g style="fill:#f0f;text-anchor:stop;fill-rule:evenodd;stroke-linecap:round;marker:url(#nirvana)"> - <path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/> - <path style="color:#000;fill-rule:nonzero;" d="m1,1z"/> - <path d="m1,1z"/> - </g> - <g style="fill:#f0f;text-anchor:stop;fill-rule:evenodd;stroke-linecap:round;marker:url(#nirvana)"> - <path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/> - <path style="color:#000;fill-rule:nonzero;" d="m1,1z"/> - </g> - <g style="text-anchor:stop;fill-rule:nonzero;marker:none;stroke-linecap:butt"> - <path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/> - <path style="fill-rule:nonzero;" d="m1,1z"/> - <path d="m1,1z"/> - </g> - </g> -</svg> diff --git a/unittests/cdata.svg b/unittests/cdata.svg deleted file mode 100644 index 8ecb680..0000000 --- a/unittests/cdata.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <script type="application/ecmascript"><![CDATA[ - alert('pb&j'); - ]]></script> -</svg> diff --git a/unittests/collapse-gradients-gradientUnits.svg b/unittests/collapse-gradients-gradientUnits.svg deleted file mode 100644 index 76f6169..0000000 --- a/unittests/collapse-gradients-gradientUnits.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<defs> - <linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"> - <stop offset="0" stop-color="blue" /> - <stop offset="1" stop-color="yellow" /> - </linearGradient> - <radialGradient id="g2" xlink:href="#g1" cx="50%" cy="50%" r="30%" gradientUnits="objectBoundingBox"/> -</defs> -<rect fill="url(#g2)" width="200" height="200"/> -</svg> diff --git a/unittests/collapse-gradients-preserve-xlink-href.svg b/unittests/collapse-gradients-preserve-xlink-href.svg deleted file mode 100644 index f736922..0000000 --- a/unittests/collapse-gradients-preserve-xlink-href.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<defs> - <linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"> - <stop offset="0" stop-color="blue" /> - <stop offset="1" stop-color="yellow" /> - </linearGradient> - <radialGradient id="g2" xlink:href="#g1" cx="100" cy="100" r="70"/> - <radialGradient id="g3" xlink:href="#g2" cx="100" cy="100" r="70"/> -</defs> -<rect fill="url(#g1)" width="200" height="200"/> -<rect fill="url(#g3)" width="200" height="200" y="200"/> -</svg> diff --git a/unittests/collapse-gradients.svg b/unittests/collapse-gradients.svg deleted file mode 100644 index a45f962..0000000 --- a/unittests/collapse-gradients.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<defs> - <linearGradient id="grad1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" spreadMethod="reflect" gradientTransform="matrix(1,2,3,4,5,6)"> - <stop offset="0" stop-color="blue" /> - <stop offset="1" stop-color="yellow" /> - </linearGradient> - <radialGradient id="grad2" xlink:href="#grad1" cx="100" cy="100" r="70"/> -</defs> -<rect fill="url(#grad2)" width="200" height="200"/> -</svg> diff --git a/unittests/collapse-same-path-points.svg b/unittests/collapse-same-path-points.svg deleted file mode 100644 index b05f4d1..0000000 --- a/unittests/collapse-same-path-points.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="210" height="210"> - <path stroke="yellow" fill="red" d="M100,100 L200.12345,200.12345 C215,205 185,195 200.12345,200.12345 Z"/> -</svg> diff --git a/unittests/collapse-straight-path-segments.svg b/unittests/collapse-straight-path-segments.svg deleted file mode 100644 index fa8e030..0000000 --- a/unittests/collapse-straight-path-segments.svg +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="dot"> - <circle r="5px"/> - </marker> - </defs> - - <!-- h/v commands should be collapsed into a single h/v commands --> - <path d="m0 0h10 20"/> - <path d="m0 0v10 20"/> - <path d="m0 0h10 0.5v10 0.5"/> - <!-- h/v commands should not be collapsed if they have different direction --> - <path d="m0 0h10 -1v10 -1"/> - <!-- h/v commands should also be collapsed if only start/end markers are present --> - <path d="m0 0h10 20" marker-start="url(#dot)" marker-end="url(#dot)"/> - <path d="m0 0h10 20" style="marker-start:url(#dot);marker-end:url(#dot)"/> - <!-- h/v commands should be preserved if intermediate markers are present --> - <path d="m0 0h10 20" marker="url(#dot)"/> - <path d="m0 0h10 20" marker-mid="url(#dot)"/> - <path d="m0 0h10 20" style="marker:url(#dot)"/> - <path d="m0 0h10 20" style="marker-mid:url(#dot)"/> - - <!-- all consecutive lineto commands pointing into the sam direction - should be collapsed into a single (implicit if possible) lineto command --> - <path d="m 0 0 l 10 20 0.25 0.5 l 0.75 1.5 l 5 10 0.2 0.4 l 3 6 0.8 1.6 l 0 1 l 1 2 9 18"/> - <!-- must not be collapsed (same slope, but different direction) --> - <path d="m 0 0 10 10 -20 -20 l 10 10 -20 -20"/> - <!-- first parameter pair of a moveto subpath must not be collapsed as it's not drawn on canvas --> - <path d="m0 0 1 2 m 1 2 1 2l 1 2 m 1 2 1 2 1 2"/> - <!-- real world example of straight path with multiple nodes --> - <path d="m 6.3227953,7.1547422 10.6709787,5.9477588 9.20334,5.129731 22.977448,12.807101 30.447251,16.970601 7.898986,4.402712"/> -</svg> diff --git a/unittests/color-formats.svg b/unittests/color-formats.svg deleted file mode 100644 index 0272c7e..0000000 --- a/unittests/color-formats.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" version="1.1"> -<defs> - <linearGradient id="g1" x1="0" y1="0" x2="1" y2="0"> - <stop offset="0.5" stop-color="rgb(50.0%, 0%, .0%)" /> - </linearGradient> - <solidColor id="c1" solid-color="lightgoldenrodyellow"/> -</defs> - <rect id="rect" width="100" height="100" fill="rgb(15,16,17)" stroke="darkgrey" /> - <circle id="circle" cx="100" cy="100" r="30" fill="url(#g1)" stroke="url(#c1)" /> - <ellipse id="ellipse" cx="100" cy="100" rx="30" ry="30" style="fill:#ffffff" fill="black" /> -</svg> diff --git a/unittests/comment-beside-xml-decl.svg b/unittests/comment-beside-xml-decl.svg deleted file mode 100644 index cd3ecff..0000000 --- a/unittests/comment-beside-xml-decl.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<!-- Oh look a comment --> -<!-- generated by foobar version 20120503 --> -<!-- And another --> -<svg xmlns="http://www.w3.org/2000/svg"> - <!-- This comment is meant to test whether removing a comment before <svg> - messes up removing comments thereafter --> - <!-- And this one is meant to test whether iteration works correctly in - <svg> as well as the document element --> -</svg> diff --git a/unittests/comments.svg b/unittests/comments.svg deleted file mode 100644 index 06a75f2..0000000 --- a/unittests/comments.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" ?> -<!-- Empty --> -<!-- Comment #2 --> -<svg xmlns="http://www.w3.org/2000/svg"> -</svg> -<!-- After --> diff --git a/unittests/commonized-referenced-elements.svg b/unittests/commonized-referenced-elements.svg deleted file mode 100644 index 3a152fb..0000000 --- a/unittests/commonized-referenced-elements.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <g id="g"> - <rect width="200" height="100" fill="#0f0"/> - <rect width="200" height="100" fill="#0f0"/> - <rect width="200" height="100" fill="#0f0"/> - <circle id="e" r="20" fill="#0f0"/> - </g> - <use xlink:href="#e" /> -</svg> diff --git a/unittests/css-reference.svg b/unittests/css-reference.svg deleted file mode 100644 index 6330c60..0000000 --- a/unittests/css-reference.svg +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - -<defs> - <linearGradient id="g1"> - <stop offset="0" stop-color="red"/> - <stop offset="1" stop-color="blue"/> - </linearGradient> - <linearGradient id="g2"> - <stop offset="0" stop-color="green"/> - <stop offset="1" stop-color="yellow"/> - </linearGradient> -</defs> -<style type="text/css"><![CDATA[ - rect { - stroke: red; - stroke-width: 10; - fill:url(#g1) - } -]]></style> - -<style type="text/css">.circ { fill: none; stroke: url("#g2"); stroke-width: 15 }</style> - -<rect height="300" width="300"/> -<circle class="circ" cx="350" cy="350" r="40"/> - -</svg> diff --git a/unittests/descriptive-elements-with-text.svg b/unittests/descriptive-elements-with-text.svg deleted file mode 100644 index c991ddd..0000000 --- a/unittests/descriptive-elements-with-text.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns="http://www.w3.org/2000/svg"> - <title>This is a title element with only text node children - This is a desc element with only text node children - This is a metadata element with only text node children - diff --git a/unittests/doctype.svg b/unittests/doctype.svg deleted file mode 100644 index d19e074..0000000 --- a/unittests/doctype.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - -]> - diff --git a/unittests/dont-collapse-gradients.svg b/unittests/dont-collapse-gradients.svg deleted file mode 100644 index 00b58f5..0000000 --- a/unittests/dont-collapse-gradients.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/unittests/dont-convert-short-color-names.svg b/unittests/dont-convert-short-color-names.svg deleted file mode 100644 index cbcece7..0000000 --- a/unittests/dont-convert-short-color-names.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/duplicate-gradient-stops-pct.svg b/unittests/duplicate-gradient-stops-pct.svg deleted file mode 100644 index 43c99c4..0000000 --- a/unittests/duplicate-gradient-stops-pct.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/duplicate-gradient-stops.svg b/unittests/duplicate-gradient-stops.svg deleted file mode 100644 index 4629bd6..0000000 --- a/unittests/duplicate-gradient-stops.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/unittests/duplicate-gradients-update-style.svg b/unittests/duplicate-gradients-update-style.svg deleted file mode 100644 index b18d7b9..0000000 --- a/unittests/duplicate-gradients-update-style.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/unittests/empty-descriptive-elements.svg b/unittests/empty-descriptive-elements.svg deleted file mode 100644 index 2790084..0000000 --- a/unittests/empty-descriptive-elements.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/empty-g.svg b/unittests/empty-g.svg deleted file mode 100644 index ccb7355..0000000 --- a/unittests/empty-g.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/unittests/empty-style.svg b/unittests/empty-style.svg deleted file mode 100644 index a2d2afd..0000000 --- a/unittests/empty-style.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/encoding-iso-8859-15.svg b/unittests/encoding-iso-8859-15.svg deleted file mode 100644 index 626aca4..0000000 --- a/unittests/encoding-iso-8859-15.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - ߤ - diff --git a/unittests/encoding-utf8.svg b/unittests/encoding-utf8.svg deleted file mode 100644 index dd63f12..0000000 --- a/unittests/encoding-utf8.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - Hello in many languages: -ar: أهلا -bn: হ্যালো -el: Χαίρετε -en: Hello -hi: नमस्ते -iw: שלום -ja: こんにちは -km: ជំរាបសួរ -ml: ഹലോ -ru: Здравствуйте -ur: ہیلو -zh: 您好 - “”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿� - :-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔ - ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ - diff --git a/unittests/entities.svg b/unittests/entities.svg deleted file mode 100644 index 2308b46..0000000 --- a/unittests/entities.svg +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/unittests/fill-none.svg b/unittests/fill-none.svg deleted file mode 100644 index 6442c90..0000000 --- a/unittests/fill-none.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/flowtext-less.svg b/unittests/flowtext-less.svg deleted file mode 100644 index eea559c..0000000 --- a/unittests/flowtext-less.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - abcd - - diff --git a/unittests/flowtext.svg b/unittests/flowtext.svg deleted file mode 100644 index 9409b4f..0000000 --- a/unittests/flowtext.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - sfdadasdasdasdadsa abcd - - diff --git a/unittests/font-styles.svg b/unittests/font-styles.svg deleted file mode 100644 index e4120df..0000000 --- a/unittests/font-styles.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/full-descriptive-elements.svg b/unittests/full-descriptive-elements.svg deleted file mode 100644 index 8decf2d..0000000 --- a/unittests/full-descriptive-elements.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - This is an example SVG file - Unit test for Scour's --remove-titles option - - - This is an example SVG file - Unit test for Scour's - --remove-descriptions option - - - - - - - No One - - - - - - diff --git a/unittests/gradient-default-attrs.svg b/unittests/gradient-default-attrs.svg deleted file mode 100644 index 25cdb82..0000000 --- a/unittests/gradient-default-attrs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/unittests/group-creation.svg b/unittests/group-creation.svg deleted file mode 100644 index 96776c0..0000000 --- a/unittests/group-creation.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/group-no-creation-tspan.svg b/unittests/group-no-creation-tspan.svg deleted file mode 100644 index 65f3803..0000000 --- a/unittests/group-no-creation-tspan.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - text1 - text2 - text3 - - diff --git a/unittests/group-no-creation.svg b/unittests/group-no-creation.svg deleted file mode 100644 index bea6419..0000000 --- a/unittests/group-no-creation.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/group-sibling-merge-crash.svg b/unittests/group-sibling-merge-crash.svg deleted file mode 100644 index 3e50347..0000000 --- a/unittests/group-sibling-merge-crash.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/unittests/group-sibling-merge.svg b/unittests/group-sibling-merge.svg deleted file mode 100644 index c7f0d02..0000000 --- a/unittests/group-sibling-merge.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - Produced by GNUPLOT 5.2 patchlevel 8 - - - - - 0 - - - - - - 5000 - - - - - - 10000 - - - - - - 15000 - - - diff --git a/unittests/groups-in-switch-with-id.svg b/unittests/groups-in-switch-with-id.svg deleted file mode 100644 index 317cfcc..0000000 --- a/unittests/groups-in-switch-with-id.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/unittests/groups-in-switch.svg b/unittests/groups-in-switch.svg deleted file mode 100644 index 96394fd..0000000 --- a/unittests/groups-in-switch.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/unittests/groups-with-title-desc.svg b/unittests/groups-with-title-desc.svg deleted file mode 100644 index 7983dc0..0000000 --- a/unittests/groups-with-title-desc.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Group 1 - - - - - Group 1 - - - - diff --git a/unittests/ids-protect.svg b/unittests/ids-protect.svg deleted file mode 100644 index 9809209..0000000 --- a/unittests/ids-protect.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - Text 1 - Text 2 - Text 3 - Text custom - My text - diff --git a/unittests/ids-to-strip.svg b/unittests/ids-to-strip.svg deleted file mode 100644 index 1ac59bc..0000000 --- a/unittests/ids-to-strip.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - Fooey - - - - - - - diff --git a/unittests/ids.svg b/unittests/ids.svg deleted file mode 100644 index b787343..0000000 --- a/unittests/ids.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/important-groups-in-defs.svg b/unittests/important-groups-in-defs.svg deleted file mode 100644 index 18ba1df..0000000 --- a/unittests/important-groups-in-defs.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/inkscape.svg b/unittests/inkscape.svg deleted file mode 100644 index a51ad49..0000000 --- a/unittests/inkscape.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/unittests/minimal.svg b/unittests/minimal.svg deleted file mode 100644 index b9d264c..0000000 --- a/unittests/minimal.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/unittests/move-common-attributes-to-grandparent.svg b/unittests/move-common-attributes-to-grandparent.svg deleted file mode 100644 index 4e202bd..0000000 --- a/unittests/move-common-attributes-to-grandparent.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/unittests/move-common-attributes-to-parent.svg b/unittests/move-common-attributes-to-parent.svg deleted file mode 100644 index f390c89..0000000 --- a/unittests/move-common-attributes-to-parent.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - -Hello -World! -Goodbye -Cruel World! - - diff --git a/unittests/nested-defs.svg b/unittests/nested-defs.svg deleted file mode 100644 index 7091985..0000000 --- a/unittests/nested-defs.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/unittests/nested-useless-groups.svg b/unittests/nested-useless-groups.svg deleted file mode 100644 index 73b5f88..0000000 --- a/unittests/nested-useless-groups.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/unittests/newlines.svg b/unittests/newlines.svg deleted file mode 100644 index a909603..0000000 --- a/unittests/newlines.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/unittests/no-collapse-lines.svg b/unittests/no-collapse-lines.svg deleted file mode 100644 index 85da385..0000000 --- a/unittests/no-collapse-lines.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/unittests/orient-marker.svg b/unittests/orient-marker.svg deleted file mode 100644 index 19ecd19..0000000 --- a/unittests/orient-marker.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/overflow-marker.svg b/unittests/overflow-marker.svg deleted file mode 100644 index ec068d9..0000000 --- a/unittests/overflow-marker.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/overflow-svg.svg b/unittests/overflow-svg.svg deleted file mode 100644 index 8830a80..0000000 --- a/unittests/overflow-svg.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/unittests/path-abs-to-rel.svg b/unittests/path-abs-to-rel.svg deleted file mode 100644 index c9cc803..0000000 --- a/unittests/path-abs-to-rel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-bez-optimize.svg b/unittests/path-bez-optimize.svg deleted file mode 100644 index 30761f3..0000000 --- a/unittests/path-bez-optimize.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/path-command-rewrites.svg b/unittests/path-command-rewrites.svg deleted file mode 100644 index 47ddc61..0000000 --- a/unittests/path-command-rewrites.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/unittests/path-elliptical-flags.svg b/unittests/path-elliptical-flags.svg deleted file mode 100644 index cdf13ba..0000000 --- a/unittests/path-elliptical-flags.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/unittests/path-implicit-line.svg b/unittests/path-implicit-line.svg deleted file mode 100644 index a42848e..0000000 --- a/unittests/path-implicit-line.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-line-optimize.svg b/unittests/path-line-optimize.svg deleted file mode 100644 index 13cc139..0000000 --- a/unittests/path-line-optimize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-no-optimize.svg b/unittests/path-no-optimize.svg deleted file mode 100644 index bda0fff..0000000 --- a/unittests/path-no-optimize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-precision-control-points.svg b/unittests/path-precision-control-points.svg deleted file mode 100644 index add0f58..0000000 --- a/unittests/path-precision-control-points.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/unittests/path-precision.svg b/unittests/path-precision.svg deleted file mode 100644 index 9222ed3..0000000 --- a/unittests/path-precision.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/unittests/path-quad-optimize.svg b/unittests/path-quad-optimize.svg deleted file mode 100644 index bbe3bc9..0000000 --- a/unittests/path-quad-optimize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-simple-triangle.svg b/unittests/path-simple-triangle.svg deleted file mode 100644 index 94ab17e..0000000 --- a/unittests/path-simple-triangle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/unittests/path-sn.svg b/unittests/path-sn.svg deleted file mode 100644 index 0b9f7d2..0000000 --- a/unittests/path-sn.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-truncate-zeros-calc.svg b/unittests/path-truncate-zeros-calc.svg deleted file mode 100644 index c889fff..0000000 --- a/unittests/path-truncate-zeros-calc.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-truncate-zeros.svg b/unittests/path-truncate-zeros.svg deleted file mode 100644 index ad1c6d5..0000000 --- a/unittests/path-truncate-zeros.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-use-scientific-notation.svg b/unittests/path-use-scientific-notation.svg deleted file mode 100644 index afbbf05..0000000 --- a/unittests/path-use-scientific-notation.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/path-with-caps.svg b/unittests/path-with-caps.svg deleted file mode 100644 index 3c24163..0000000 --- a/unittests/path-with-caps.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/unittests/path-with-closepath.svg b/unittests/path-with-closepath.svg deleted file mode 100644 index 80858ca..0000000 --- a/unittests/path-with-closepath.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polygon-coord-neg-first.svg b/unittests/polygon-coord-neg-first.svg deleted file mode 100644 index 9f87a3e..0000000 --- a/unittests/polygon-coord-neg-first.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polygon-coord-neg.svg b/unittests/polygon-coord-neg.svg deleted file mode 100644 index 73fe0b9..0000000 --- a/unittests/polygon-coord-neg.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polygon-coord.svg b/unittests/polygon-coord.svg deleted file mode 100644 index 15940d4..0000000 --- a/unittests/polygon-coord.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polygon.svg b/unittests/polygon.svg deleted file mode 100644 index d927a00..0000000 --- a/unittests/polygon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/polyline-coord-neg-first.svg b/unittests/polyline-coord-neg-first.svg deleted file mode 100644 index 41d1981..0000000 --- a/unittests/polyline-coord-neg-first.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polyline-coord-neg.svg b/unittests/polyline-coord-neg.svg deleted file mode 100644 index da82dad..0000000 --- a/unittests/polyline-coord-neg.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/polyline-coord.svg b/unittests/polyline-coord.svg deleted file mode 100644 index fc209ed..0000000 --- a/unittests/polyline-coord.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/protection.svg b/unittests/protection.svg deleted file mode 100644 index f2930f5..0000000 --- a/unittests/protection.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/unittests/quot-in-url.svg b/unittests/quot-in-url.svg deleted file mode 100644 index 6d82567..0000000 --- a/unittests/quot-in-url.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/unittests/quotes-in-styles.svg b/unittests/quotes-in-styles.svg deleted file mode 100644 index 38a30f2..0000000 --- a/unittests/quotes-in-styles.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/raster-formats.svg b/unittests/raster-formats.svg deleted file mode 100644 index c31b65a..0000000 --- a/unittests/raster-formats.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - Three different formats - - - - \ No newline at end of file diff --git a/unittests/raster-paths-local.svg b/unittests/raster-paths-local.svg deleted file mode 100644 index 61db8ab..0000000 --- a/unittests/raster-paths-local.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - Local files - - - - - - - - Local files (file: protocol) - - - - - - - \ No newline at end of file diff --git a/unittests/raster-paths-remote.svg b/unittests/raster-paths-remote.svg deleted file mode 100644 index ede7783..0000000 --- a/unittests/raster-paths-remote.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - Files from internet - - - - \ No newline at end of file diff --git a/unittests/raster.gif b/unittests/raster.gif deleted file mode 100644 index 6ad1d03..0000000 Binary files a/unittests/raster.gif and /dev/null differ diff --git a/unittests/raster.jpg b/unittests/raster.jpg deleted file mode 100644 index f2a3c4b..0000000 Binary files a/unittests/raster.jpg and /dev/null differ diff --git a/unittests/raster.png b/unittests/raster.png deleted file mode 100644 index 81b33f6..0000000 Binary files a/unittests/raster.png and /dev/null differ diff --git a/unittests/redundant-svg-namespace.svg b/unittests/redundant-svg-namespace.svg deleted file mode 100644 index 1d1dd8d..0000000 --- a/unittests/redundant-svg-namespace.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Test - - - Hallo World - diff --git a/unittests/referenced-elements-1.svg b/unittests/referenced-elements-1.svg deleted file mode 100644 index e779080..0000000 --- a/unittests/referenced-elements-1.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - Fooey - - - - - - - diff --git a/unittests/referenced-font.svg b/unittests/referenced-font.svg deleted file mode 100644 index 7d992ec..0000000 --- a/unittests/referenced-font.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - Text - diff --git a/unittests/refs-in-defs.svg b/unittests/refs-in-defs.svg deleted file mode 100644 index 8636c5a..0000000 --- a/unittests/refs-in-defs.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/unittests/remove-default-attr-order.svg b/unittests/remove-default-attr-order.svg deleted file mode 100644 index 506c9ce..0000000 --- a/unittests/remove-default-attr-order.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/unittests/remove-default-attr-std-deviation.svg b/unittests/remove-default-attr-std-deviation.svg deleted file mode 100644 index ba88368..0000000 --- a/unittests/remove-default-attr-std-deviation.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/unittests/remove-duplicate-gradients-master-without-id.svg b/unittests/remove-duplicate-gradients-master-without-id.svg deleted file mode 100644 index 66727e9..0000000 --- a/unittests/remove-duplicate-gradients-master-without-id.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/unittests/remove-duplicate-gradients.svg b/unittests/remove-duplicate-gradients.svg deleted file mode 100644 index d84c089..0000000 --- a/unittests/remove-duplicate-gradients.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/unittests/remove-unused-attributes-on-parent.svg b/unittests/remove-unused-attributes-on-parent.svg deleted file mode 100644 index 7f68d15..0000000 --- a/unittests/remove-unused-attributes-on-parent.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/unittests/scour-lengths.svg b/unittests/scour-lengths.svg deleted file mode 100644 index f5c0d5c..0000000 --- a/unittests/scour-lengths.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/shorten-ids-stable-output.svg b/unittests/shorten-ids-stable-output.svg deleted file mode 100644 index 6905ec1..0000000 --- a/unittests/shorten-ids-stable-output.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/unittests/shorten-ids.svg b/unittests/shorten-ids.svg deleted file mode 100644 index 7852c57..0000000 --- a/unittests/shorten-ids.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/unittests/sodipodi.svg b/unittests/sodipodi.svg deleted file mode 100644 index 935884a..0000000 --- a/unittests/sodipodi.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/unittests/straight-curve.svg b/unittests/straight-curve.svg deleted file mode 100644 index 95cd862..0000000 --- a/unittests/straight-curve.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/stroke-none.svg b/unittests/stroke-none.svg deleted file mode 100644 index 84f6c66..0000000 --- a/unittests/stroke-none.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/unittests/stroke-nowidth.svg b/unittests/stroke-nowidth.svg deleted file mode 100644 index 2ca5809..0000000 --- a/unittests/stroke-nowidth.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/stroke-transparent.svg b/unittests/stroke-transparent.svg deleted file mode 100644 index 4ff39a2..0000000 --- a/unittests/stroke-transparent.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/style-cdata.svg b/unittests/style-cdata.svg deleted file mode 100644 index 4740da9..0000000 --- a/unittests/style-cdata.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - diff --git a/unittests/style-to-attr.svg b/unittests/style-to-attr.svg deleted file mode 100644 index 3bbe3a0..0000000 --- a/unittests/style-to-attr.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/unittests/style.svg b/unittests/style.svg deleted file mode 100644 index 2148103..0000000 --- a/unittests/style.svg +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/unittests/transform-matrix-is-identity.svg b/unittests/transform-matrix-is-identity.svg deleted file mode 100644 index 9764b28..0000000 --- a/unittests/transform-matrix-is-identity.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/transform-matrix-is-rotate-135.svg b/unittests/transform-matrix-is-rotate-135.svg deleted file mode 100644 index a0583bc..0000000 --- a/unittests/transform-matrix-is-rotate-135.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-rotate-225.svg b/unittests/transform-matrix-is-rotate-225.svg deleted file mode 100644 index 1aa21ef..0000000 --- a/unittests/transform-matrix-is-rotate-225.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-rotate-45.svg b/unittests/transform-matrix-is-rotate-45.svg deleted file mode 100644 index 1749d98..0000000 --- a/unittests/transform-matrix-is-rotate-45.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-rotate-90.svg b/unittests/transform-matrix-is-rotate-90.svg deleted file mode 100644 index 269d526..0000000 --- a/unittests/transform-matrix-is-rotate-90.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-rotate-neg-45.svg b/unittests/transform-matrix-is-rotate-neg-45.svg deleted file mode 100644 index 37b46e8..0000000 --- a/unittests/transform-matrix-is-rotate-neg-45.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-rotate-neg-90.svg b/unittests/transform-matrix-is-rotate-neg-90.svg deleted file mode 100644 index 8fbbd4f..0000000 --- a/unittests/transform-matrix-is-rotate-neg-90.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-scale-2-3.svg b/unittests/transform-matrix-is-scale-2-3.svg deleted file mode 100644 index 7a04ce5..0000000 --- a/unittests/transform-matrix-is-scale-2-3.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/transform-matrix-is-scale-neg-1.svg b/unittests/transform-matrix-is-scale-neg-1.svg deleted file mode 100644 index d402058..0000000 --- a/unittests/transform-matrix-is-scale-neg-1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-matrix-is-translate.svg b/unittests/transform-matrix-is-translate.svg deleted file mode 100644 index 0dfcd9d..0000000 --- a/unittests/transform-matrix-is-translate.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/transform-rotate-fold-3args.svg b/unittests/transform-rotate-fold-3args.svg deleted file mode 100644 index 0139610..0000000 --- a/unittests/transform-rotate-fold-3args.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/transform-rotate-is-identity.svg b/unittests/transform-rotate-is-identity.svg deleted file mode 100644 index 198ba11..0000000 --- a/unittests/transform-rotate-is-identity.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/transform-rotate-trim-range-719.5.svg b/unittests/transform-rotate-trim-range-719.5.svg deleted file mode 100644 index f0bb947..0000000 --- a/unittests/transform-rotate-trim-range-719.5.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/transform-rotate-trim-range-neg-540.0.svg b/unittests/transform-rotate-trim-range-neg-540.0.svg deleted file mode 100644 index 3e857f6..0000000 --- a/unittests/transform-rotate-trim-range-neg-540.0.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/transform-scale-is-identity.svg b/unittests/transform-scale-is-identity.svg deleted file mode 100644 index 037d38a..0000000 --- a/unittests/transform-scale-is-identity.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/transform-skewX-is-identity.svg b/unittests/transform-skewX-is-identity.svg deleted file mode 100644 index b038c6e..0000000 --- a/unittests/transform-skewX-is-identity.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-skewY-is-identity.svg b/unittests/transform-skewY-is-identity.svg deleted file mode 100644 index 27da015..0000000 --- a/unittests/transform-skewY-is-identity.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/unittests/transform-translate-is-identity.svg b/unittests/transform-translate-is-identity.svg deleted file mode 100644 index 6c62d23..0000000 --- a/unittests/transform-translate-is-identity.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/unittests/unreferenced-defs.svg b/unittests/unreferenced-defs.svg deleted file mode 100644 index 2fd8a26..0000000 --- a/unittests/unreferenced-defs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/unittests/unreferenced-font.svg b/unittests/unreferenced-font.svg deleted file mode 100644 index 560c83f..0000000 --- a/unittests/unreferenced-font.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - Text - diff --git a/unittests/unreferenced-linearGradient.svg b/unittests/unreferenced-linearGradient.svg deleted file mode 100644 index f588eac..0000000 --- a/unittests/unreferenced-linearGradient.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/unreferenced-pattern.svg b/unittests/unreferenced-pattern.svg deleted file mode 100644 index 7bcff58..0000000 --- a/unittests/unreferenced-pattern.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/unreferenced-radialGradient.svg b/unittests/unreferenced-radialGradient.svg deleted file mode 100644 index bfa35c8..0000000 --- a/unittests/unreferenced-radialGradient.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/useless-defs.svg b/unittests/useless-defs.svg deleted file mode 100644 index f4663ff..0000000 --- a/unittests/useless-defs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/unittests/viewbox-create.svg b/unittests/viewbox-create.svg deleted file mode 100644 index 0d250db..0000000 --- a/unittests/viewbox-create.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/viewbox-remove.svg b/unittests/viewbox-remove.svg deleted file mode 100644 index 8fa8307..0000000 --- a/unittests/viewbox-remove.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/unittests/whitespace-defs.svg b/unittests/whitespace-defs.svg deleted file mode 100644 index a32fcb4..0000000 --- a/unittests/whitespace-defs.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/unittests/whitespace.svg b/unittests/whitespace.svg deleted file mode 100644 index 2bb48a6..0000000 --- a/unittests/whitespace.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - text1 text2 - text1 text2 - text1 text2 - text1 text2 - text1 text2 - text1 text2 - - - text1 - text2 - text1 - text2 - text1 - text2 - - - text1 text2 - text1 text2 - text1 text2 - text1 text2 - text1 text2 - text1 text2 - - - text1 - text2 - text1 tspan1 text2 - text1 tspan1 tspan2 text2 - - - text1 -text2 - text1tspantext2 - text1 -tspan -text2 - diff --git a/unittests/xml-namespace-attrs.svg b/unittests/xml-namespace-attrs.svg deleted file mode 100644 index 81c5fb4..0000000 --- a/unittests/xml-namespace-attrs.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/unittests/xml-ns-decl.svg b/unittests/xml-ns-decl.svg deleted file mode 100644 index 0f057a7..0000000 --- a/unittests/xml-ns-decl.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - image/svg+xml - - Open Clip Art Logo - 10-01-2004 - - - Andreas Nilsson - - - - - - Jon Phillips, Tobias Jakobs - - - This is one version of the official Open Clip Art Library logo. - logo, open clip art library logo, logotype - - - - - - - - - diff --git a/unittests/xml-space.svg b/unittests/xml-space.svg deleted file mode 100644 index 88a9f50..0000000 --- a/unittests/xml-space.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - Some random text. - \ No newline at end of file diff --git a/unittests/xml-well-formed.svg b/unittests/xml-well-formed.svg deleted file mode 100644 index 5c8d706..0000000 --- a/unittests/xml-well-formed.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - 2 < 5 - Peanut Butter & Jelly - - - - - ΉTML & CSS -