pax_global_header00006660000000000000000000000064131415024770014516gustar00rootroot0000000000000052 comment=e36cd4832a4f8337bef6ca0125bc063da4de3f93 scour-0.36/000077500000000000000000000000001314150247700126015ustar00rootroot00000000000000scour-0.36/.gitignore000066400000000000000000000000641314150247700145710ustar00rootroot00000000000000*.py[cod] *.sublime-workspace build dist *.egg-info scour-0.36/.travis.yml000066400000000000000000000004401314150247700147100ustar00rootroot00000000000000sudo: false language: python python: - pypy - 2.7 - 3.3 - 3.4 - 3.5 - 3.6 install: - pip install tox-travis codecov script: - tox matrix: fast_finish: true include: - python: 3.5 env: - TOXENV=flake8 after_success: - coverage combine && codecovscour-0.36/CONTRIBUTING.md000066400000000000000000000041761314150247700150420ustar00rootroot00000000000000# 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 [`testscour.py`](https://github.com/scour-project/scour/blob/master/testscour.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)scour-0.36/HISTORY.md000066400000000000000000000521461314150247700142740ustar00rootroot00000000000000# Release Notes for Scour ## 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/62)) * 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 shortended 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 analoguous 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 caculations 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 continouous 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 ingoring 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 ingore 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 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/LICENSE����������������������������������������������������������������������������������0000664�0000000�0000000�00000026135�13141502477�0013615�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/Makefile���������������������������������������������������������������������������������0000664�0000000�0000000�00000001167�13141502477�0014246�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������all: clean install 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 python setup.py register python setup.py sdist upload check: test flake8 test: python testscour.py test_version: PYTHONPATH=. python -m scour.scour --version test_help: PYTHONPATH=. python -m scour.scour --help flake8: flake8 --max-line-length=119 coverage: coverage run --source=scour testscour.py coverage html coverage report���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/README.md��������������������������������������������������������������������������������0000664�0000000�0000000�00000004414�13141502477�0014063�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 an SVG optimizer/cleaner that reduces the size of scalable vector graphics by optimizing structure and removing unnecessary data written in Python. 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 renderes 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 agressive cleaning. Scour is open-source and licensed under [Apache License 2.0](https://github.com/codedread/scour/blob/master/LICENSE). 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 Eduard "Ede_123" Braun. ## Installation Scour requires [Python](https://www.python.org) 2.7 or 3.3+. Further, for installation, [pip](https://pip.pypa.io) should be used. To install the [latest release](https://pypi.python.org/pypi/scour) of Scour from PyPI: ```console pip install scour ``` To install the [latest trunk](https://github.com/codedread/scour) version (which might be broken!) from GitHub: ```console pip install https://github.com/codedread/scour/archive/master.zip ``` ## Usage Standard: ```console scour -i input.svg -o output.svg ``` Better (for older versions of Internet Explorer): ```console scour -i input.svg -o output.svg --enable-viewboxing ``` Maximum scrubbing: ```console scour -i input.svg -o output.svg --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` Maximum scrubbing and a compressed SVGZ file: ```console scour -i input.svg -o output.svgz --enable-viewboxing --enable-id-stripping \ --enable-comment-stripping --shorten-ids --indent=none ``` ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/scour.sublime-project��������������������������������������������������������������������0000664�0000000�0000000�00000000716�13141502477�0016766�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "folders": [ { "name": "Scour", "path": ".", "folder_exclude_patterns": ["*.egg-info", "build", "dist"], "file_exclude_patterns": ["*.pyc", "*.pyo", "*.pyd"] } ], "settings": { "default_encoding": "UTF-8", "detect_indentation": false, "ensure_newline_at_eof_on_save": true, "tab_size": 3, "translate_tabs_to_spaces": true, "trim_trailing_white_space_on_save": true, "use_tab_stops": true } } ��������������������������������������������������scour-0.36/scour/�����������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13141502477�0013734�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/scour/__init__.py������������������������������������������������������������������������0000664�0000000�0000000�00000001453�13141502477�0016050�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������############################################################################### # # 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. # ############################################################################### __version__ = u'0.36' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/scour/scour.py���������������������������������������������������������������������������0000664�0000000�0000000�00000501476�13141502477�0015456�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python # -*- coding: utf-8 -*- # Scour # # Copyright 2010 Jeff Schiller # Copyright 2010 Louis Simard # Copyright 2013-2014 Tavendo GmbH # # 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. # Notes: # rubys' path-crunching ideas here: http://intertwingly.net/code/svgtidy/spec.rb # (and implemented here: http://intertwingly.net/code/svgtidy/svgtidy.rb ) # Yet more ideas here: http://wiki.inkscape.org/wiki/index.php/Save_Cleaned_SVG # # * Process Transformations # * 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) # Next Up: # - why are marker-start, -end not removed from the style attribute? # - why are only overflow style properties considered and not attributes? # - only remove unreferenced elements if they are not children of a referenced element # - add an option to remove ids if they match the Inkscape-style of IDs # - investigate point-reducing algorithms # - parse transform attribute # - if a <g> has only one element in it, collapse the <g> (ensure transform, etc are carried down) 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 collections import namedtuple from decimal import Context, Decimal, InvalidOperation, getcontext import six from six.moves import range, urllib 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__ APP = u'scour' VER = __version__ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' # select the most precise walltime measurement function available on the platform if sys.platform.startswith('win'): walltime = time.clock else: walltime = time.time 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', 'ADOBE_ILLUSTRATOR': 'http://ns.adobe.com/AdobeIllustrator/10.0/', 'ADOBE_GRAPHS': 'http://ns.adobe.com/Graphs/1.0/', 'ADOBE_SVG_VIEWER': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', 'ADOBE_VARIABLES': 'http://ns.adobe.com/Variables/1.0/', 'ADOBE_SFW': 'http://ns.adobe.com/SaveForWeb/1.0/', 'ADOBE_EXTENSIBILITY': 'http://ns.adobe.com/Extensibility/1.0/', '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' } 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']] # 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', ] 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)', } # 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 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 # 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', } # @staticmethod 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) 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 # parse out number, exponent and unit 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 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 == 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 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 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 # 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(';') 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) 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][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 is not None: if id in ids: ids[id][0] += 1 ids[id][1].append(node) else: ids[id] = [1, [node]] def removeUnusedDefs(doc, defElem, elemsToRemove=None): if elemsToRemove is None: elemsToRemove = [] 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 == 1 and (elem.getAttribute('id') == '' or elem.getAttribute('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) # 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 _num_elements_removed num = 0 # Remove certain unreferenced elements outside of defs removeTags = ['linearGradient', 'radialGradient', 'pattern'] identifiedElements = findElementsWithId(doc.documentElement) referencedIDs = findReferencedElements(doc.documentElement) 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 _num_elements_removed += 1 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) _num_elements_removed += 1 num += 1 return num 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. Returns the number of bytes saved by shortening ID names in the document. """ num = 0 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] # Add unreferenced IDs to end of idList in arbitrary order idList.extend([rid for rid in unprotectedElements if rid not in idList]) curIdNum = 1 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 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 = '' while idnum > 0: idnum -= 1 rid = chr((idnum % 26) + ord('a')) + rid idnum = int(idnum / 26) 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. Updates identifiedElements and referencedIDs. Does not handle the case where idTo is already the ID name of another element in doc. Returns the number of bytes saved by this replacement. """ num = 0 definingNode = identifiedElements[idFrom] definingNode.setAttribute("id", idTo) del identifiedElements[idFrom] identifiedElements[idTo] = definingNode num += len(idFrom) - len(idTo) # Update references to renamed node referringNodes = referencedIDs.get(idFrom) if referringNodes is not None: # 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 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 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) 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) 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 list(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. Returns the number of ID attributes removed """ global _num_ids_removed keepTags = ['font'] num = 0 for id in list(identifiedElements.keys()): node = identifiedElements[id] if id not in referencedIDs and node.nodeName not in keepTags: node.removeAttribute('id') _num_ids_removed += 1 num += 1 return num def removeNamespacedAttributes(node, namespaces): global _num_attributes_removed num = 0 if node.nodeType == 1: # 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: num += 1 _num_attributes_removed += 1 node.removeAttribute(attrName) # now recurse for children for child in node.childNodes: num += removeNamespacedAttributes(child, namespaces) return num def removeNamespacedElements(node, namespaces): global _num_elements_removed 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 is not None and child.namespaceURI in namespaces: childrenToRemove.append(child) for child in childrenToRemove: num += 1 _num_elements_removed += 1 node.removeChild(child) # now recurse for children for child in node.childNodes: num += removeNamespacedElements(child, namespaces) return num def removeDescriptiveElements(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 global _num_elements_removed num = 0 elementsToRemove = [] for elementType in elementTypes: elementsToRemove.extend(doc.documentElement.getElementsByTagName(elementType)) for element in elementsToRemove: element.parentNode.removeChild(element) num += 1 _num_elements_removed += 1 return num 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 _num_elements_removed 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 == 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) _num_elements_removed += 1 num += 1 # 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 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 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 # for each subsequent child element for childNum in range(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 distinctAttrs = [] # loop through all current 'common' attributes for name in list(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 list(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 def createGroupsForCommonAttributes(elem): """ 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. """ num = 0 global _num_elements_removed # 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) != '' 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 == 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 _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 == 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 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 # 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 # for each child, if at least one child inherits the parent's attribute, then remove for childNum in range(len(childElements)): child = childElements[childNum] inheritedAttrs = [] for name in list(unusedAttrs.keys()): val = child.getAttribute(name) if val == '' or val is 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 list(unusedAttrs.keys()): elem.removeAttribute(name) num += 1 return num def removeDuplicateGradientStops(doc): global _num_elements_removed 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)) 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 _num_elements_removed += 1 # linear gradients return num def collapseSinglyReferencedGradients(doc): global _num_elements_removed num = 0 identifiedElements = findElementsWithId(doc.documentElement) # make sure to reset the ref'ed ids for when we are running this in testscour for rid, nodeCount in six.iteritems(findReferencedElements(doc.documentElement)): 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 is not 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) # 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) # 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 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)) # now remove the xlink:href from refElem refElem.removeAttributeNS(NS['XLINK'], 'href') # now delete elem elem.parentNode.removeChild(elem) _num_elements_removed += 1 num += 1 return num def removeDuplicateGradients(doc): global _num_elements_removed num = 0 gradientsToRemove = {} duplicateToMaster = {} 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 # 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 if someGradAttrsDoNotMatch: continue # 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 range(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 ograd not in gradientsToRemove: if ograd not in duplicateToMaster: if grad not in gradientsToRemove: 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 list(gradientsToRemove.keys()): master_id = masterGrad.getAttribute('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 # for each element that referenced the gradient we are going to replace dup_id with master_id dup_id = dupGrad.getAttribute('id') funcIRI = re.compile('url\\([\'"]?#' + dup_id + '[\'"]?\\)') # matches url(#a), url('#a') and url("#a") for elem in referencedIDs[dup_id][1]: # find out which attribute referenced the duplicate gradient for attr in ['fill', 'stroke']: v = elem.getAttribute(attr) (v_new, n) = funcIRI.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) = funcIRI.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 dupGrad.parentNode.removeChild(dupGrad) _num_elements_removed += 1 num += 1 return num def _getStyle(node): 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 list(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: # 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 # 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 # 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 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 # 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 # 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-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 # 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 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 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 # 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.keys()): if propName in svgAttributes: node.setAttribute(propName, styleMap[propName]) del styleMap[propName] _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 == 9: return None # check styles first (they take precedence over presentation attributes) styles = _getStyle(parentNode) if style in styles.keys(): 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 != 1: 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.keys()) 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 meaningfull and has to be kept # (e.g nodes without children at the end of the DOM tree, text nodes, ...) return True 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. 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 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 # 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, 'linearGradient'), DefaultAttribute('x2', 1, Unit.NONE, '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, 'radialGradient'), DefaultAttribute('r', 0.5, Unit.NONE, 'radialGradient', conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('cx', 50, Unit.PCT, 'radialGradient'), DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('cy', 50, Unit.PCT, 'radialGradient'), DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('spreadMethod', 'pad'), # filter effects 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('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') ] def taint(taintedSet, taintedAttribute): 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 def removeDefaultAttributeValue(node, attribute): """ Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled """ if not node.hasAttribute(attribute.name): return 0 if (attribute.elements is not None) and (node.nodeName not in attribute.elements): return 0 # 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 return 0 def removeDefaultAttributeValues(node, options, tainted=set()): u"""'tainted' keeps a set of attributes defined in parent nodes. For such attributes, we don't delete attributes with default values.""" num = 0 if node.nodeType != 1: return 0 # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) for attribute in default_attributes: num += removeDefaultAttributeValue(node, attribute) # 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 list(default_properties.keys()): 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.keys()): if attribute not in tainted: if attribute in list(default_properties.keys()): if styles[attribute] == default_properties[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 if s in list(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) 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 def convertColors(element): """ Recursively converts all color properties into #RRGGBB format if shorter """ numBytes = 0 if element.nodeType != 1: 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'] # 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 list(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 recurse for our child elements for child in element.childNodes: numBytes += convertColors(child) 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 _num_bytes_saved_in_path_data global _num_path_segments_removed # this gets the parser object from svg_regex.py oldPathStr = element.getAttribute('d') path = svg_parser.parse(oldPathStr) style = _getStyle(element) # 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'] ) # 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': 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 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) # 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 has_round_or_square_linecaps: for pathIndex in range(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:]) _num_path_segments_removed += 1 else: # else skip move coordinate i = 2 while i < len(data): if data[i] == data[i + 1] == 0: del data[i:i + 2] _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] _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] _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] _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]) _num_path_segments_removed += 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: 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 else: newPath.append((prevCmd, prevData)) prevCmd = cmd prevData = data # flush last command and data newPath.append((prevCmd, prevData)) 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]])) _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]])) _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]])) _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]])) _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]])) _num_path_segments_removed += 1 else: j = 0 while j <= 5: curveTuples.append(data[i + j]) j += 1 # 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 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]])) _num_path_segments_removed += 1 else: j = 0 while j <= 3: curveTuples.append(data[i + j]) j += 1 quad_ctl_pt = (data[i + 2] - data[i], data[i + 3] - data[i + 1]) i += 4 if curveTuples: newPath.append(('q', curveTuples)) else: newPath.append((cmd, data)) path = newPath # 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] # 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] _num_path_segments_removed += 1 else: coordIndex += 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] _num_path_segments_removed += 1 else: coordIndex += 2 # 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] _num_path_segments_removed += 1 else: coordIndex += 2 # 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 newPathStr = serializePath(path, options) # 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): _num_bytes_saved_in_path_data += (len(oldPathStr) - len(newPathStr)) element.setAttribute('d', newPathStr) def parseListOfPoints(s): """ Parse string into a list of points. 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.split(r"\s*[\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("-") # 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]) # 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 [] i += 2 return nums def cleanPolygon(elem, options): """ Remove unnecessary closing point of polygon points attribute """ global _num_points_removed_from_polygon 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)) def cleanPolyline(elem, options): """ 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 Returns False if there aren't any Returns a list of bools set to True for coordinates 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 % 6) < 4 for index in indices] elif cmd in ['s', 'q']: # s: (x2 y2 x y)+ q: (x1 y1 x y)+ return [(index % 4) < 2 for index in indices] return False 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, reduce_precision=controlPoints(cmd, data)) 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]) def scourCoordinates(data, options, force_whitespace=False, reduce_precision=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 force_whitespace is True) """ if data is not None: newData = [] c = 0 previousCoord = '' for coord in data: cp = reduce_precision[c] if isinstance(reduce_precision, list) else reduce_precision scouredCoord = scourUnitlessLength(coord, renderer_workaround=options.renderer_workaround, reduce_precision=cp) # 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' if c > 0 and (force_whitespace or scouredCoord[0].isdigit() or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) ): newData.append(' ') # 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, 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: return ''.join(newData) return '' def scourLength(length): """ Scours a length. Accepts units. """ length = SVGLength(length) return scourUnitlessLength(length.value) + Unit.str(length.units) def scourUnitlessLength(length, renderer_workaround=False, reduce_precision=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 # reduce numeric precision # plus() corresponds to the unary prefix plus operator and applies context precision and rounding if reduce_precision: length = scouringContextC.plus(length) else: length = scouringContext.plus(length) # 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() # 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 # 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 sci = six.text_type(length) + 'e' + six.text_type(exponent) if len(sci) < len(nonsci): return_value = sci 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. 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 list(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) for child in element.childNodes: if child.nodeType == 1: 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 def optimizeTransform(transform): """ 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]) # 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 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]) 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] 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 for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: val = element.getAttribute(transformAttr) if val != '': transform = svg_transform_parser.parse(val) optimizeTransform(transform) newVal = serializeTransform(transform) if len(newVal) < len(val): if len(newVal): element.setAttribute(transformAttr, newVal) else: element.removeAttribute(transformAttr) num += len(val) - len(newVal) for child in element.childNodes: if child.nodeType == 1: num += optimizeTransforms(child, options) return num def removeComments(element): """ Removes comments from the element and its children. """ global _num_bytes_saved_in_comments num = 0 if isinstance(element, xml.dom.minidom.Comment): _num_bytes_saved_in_comments += len(element.data) element.parentNode.removeChild(element) num += 1 else: for subelement in element.childNodes[:]: num += removeComments(subelement) return num def embedRasters(element, options): import base64 """ Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers """ global _num_rasters_embedded 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:] # 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) # 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) # 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 # 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) # 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) # 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) # 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 def properlySizeDoc(docElement, options): # 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)): return # else we have a statically sized image and we should try to remedy that # parse viewBox attribute vbSep = re.split('[, ]+', docElement.getAttribute('viewBox')) # 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 # 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 != 1: return if node.prefix == oldprefix: localName = node.localName namespace = node.namespaceURI doc = node.ownerDocument parent = node.parentNode # 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.localName, attr.nodeValue) # 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 # now do all child nodes for child in node.childNodes: remapNamespacePrefix(child, oldprefix, newprefix) def makeWellFormed(str): # Don't escape quotation marks for now as they are fine in text nodes # as well as in attributes if used reciprocally # xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} xml_ents = {'<': '<', '>': '>', '&': '&'} # 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]) # 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, ind=0, preserveWhitespace=False): outParts = [] indent = ind I = '' newline = '' if options.newlines: if options.indent_type == 'tab': I = '\t' elif options.indent_type == 'space': I = ' ' I *= options.indent_depth newline = '\n' outParts.extend([(I * ind), '<', element.nodeName]) # 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]) # now serialize the other attributes known_attr = [ # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html # (but should be kept inuitively ordered) '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', ] attrList = element.attributes attrName2Index = dict([(attrList.item(i).nodeName, i) for i in range(attrList.length)]) # use custom order for known attributes and alphabetical order for the rest attrIndices = [] for name in known_attr: if name in attrName2Index: attrIndices.append(attrName2Index[name]) del attrName2Index[name] attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] for index in attrIndices: attr = attrList.item(index) 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 = "'" attrValue = makeWellFormed(attr.nodeValue) if attr.nodeName == 'style': # sort declarations attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) 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, '=', quot, attrValue, quot]) if attr.nodeName == 'xml:space': if attrValue == 'preserve': preserveWhitespace = True elif attrValue == 'default': preserveWhitespace = False # if no children, self-close children = element.childNodes if children.length > 0: outParts.append('>') 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([newline, 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(newline) else: outParts.append('/>') if indent > 0: outParts.append(newline) 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): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) # default or invalid value if(options.cdigits < 0): options.cdigits = options.digits # 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) # globals for tracking statistics # TODO: get rid of these globals... global _num_elements_removed global _num_attributes_removed global _num_ids_removed global _num_comments_removed global _num_style_properties_fixed global _num_rasters_embedded global _num_path_segments_removed global _num_points_removed_from_polygon global _num_bytes_saved_in_path_data global _num_bytes_saved_in_colors global _num_bytes_saved_in_comments global _num_bytes_saved_in_ids global _num_bytes_saved_in_lengths global _num_bytes_saved_in_transforms _num_elements_removed = 0 _num_attributes_removed = 0 _num_ids_removed = 0 _num_comments_removed = 0 _num_style_properties_fixed = 0 _num_rasters_embedded = 0 _num_path_segments_removed = 0 _num_points_removed_from_polygon = 0 _num_bytes_saved_in_path_data = 0 _num_bytes_saved_in_colors = 0 _num_bytes_saved_in_comments = 0 _num_bytes_saved_in_ids = 0 _num_bytes_saved_in_lengths = 0 _num_bytes_saved_in_transforms = 0 doc = xml.dom.minidom.parseString(in_string) # 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) # remove descriptive elements removeDescriptiveElements(doc, options) # 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 is False: while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: pass while removeNamespacedAttributes(doc.documentElement, unwanted_ns) > 0: pass # 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) for attr in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attr) _num_attributes_removed += 1 # 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? # 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 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) for attrName in xmlnsDeclsToRemove: doc.documentElement.removeAttribute(attrName) _num_attributes_removed += 1 for prefix in redundantPrefixes: remapNamespacePrefix(doc.documentElement, prefix, '') if options.strip_comments: _num_comments_removed = removeComments(doc) if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): doc.documentElement.removeAttribute('xml:space') _num_attributes_removed += 1 # repair style (remove unnecessary style properties and change them into XML attributes) _num_style_properties_fixed = repairStyle(doc.documentElement, options) # convert colors to #RRGGBB format if options.simple_colors: _num_bytes_saved_in_colors = convertColors(doc.documentElement) # remove unreferenced gradients/patterns outside of defs # and most unreferenced elements inside of defs while removeUnreferencedElements(doc, options.keep_defs) > 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 [1, 4, 8]: break elif child.nodeType == 3 and not child.nodeValue.isspace(): break else: removeElem = True if removeElem: elem.parentNode.removeChild(elem) _num_elements_removed += 1 if options.strip_ids: bContinueLooping = True while bContinueLooping: identifiedElements = unprotected_ids(doc, options) referencedIDs = findReferencedElements(doc.documentElement) bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) while removeDuplicateGradientStops(doc) > 0: pass # remove gradients that are only referenced by one other gradient while collapseSinglyReferencedGradients(doc) > 0: pass # remove duplicate gradients while removeDuplicateGradients(doc) > 0: pass # 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) # 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: _num_attributes_removed += moveCommonAttributesToParentGroup(child, referencedIds) # remove unused attributes from parent _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 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: _num_bytes_saved_in_ids += 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))) viewBox = doc.documentElement.getAttribute('viewBox') if viewBox: lengths = re.split('[, ]+', viewBox) lengths = [scourUnitlessLength(length) for length in lengths] doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) # more length scouring in this function _num_bytes_saved_in_lengths = reducePrecision(doc.documentElement) # remove default values of attributes _num_attributes_removed += removeDefaultAttributeValues(doc.documentElement, options) # reduce the length of transformation attributes _num_bytes_saved_in_transforms = 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' # now strip out empty lines lines = [] # Get rid of empty lines for line in out_string.splitlines(True): if line.strip(): lines.append(line) # 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 = "" 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 # used mostly by unit tests # input is a filename # returns the minidom doc representation of the SVG def scourXmlFile(filename, options=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) # open the file and scour it with open(filename, "rb") as f: in_string = f.read() out_string = scourString(in_string, options) # 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: 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. """ 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) # 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) # 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.)") _options_parser.add_option("-i", action="store", dest="infilename", metavar="INPUT.SVG", help="alternative way to specify input filename") _options_parser.add_option("-o", action="store", dest="outfilename", metavar="OUTPUT.SVG", help="alternative way to specify output filename") _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("--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 defsault 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) 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 return [infile, outfile] def getReport(): return ( ' Number of elements removed: ' + str(_num_elements_removed) + os.linesep + ' Number of attributes removed: ' + str(_num_attributes_removed) + os.linesep + ' Number of unreferenced IDs removed: ' + str(_num_ids_removed) + os.linesep + ' Number of comments removed: ' + str(_num_comments_removed) + os.linesep + ' Number of style properties fixed: ' + str(_num_style_properties_fixed) + os.linesep + ' Number of raster images embedded: ' + str(_num_rasters_embedded) + os.linesep + ' Number of path segments reduced/removed: ' + str(_num_path_segments_removed) + os.linesep + ' Number of points removed from polygons: ' + str(_num_points_removed_from_polygon) + os.linesep + ' Number of bytes saved in path data: ' + str(_num_bytes_saved_in_path_data) + os.linesep + ' Number of bytes saved in colors: ' + str(_num_bytes_saved_in_colors) + os.linesep + ' Number of bytes saved in comments: ' + str(_num_bytes_saved_in_comments) + os.linesep + ' Number of bytes saved in IDs: ' + str(_num_bytes_saved_in_ids) + os.linesep + ' Number of bytes saved in lengths: ' + str(_num_bytes_saved_in_lengths) + os.linesep + ' Number of bytes saved in transformations: ' + str(_num_bytes_saved_in_transforms) ) def start(options, input, output): # sanitize options (take missing attributes from defaults, discard unknown attributes) options = sanitizeOptions(options) start = walltime() # do the work in_string = input.read() out_string = scourString(in_string, options).encode("UTF-8") output.write(out_string) # 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() end = walltime() # run-time in ms duration = int(round((end - start) * 1000.)) oldsize = len(in_string) newsize = len(out_string) sizediff = (newsize / oldsize) * 100. 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(getReport(), file=options.ensure_value("stdout", sys.stdout)) def run(): options = parse_args() (input, output) = getInOut(options) start(options, input, output) if __name__ == '__main__': run() ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/scour/svg_regex.py�����������������������������������������������������������������������0000664�0000000�0000000�00000025420�13141502477�0016302�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This software is OSI Certified Open Source Software. # OSI Certified is a certification mark of the Open Source Initiative. # # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of Enthought, Inc. nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ Small hand-written recursive descent parser for SVG <path> data. In [1]: from svg_regex import svg_parser In [3]: svg_parser.parse('M 10,20 30,40V50 60 70') Out[3]: [('M', [(10.0, 20.0), (30.0, 40.0)]), ('V', [50.0, 60.0, 70.0])] In [4]: svg_parser.parse('M 0.6051.5') # An edge case 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 # Sentinel. class _EOF(object): def __repr__(self): return 'EOF' EOF = _EOF() lexicon = [ ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), ('int', r'[-+]?[0-9]+'), ('command', r'[AaCcHhLlMmQqSsTtVvZz]'), ] class Lexer(object): """ Break SVG path data into tokens. The SVG spec requires that tokens are greedy. This lexer relies on Python's regexes defaulting to greediness. This style of implementation was inspired by this article: http://www.gooli.org/blog/a-simple-lexer-in-python/ """ def __init__(self, lexicon): self.lexicon = lexicon parts = [] for name, regex in lexicon: parts.append('(?P<%s>%s)' % (name, regex)) self.regex_string = '|'.join(parts) self.regex = re.compile(self.regex_string) def lex(self, text): """ Yield (token_type, str_data) tokens. The last token will be (EOF, None) where EOF is the singleton object defined in this module. """ for match in self.regex.finditer(text): for name, _ in self.lexicon: m = match.group(name) if m is not None: yield (name, m) break yield (EOF, None) svg_lexer = Lexer(lexicon) class SVGPathParser(object): """ Parse SVG <path> data into a list of commands. Each distinct command will take the form of a tuple (command, data). The `command` is just the character string that starts the command group in the <path> data, so 'M' for absolute moveto, 'm' for relative moveto, 'Z' for closepath, etc. The kind of data it carries with it depends on the command. For 'Z' (closepath), it's just None. The others are lists of individual argument groups. Multiple elements in these lists usually mean to repeat the command. The notable exception is 'M' (moveto) where only the first element is truly a moveto. The remainder are implicit linetos. See the SVG documentation for the interpretation of the individual elements for each command. The main method is `parse(text)`. It can only consume actual strings, not filelike objects or iterators. """ def __init__(self, lexer=svg_lexer): self.lexer = lexer self.command_dispatch = { 'Z': self.rule_closepath, 'z': self.rule_closepath, 'M': self.rule_moveto_or_lineto, 'm': self.rule_moveto_or_lineto, 'L': self.rule_moveto_or_lineto, 'l': self.rule_moveto_or_lineto, 'H': self.rule_orthogonal_lineto, 'h': self.rule_orthogonal_lineto, 'V': self.rule_orthogonal_lineto, 'v': self.rule_orthogonal_lineto, 'C': self.rule_curveto3, 'c': self.rule_curveto3, 'S': self.rule_curveto2, 's': self.rule_curveto2, 'Q': self.rule_curveto2, 'q': self.rule_curveto2, 'T': self.rule_curveto1, 't': self.rule_curveto1, 'A': self.rule_elliptical_arc, 'a': self.rule_elliptical_arc, } # self.number_tokens = set(['int', 'float']) self.number_tokens = list(['int', 'float']) 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) def rule_svg_path(self, next_val_fn, 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) commands.append(command_group) return commands def rule_closepath(self, next_val_fn, token): command = token[1] token = next_val_fn() return (command, []), token def rule_moveto_or_lineto(self, next_val_fn, token): command = token[1] token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: pair, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair) return (command, coordinates), token def rule_orthogonal_lineto(self, next_val_fn, token): command = token[1] token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: coord, token = self.rule_coordinate(next_val_fn, token) coordinates.append(coord) return (command, coordinates), token def rule_curveto3(self, next_val_fn, token): command = token[1] token = next_val_fn() 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) coordinates.extend(pair1) coordinates.extend(pair2) coordinates.extend(pair3) return (command, coordinates), token def rule_curveto2(self, next_val_fn, token): command = token[1] token = next_val_fn() 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) coordinates.extend(pair1) coordinates.extend(pair2) return (command, coordinates), token def rule_curveto1(self, next_val_fn, token): command = token[1] token = next_val_fn() coordinates = [] while token[0] in self.number_tokens: pair1, token = self.rule_coordinate_pair(next_val_fn, token) coordinates.extend(pair1) return (command, coordinates), token def rule_elliptical_arc(self, next_val_fn, token): command = token[1] token = next_val_fn() 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() 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() 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] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) large_arc_flag = Decimal(token[1]) * 1 token = next_val_fn() if token[1] not in ('0', '1'): raise SyntaxError("expecting a boolean flag; got %r" % (token,)) sweep_flag = Decimal(token[1]) * 1 token = next_val_fn() 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() 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() 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): 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() return x, token def rule_coordinate_pair(self, next_val_fn, 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() 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() return [x, y], token svg_parser = SVGPathParser() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/scour/svg_transform.py�������������������������������������������������������������������0000664�0000000�0000000�00000016756�13141502477�0017217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python # -*- coding: utf-8 -*- # SVG transformation list parser # # 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. """ Small recursive descent parser for SVG transform="" data. In [1]: from svg_transform import svg_transform_parser In [3]: svg_transform_parser.parse('translate(50, 50)') Out[3]: [('translate', [50.0, 50.0])] In [4]: svg_transform_parser.parse('translate(50)') Out[4]: [('translate', [50.0])] In [5]: svg_transform_parser.parse('rotate(36 50,50)') Out[5]: [('rotate', [36.0, 50.0, 50.0])] In [6]: svg_transform_parser.parse('rotate(36)') Out[6]: [('rotate', [36.0])] In [7]: svg_transform_parser.parse('skewX(20)') Out[7]: [('skewX', [20.0])] In [8]: svg_transform_parser.parse('skewY(40)') Out[8]: [('skewX', [20.0])] In [9]: svg_transform_parser.parse('scale(2 .5)') Out[9]: [('scale', [2.0, 0.5])] In [10]: svg_transform_parser.parse('scale(.5)') Out[10]: [('scale', [0.5])] In [11]: svg_transform_parser.parse('matrix(1 0 50 0 1 80)') Out[11]: [('matrix', [1.0, 0.0, 50.0, 0.0, 1.0, 80.0])] 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 # Sentinel. class _EOF(object): def __repr__(self): return 'EOF' EOF = _EOF() lexicon = [ ('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'), ('int', r'[-+]?[0-9]+'), ('command', r'(?:matrix|translate|scale|rotate|skew[XY])'), ('coordstart', r'\('), ('coordend', r'\)'), ] class Lexer(object): """ Break SVG path data into tokens. The SVG spec requires that tokens are greedy. This lexer relies on Python's regexes defaulting to greediness. This style of implementation was inspired by this article: http://www.gooli.org/blog/a-simple-lexer-in-python/ """ def __init__(self, lexicon): self.lexicon = lexicon parts = [] for name, regex in lexicon: parts.append('(?P<%s>%s)' % (name, regex)) self.regex_string = '|'.join(parts) self.regex = re.compile(self.regex_string) def lex(self, text): """ Yield (token_type, str_data) tokens. The last token will be (EOF, None) where EOF is the singleton object defined in this module. """ for match in self.regex.finditer(text): for name, _ in self.lexicon: m = match.group(name) if m is not None: yield (name, m) break yield (EOF, None) svg_lexer = Lexer(lexicon) class SVGTransformationParser(object): """ Parse SVG transform="" data into a list of commands. Each distinct command will take the form of a tuple (type, data). The `type` is the character string that defines the type of transformation in the transform data, so either of "translate", "rotate", "scale", "matrix", "skewX" and "skewY". Data is always a list of numbers contained within the transformation's parentheses. See the SVG documentation for the interpretation of the individual elements for each transformation. The main method is `parse(text)`. It can only consume actual strings, not filelike objects or iterators. """ def __init__(self, lexer=svg_lexer): self.lexer = lexer self.command_dispatch = { 'translate': self.rule_1or2numbers, 'scale': self.rule_1or2numbers, 'skewX': self.rule_1number, 'skewY': self.rule_1number, 'rotate': self.rule_1or3numbers, 'matrix': self.rule_6numbers, } # self.number_tokens = set(['int', 'float']) self.number_tokens = list(['int', 'float']) def parse(self, text): """ Parse a string of SVG transform="" data. """ gen = self.lexer.lex(text) next_val_fn = partial(next, *(gen,)) commands = [] token = next_val_fn() while token[0] is not EOF: command, token = self.rule_svg_transform(next_val_fn, token) commands.append(command) return commands def rule_svg_transform(self, next_val_fn, 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() if token[0] != 'coordstart': raise SyntaxError("expecting '('; got %r" % (token,)) numbers, token = rule(next_val_fn, token) if token[0] != 'coordend': raise SyntaxError("expecting ')'; got %r" % (token,)) token = next_val_fn() return (command, numbers), token def rule_1or2numbers(self, next_val_fn, token): numbers = [] # 1st number is mandatory token = next_val_fn() number, token = self.rule_number(next_val_fn, token) numbers.append(number) # 2nd number is optional number, token = self.rule_optional_number(next_val_fn, token) if number is not None: numbers.append(number) return numbers, token def rule_1number(self, next_val_fn, token): # this number is mandatory token = next_val_fn() number, token = self.rule_number(next_val_fn, token) numbers = [number] return numbers, token def rule_1or3numbers(self, next_val_fn, token): numbers = [] # 1st number is mandatory token = next_val_fn() number, token = self.rule_number(next_val_fn, token) numbers.append(number) # 2nd number is optional number, token = self.rule_optional_number(next_val_fn, 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) numbers.append(number) return numbers, token def rule_6numbers(self, next_val_fn, token): numbers = [] token = next_val_fn() # all numbers are mandatory for i in range(6): number, token = self.rule_number(next_val_fn, token) numbers.append(number) return numbers, token def rule_number(self, next_val_fn, 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() return x, token def rule_optional_number(self, next_val_fn, token): if token[0] not in self.number_tokens: return None, token else: x = Decimal(token[1]) * 1 token = next_val_fn() return x, token svg_transform_parser = SVGTransformationParser() ������������������scour-0.36/scour/yocto_css.py�����������������������������������������������������������������������0000664�0000000�0000000�00000005731�13141502477�0016321�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python # -*- coding: utf-8 -*- # yocto-css, an extremely bare minimum CSS parser # # Copyright 2009 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. # In order to resolve Bug 368716 (https://bugs.launchpad.net/scour/+bug/368716) # scour needed a bare-minimum CSS parser in order to determine if some elements # were still referenced by CSS properties. # I looked at css-py (a CSS parser built in Python), but that library # is about 35k of Python and requires ply to be installed. I just need # something very basic to suit scour's needs. # yocto-css takes a string of CSS and tries to spit out a list of rules # A rule is an associative array (dictionary) with the following keys: # - selector: contains the string of the selector (see CSS grammar) # - properties: contains an associative array of CSS properties for this rule # TODO: need to build up some unit tests for yocto_css # stylesheet : [ CDO | CDC | S | statement ]*; # statement : ruleset | at-rule; # at-rule : ATKEYWORD S* any* [ block | ';' S* ]; # block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; # ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; # selector : any+; # declaration : property S* ':' S* value; # property : IDENT; # value : [ any | block | ATKEYWORD S* ]+; # any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING # | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES # | 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 ���������������������������������������scour-0.36/setup.py���������������������������������������������������������������������������������0000664�0000000�0000000�00000006156�13141502477�0014323�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������############################################################################### # # 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 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 renderes 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 agressive cleaning. Website - http://www.codedread.com/scour/ (original website) - https://github.com/scour-project/scour (today) Authors: - Jeff Schiller, Louis Simard (original authors) - Tobias Oberstein (maintainer) - Eduard Braun (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' ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/testcss.py�������������������������������������������������������������������������������0000775�0000000�0000000�00000003627�13141502477�0014656�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/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() ���������������������������������������������������������������������������������������������������������scour-0.36/testscour.py�����������������������������������������������������������������������������0000775�0000000�0000000�00000332471�13141502477�0015223�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/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 makeWellFormed, parse_args, scourString, scourXmlFile, start, run 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: 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: 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: 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: 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 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 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 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') 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 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(' This is some messed-up markup '''.splitlines() for i in range(4): self.assertEqual(s[i], c[i], 'Whitespace not preserved for line ' + str(i)) class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): def runTest(self): with open('unittests/whitespace-nested.svg') as f: s = scourString(f.read()).splitlines() c = ''' Use bold text '''.splitlines() for i in range(4): self.assertEqual(s[i], c[i], 'Whitespace not preserved when nested for line ' + str(i)) 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/whitespace-important.svg') as f: s = scourString(f.read()) self.assertEqual(len(s.splitlines()), 4, 'Did not output line ending character correctly') class XmlEntities(unittest.TestCase): def runTest(self): self.assertEqual(makeWellFormed('<>&'), '<>&', 'Incorrectly translated XML entities') 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 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 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 PathEmptyMove(unittest.TestCase): def runTest(self): doc = scourXmlFile('unittests/path-empty-move.svg') self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100z') self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') 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 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__('testcss') scour = __import__('__main__') suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) unittest.main(defaultTest="suite") �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/tox.ini����������������������������������������������������������������������������������0000664�0000000�0000000�00000000450�13141502477�0014113�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[tox] envlist = pypy py27 py33 py34 py35 py36 flake8 [testenv] deps = six coverage commands = scour --version coverage run --parallel-mode --source=scour testscour.py [testenv:flake8] deps = flake8 commands = flake8 --max-line-length=119������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/�������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13141502477�0014643�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/adobe.svg����������������������������������������������������������������������0000664�0000000�0000000�00000003072�13141502477�0016440�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/cascading-default-attribute-removal.svg����������������������������������������0000664�0000000�0000000�00000002430�13141502477�0024365�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/cdata.svg����������������������������������������������������������������������0000664�0000000�0000000�00000000270�13141502477�0016437�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/collapse-gradients-gradientUnits.svg�������������������������������������������0000664�0000000�0000000�00000000773�13141502477�0023771�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> �����scour-0.36/unittests/collapse-gradients.svg���������������������������������������������������������0000664�0000000�0000000�00000001043�13141502477�0021142�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/collapse-same-path-points.svg��������������������������������������������������0000664�0000000�0000000�00000000431�13141502477�0022353�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/collapse-straight-path-segments.svg��������������������������������������������0000664�0000000�0000000�00000003176�13141502477�0023575�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/color-formats.svg��������������������������������������������������������������0000664�0000000�0000000�00000001115�13141502477�0020151�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/comment-beside-xml-decl.svg����������������������������������������������������0000664�0000000�0000000�00000000700�13141502477�0021757�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ����������������������������������������������������������������scour-0.36/unittests/comments.svg�������������������������������������������������������������������0000664�0000000�0000000�00000000171�13141502477�0017210�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" ?> <!-- Empty --> <!-- Comment #2 --> <svg xmlns="http://www.w3.org/2000/svg"> </svg> <!-- After --> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/commonized-referenced-elements.svg���������������������������������������������0000664�0000000�0000000�00000000504�13141502477�0023441�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<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> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/css-reference.svg��������������������������������������������������������������0000664�0000000�0000000�00000001263�13141502477�0020112�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������scour-0.36/unittests/descriptive-elements-with-text.svg���������������������������������������������0000664�0000000�0000000�00000000477�13141502477�0023462�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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 scour-0.36/unittests/doctype.svg000066400000000000000000000005361314150247700170370ustar00rootroot00000000000000 ]> scour-0.36/unittests/dont-collapse-gradients.svg000066400000000000000000000011271314150247700221070ustar00rootroot00000000000000 scour-0.36/unittests/dont-convert-short-color-names.svg000066400000000000000000000002251314150247700233570ustar00rootroot00000000000000 scour-0.36/unittests/duplicate-gradient-stops-pct.svg000066400000000000000000000007611314150247700230670ustar00rootroot00000000000000 scour-0.36/unittests/duplicate-gradient-stops.svg000066400000000000000000000013441314150247700223010ustar00rootroot00000000000000 scour-0.36/unittests/duplicate-gradients-update-style.svg000066400000000000000000000014241314150247700237330ustar00rootroot00000000000000 scour-0.36/unittests/empty-descriptive-elements.svg000066400000000000000000000001521314150247700226510ustar00rootroot00000000000000 scour-0.36/unittests/empty-g.svg000066400000000000000000000003131314150247700167430ustar00rootroot00000000000000 scour-0.36/unittests/empty-style.svg000066400000000000000000000001411314150247700176540ustar00rootroot00000000000000 scour-0.36/unittests/encoding-iso-8859-15.svg000066400000000000000000000002131314150247700205740ustar00rootroot00000000000000 ߤ scour-0.36/unittests/encoding-utf8.svg000066400000000000000000000013331314150247700200360ustar00rootroot00000000000000 Hello in many languages: ar: أهلا bn: হ্যালো el: Χαίρετε en: Hello hi: नमस्ते iw: שלום ja: こんにちは km: ជំរាបសួរ ml: ഹലോ ru: Здравствуйте ur: ہیلو zh: 您好 “”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿� :-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔ ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ scour-0.36/unittests/fill-none.svg000066400000000000000000000006561314150247700172560ustar00rootroot00000000000000 scour-0.36/unittests/flowtext-less.svg000066400000000000000000000036701314150247700202120ustar00rootroot00000000000000 image/svg+xml abcd scour-0.36/unittests/flowtext.svg000066400000000000000000000050361314150247700172440ustar00rootroot00000000000000 image/svg+xml sfdadasdasdasdadsa abcd scour-0.36/unittests/font-styles.svg000066400000000000000000000002421314150247700176510ustar00rootroot00000000000000 scour-0.36/unittests/full-descriptive-elements.svg000066400000000000000000000023061314150247700224600ustar00rootroot00000000000000 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 scour-0.36/unittests/gradient-default-attrs.svg000066400000000000000000000020071314150247700217350ustar00rootroot00000000000000 scour-0.36/unittests/group-creation.svg000066400000000000000000000004751314150247700203300ustar00rootroot00000000000000 scour-0.36/unittests/group-no-creation-tspan.svg000066400000000000000000000005361314150247700220630ustar00rootroot00000000000000 text1 text2 text3 scour-0.36/unittests/group-no-creation.svg000066400000000000000000000005011314150247700207300ustar00rootroot00000000000000 scour-0.36/unittests/groups-in-switch-with-id.svg000066400000000000000000000011411314150247700221460ustar00rootroot00000000000000 scour-0.36/unittests/groups-in-switch.svg000066400000000000000000000011231314150247700206030ustar00rootroot00000000000000 scour-0.36/unittests/groups-with-title-desc.svg000066400000000000000000000005551314150247700217140ustar00rootroot00000000000000 Group 1 Group 1 scour-0.36/unittests/ids-protect.svg000066400000000000000000000005461314150247700176260ustar00rootroot00000000000000 Text 1 Text 2 Text 3 Text custom My text scour-0.36/unittests/ids-to-strip.svg000066400000000000000000000005341314150247700177240ustar00rootroot00000000000000 Fooey scour-0.36/unittests/ids.svg000066400000000000000000000006511314150247700161450ustar00rootroot00000000000000 scour-0.36/unittests/important-groups-in-defs.svg000066400000000000000000000004611314150247700222420ustar00rootroot00000000000000 scour-0.36/unittests/inkscape.svg000066400000000000000000000010411314150247700171550ustar00rootroot00000000000000 scour-0.36/unittests/minimal.svg000066400000000000000000000001211314150247700170040ustar00rootroot00000000000000 scour-0.36/unittests/move-common-attributes-to-grandparent.svg000066400000000000000000000005601314150247700247300ustar00rootroot00000000000000 scour-0.36/unittests/move-common-attributes-to-parent.svg000066400000000000000000000006641314150247700237210ustar00rootroot00000000000000 Hello World! Goodbye Cruel World! scour-0.36/unittests/nested-defs.svg000066400000000000000000000013341314150247700175660ustar00rootroot00000000000000 scour-0.36/unittests/nested-useless-groups.svg000066400000000000000000000003751314150247700216510ustar00rootroot00000000000000 scour-0.36/unittests/no-collapse-lines.svg000066400000000000000000000006371314150247700207160ustar00rootroot00000000000000 scour-0.36/unittests/orient-marker.svg000066400000000000000000000006361314150247700201500ustar00rootroot00000000000000 scour-0.36/unittests/overflow-marker.svg000066400000000000000000000006661314150247700205160ustar00rootroot00000000000000 scour-0.36/unittests/overflow-svg.svg000066400000000000000000000005301314150247700200220ustar00rootroot00000000000000 scour-0.36/unittests/path-abs-to-rel.svg000066400000000000000000000006401314150247700202630ustar00rootroot00000000000000 scour-0.36/unittests/path-bez-optimize.svg000066400000000000000000000007071314150247700207400ustar00rootroot00000000000000 scour-0.36/unittests/path-empty-move.svg000066400000000000000000000002301314150247700204130ustar00rootroot00000000000000 scour-0.36/unittests/path-implicit-line.svg000066400000000000000000000003011314150247700210470ustar00rootroot00000000000000 scour-0.36/unittests/path-line-optimize.svg000066400000000000000000000003131314150247700211000ustar00rootroot00000000000000 scour-0.36/unittests/path-no-optimize.svg000066400000000000000000000004231314150247700205670ustar00rootroot00000000000000 scour-0.36/unittests/path-precision-control-points.svg000066400000000000000000000007171314150247700233060ustar00rootroot00000000000000 scour-0.36/unittests/path-precision.svg000066400000000000000000000010051314150247700203050ustar00rootroot00000000000000 scour-0.36/unittests/path-quad-optimize.svg000066400000000000000000000003441314150247700211070ustar00rootroot00000000000000 scour-0.36/unittests/path-simple-triangle.svg000066400000000000000000000003361314150247700214140ustar00rootroot00000000000000 scour-0.36/unittests/path-sn.svg000066400000000000000000000002371314150247700167400ustar00rootroot00000000000000 scour-0.36/unittests/path-truncate-zeros-calc.svg000066400000000000000000000003071314150247700222030ustar00rootroot00000000000000 scour-0.36/unittests/path-truncate-zeros.svg000066400000000000000000000002431314150247700213020ustar00rootroot00000000000000 scour-0.36/unittests/path-use-scientific-notation.svg000066400000000000000000000002211314150247700230540ustar00rootroot00000000000000 scour-0.36/unittests/path-with-caps.svg000066400000000000000000000010121314150247700202070ustar00rootroot00000000000000 scour-0.36/unittests/path-with-closepath.svg000066400000000000000000000002421314150247700212470ustar00rootroot00000000000000 scour-0.36/unittests/polygon-coord-neg-first.svg000066400000000000000000000002341314150247700220520ustar00rootroot00000000000000 scour-0.36/unittests/polygon-coord-neg.svg000066400000000000000000000002331314150247700207240ustar00rootroot00000000000000 scour-0.36/unittests/polygon-coord.svg000066400000000000000000000002221314150247700201530ustar00rootroot00000000000000 scour-0.36/unittests/polygon.svg000066400000000000000000000003651314150247700170570ustar00rootroot00000000000000 scour-0.36/unittests/polyline-coord-neg-first.svg000066400000000000000000000002351314150247700222170ustar00rootroot00000000000000 scour-0.36/unittests/polyline-coord-neg.svg000066400000000000000000000002341314150247700210710ustar00rootroot00000000000000 scour-0.36/unittests/polyline-coord.svg000066400000000000000000000002231314150247700203200ustar00rootroot00000000000000 scour-0.36/unittests/protection.svg000066400000000000000000000006231314150247700175530ustar00rootroot00000000000000 scour-0.36/unittests/quot-in-url.svg000066400000000000000000000005641314150247700175650ustar00rootroot00000000000000 scour-0.36/unittests/raster-formats.svg000066400000000000000000000007071314150247700203410ustar00rootroot00000000000000 Three different formats scour-0.36/unittests/raster-paths-local.svg000066400000000000000000000022551314150247700210750ustar00rootroot00000000000000 Local files Local files (file: protocol) scour-0.36/unittests/raster-paths-remote.svg000066400000000000000000000007661314150247700213030ustar00rootroot00000000000000 Files from internet scour-0.36/unittests/raster.gif000066400000000000000000000000511314150247700166260ustar00rootroot00000000000000GIF87a,P;scour-0.36/unittests/raster.jpg000066400000000000000000000005361314150247700166510ustar00rootroot00000000000000JFIFHHCreated with GIMPCC 7w  89vx ?1횏{+z.L`scour-0.36/unittests/raster.png000066400000000000000000000001301314150247700166430ustar00rootroot00000000000000PNG  IHDRf' PLTEfN IDATcHbaaIENDB`scour-0.36/unittests/redundant-svg-namespace.svg000066400000000000000000000005131314150247700220760ustar00rootroot00000000000000 Test scour-0.36/unittests/referenced-elements-1.svg000066400000000000000000000005231314150247700214360ustar00rootroot00000000000000 Fooey scour-0.36/unittests/referenced-font.svg000066400000000000000000000013101314150247700204250ustar00rootroot00000000000000 Text scour-0.36/unittests/refs-in-defs.svg000066400000000000000000000005321314150247700176460ustar00rootroot00000000000000 scour-0.36/unittests/remove-duplicate-gradients.svg000066400000000000000000000025531314150247700226140ustar00rootroot00000000000000 scour-0.36/unittests/remove-unused-attributes-on-parent.svg000066400000000000000000000005101314150247700242430ustar00rootroot00000000000000 scour-0.36/unittests/scour-lengths.svg000066400000000000000000000004501314150247700201600ustar00rootroot00000000000000 scour-0.36/unittests/shorten-ids.svg000066400000000000000000000007001314150247700176200ustar00rootroot00000000000000 scour-0.36/unittests/sodipodi.svg000066400000000000000000000010341314150247700171740ustar00rootroot00000000000000 scour-0.36/unittests/straight-curve.svg000066400000000000000000000002401314150247700203270ustar00rootroot00000000000000 scour-0.36/unittests/stroke-none.svg000066400000000000000000000017431314150247700176350ustar00rootroot00000000000000 scour-0.36/unittests/stroke-nowidth.svg000066400000000000000000000006011314150247700203420ustar00rootroot00000000000000 scour-0.36/unittests/stroke-transparent.svg000066400000000000000000000006031314150247700212310ustar00rootroot00000000000000 scour-0.36/unittests/style-cdata.svg000066400000000000000000000007651314150247700176060ustar00rootroot00000000000000 scour-0.36/unittests/style-to-attr.svg000066400000000000000000000003751314150247700201210ustar00rootroot00000000000000 scour-0.36/unittests/style.svg000066400000000000000000000002441314150247700165240ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-identity.svg000066400000000000000000000002421314150247700231370ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-135.svg000066400000000000000000000004451314150247700231170ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-225.svg000066400000000000000000000004451314150247700231170ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-45.svg000066400000000000000000000004431314150247700230350ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-90.svg000066400000000000000000000003331314150247700230330ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-neg-45.svg000066400000000000000000000004431314150247700236040ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-rotate-neg-90.svg000066400000000000000000000003331314150247700236020ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-scale-2-3.svg000066400000000000000000000002441314150247700226760ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-scale-neg-1.svg000066400000000000000000000003351314150247700233050ustar00rootroot00000000000000 scour-0.36/unittests/transform-matrix-is-translate.svg000066400000000000000000000002421314150247700233030ustar00rootroot00000000000000 scour-0.36/unittests/transform-rotate-fold-3args.svg000066400000000000000000000004431314150247700226330ustar00rootroot00000000000000 scour-0.36/unittests/transform-rotate-is-identity.svg000066400000000000000000000002311314150247700231270ustar00rootroot00000000000000 scour-0.36/unittests/transform-rotate-trim-range-719.5.svg000066400000000000000000000004411314150247700234160ustar00rootroot00000000000000 scour-0.36/unittests/transform-rotate-trim-range-neg-540.0.svg000066400000000000000000000004541314150247700241540ustar00rootroot00000000000000 scour-0.36/unittests/transform-skewX-is-identity.svg000066400000000000000000000003421314150247700227350ustar00rootroot00000000000000 scour-0.36/unittests/transform-skewY-is-identity.svg000066400000000000000000000003421314150247700227360ustar00rootroot00000000000000 scour-0.36/unittests/transform-translate-is-identity.svg000066400000000000000000000004541314150247700236350ustar00rootroot00000000000000 scour-0.36/unittests/unreferenced-defs.svg000066400000000000000000000013311314150247700207460ustar00rootroot00000000000000 scour-0.36/unittests/unreferenced-font.svg000066400000000000000000000012721314150247700207770ustar00rootroot00000000000000 Text scour-0.36/unittests/unreferenced-linearGradient.svg000066400000000000000000000003261314150247700227600ustar00rootroot00000000000000 scour-0.36/unittests/unreferenced-pattern.svg000066400000000000000000000003561314150247700215100ustar00rootroot00000000000000 scour-0.36/unittests/unreferenced-radialGradient.svg000066400000000000000000000003131314150247700227360ustar00rootroot00000000000000 scour-0.36/unittests/useless-defs.svg000066400000000000000000000015051314150247700177670ustar00rootroot00000000000000 scour-0.36/unittests/viewbox-create.svg000066400000000000000000000001701314150247700203060ustar00rootroot00000000000000 scour-0.36/unittests/viewbox-remove.svg000066400000000000000000000002261314150247700203420ustar00rootroot00000000000000 scour-0.36/unittests/whitespace-defs.svg000066400000000000000000000002511314150247700204350ustar00rootroot00000000000000 scour-0.36/unittests/whitespace-important.svg000066400000000000000000000003131314150247700215300ustar00rootroot00000000000000 This is some messed-up markup scour-0.36/unittests/whitespace-nested.svg000066400000000000000000000003341314150247700210000ustar00rootroot00000000000000 Use bold text scour-0.36/unittests/xml-namespace-attrs.svg000066400000000000000000000024231314150247700212520ustar00rootroot00000000000000 scour-0.36/unittests/xml-ns-decl.svg000066400000000000000000000032231314150247700175070ustar00rootroot00000000000000 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 scour-0.36/unittests/xml-space.svg000066400000000000000000000003221314150247700172520ustar00rootroot00000000000000 Some random text. scour-0.36/unittests/xml-well-formed.svg000066400000000000000000000007411314150247700204010ustar00rootroot00000000000000 2 < 5 Peanut Butter & Jelly ΉTML & CSS