diff -Nru scour-0.37/CONTRIBUTING.md scour-0.38.2/CONTRIBUTING.md --- scour-0.37/CONTRIBUTING.md 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/CONTRIBUTING.md 2020-11-22 14:05:13.000000000 +0000 @@ -23,7 +23,7 @@ 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)). +These tests are run automatically on all PRs using [TravisCI](https://travis-ci.org/scour-project/scour) and have to pass at all times! When you add new functionality you should always include suitable tests with your PR (see [`test_scour.py`](https://github.com/scour-project/scour/blob/master/test_scour.py)). ### Coverage diff -Nru scour-0.37/debian/changelog scour-0.38.2/debian/changelog --- scour-0.37/debian/changelog 2019-12-17 12:45:53.000000000 +0000 +++ scour-0.38.2/debian/changelog 2020-12-03 05:42:22.000000000 +0000 @@ -1,8 +1,24 @@ -scour (0.37-4build1) focal; urgency=medium +scour (0.38.2-1) unstable; urgency=medium - * No-change rebuild to generate dependencies on python2. + [ Martin Pitt ] + * New upstream version 0.38.2. (Closes: #975648) + * Bump Standards-Version to 4.5.1. No changes necessary. - -- Matthias Klose Tue, 17 Dec 2019 12:45:53 +0000 + [ Debian Janitor ] + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, Repository-Browse. + Changes-By: lintian-brush + Fixes: lintian: upstream-metadata-file-is-missing + See-also: https://lintian.debian.org/tags/upstream-metadata-file-is-missing.html + Fixes: lintian: upstream-metadata-missing-bug-tracking + See-also: https://lintian.debian.org/tags/upstream-metadata-missing-bug-tracking.html + Fixes: lintian: upstream-metadata-missing-repository + See-also: https://lintian.debian.org/tags/upstream-metadata-missing-repository.html + * Fix field name typo in debian/copyright (X-Comment => Comment). + Changes-By: lintian-brush + Fixes: lintian: field-name-typo-in-dep5-copyright + See-also: https://lintian.debian.org/tags/field-name-typo-in-dep5-copyright.html + + -- Martin Pitt Thu, 03 Dec 2020 06:42:22 +0100 scour (0.37-4) unstable; urgency=medium diff -Nru scour-0.37/debian/control scour-0.38.2/debian/control --- scour-0.37/debian/control 2019-10-30 22:43:38.000000000 +0000 +++ scour-0.38.2/debian/control 2020-12-03 05:32:01.000000000 +0000 @@ -8,7 +8,7 @@ python3-setuptools, python3-six, Build-Depends-Indep: perl -Standards-Version: 4.4.1 +Standards-Version: 4.5.1 Homepage: https://github.com/codedread/scour Vcs-Git: https://salsa.debian.org/debian/scour.git Vcs-Browser: https://salsa.debian.org/debian/scour diff -Nru scour-0.37/debian/copyright scour-0.38.2/debian/copyright --- scour-0.37/debian/copyright 2019-10-30 22:43:38.000000000 +0000 +++ scour-0.38.2/debian/copyright 2020-11-26 09:39:17.000000000 +0000 @@ -55,5 +55,5 @@ 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. -X-Comment: On Debian systems, the complete text of the license can be +Comment: On Debian systems, the complete text of the license can be found in `/usr/share/common-licenses/Apache-2.0'. diff -Nru scour-0.37/debian/rules scour-0.38.2/debian/rules --- scour-0.37/debian/rules 2019-10-30 22:43:38.000000000 +0000 +++ scour-0.38.2/debian/rules 2020-12-03 05:39:36.000000000 +0000 @@ -14,6 +14,6 @@ override_dh_auto_test: for p in $$(py3versions -s); do \ echo "Running tests with $$p..."; \ - $$p ./testcss.py; \ - $$p ./testscour.py; \ + $$p ./test_css.py; \ + $$p ./test_scour.py; \ done diff -Nru scour-0.37/debian/upstream/metadata scour-0.38.2/debian/upstream/metadata --- scour-0.37/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/debian/upstream/metadata 2020-11-26 09:39:17.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/codedread/scour/issues +Bug-Submit: https://github.com/codedread/scour/issues/new +Repository: https://github.com/codedread/scour.git +Repository-Browse: https://github.com/codedread/scour diff -Nru scour-0.37/HISTORY.md scour-0.38.2/HISTORY.md --- scour-0.37/HISTORY.md 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/HISTORY.md 2020-11-22 14:05:13.000000000 +0000 @@ -1,5 +1,21 @@ # Release Notes for Scour +## Version 0.38.2 (2020-11-22) +* Fix another regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) + +## Version 0.38.1 (2020-09-02) +* Fix regression caused by new feature to merge sibling groups ([#260](https://github.com/scour-project/scour/issues/260)) + +## Version 0.38 (2020-08-06) +* Fix issue with dropping xlink:href attribute when collapsing referenced gradients ([#206](https://github.com/scour-project/scour/pull/206)) +* Fix issue with dropping ID while de-duplicating gradients ([#207](https://github.com/scour-project/scour/pull/207)) +* Improve `--shorten-ids` so it re-maps IDs that are already used in the document if they're shorter ([#187](https://github.com/scour-project/scour/pull/187)) +* Fix whitespace handling for SVG 1.2 flowed text ([#235](https://github.com/scour-project/scour/issues/235)) +* Improvement: Merge sibling `` nodes with identical attributes ([#208](https://github.com/scour-project/scour/pull/208)) +* Improve performance of XML serialization ([#247](https://github.com/scour-project/scour/pull/247)) +* Improve performance of gradient de-duplication ([#248](https://github.com/scour-project/scour/pull/248)) +* Some general performance improvements ([#249](https://github.com/scour-project/scour/pull/249)) + ## Version 0.37 (2018-07-04) * Fix escaping of quotes in attribute values. ([#152](https://github.com/scour-project/scour/pull/152)) * A lot of performance improvements making processing significantly faster in many cases. ([#167](https://github.com/scour-project/scour/pull/167), [#169](https://github.com/scour-project/scour/pull/169), [#171](https://github.com/scour-project/scour/pull/171), [#185](https://github.com/scour-project/scour/pull/185)) diff -Nru scour-0.37/Makefile scour-0.38.2/Makefile --- scour-0.37/Makefile 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/Makefile 2020-11-22 14:05:13.000000000 +0000 @@ -22,7 +22,7 @@ test: - python testscour.py + python test_scour.py test_version: PYTHONPATH=. python -m scour.scour --version @@ -34,6 +34,6 @@ flake8 --max-line-length=119 coverage: - coverage run --source=scour testscour.py + coverage run --source=scour test_scour.py coverage html - coverage report \ No newline at end of file + coverage report diff -Nru scour-0.37/README.md scour-0.38.2/README.md --- scour-0.37/README.md 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/README.md 2020-11-22 14:05:13.000000000 +0000 @@ -16,7 +16,7 @@ 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. +The project moved to GitLab in 2013 an is now maintained by Tobias "oberstet" Oberstein and Patrick "Ede_123" Storz. ## Installation diff -Nru scour-0.37/scour/__init__.py scour-0.38.2/scour/__init__.py --- scour-0.37/scour/__init__.py 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/scour/__init__.py 2020-11-22 14:05:13.000000000 +0000 @@ -16,4 +16,4 @@ # ############################################################################### -__version__ = u'0.37' +__version__ = u'0.38.2' diff -Nru scour-0.37/scour/scour.py scour-0.38.2/scour/scour.py --- scour-0.37/scour/scour.py 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/scour/scour.py 2020-11-22 14:05:13.000000000 +0000 @@ -74,6 +74,15 @@ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' +XML_ENTS_NO_QUOTES = {'<': '<', '>': '>', '&': '&'} +XML_ENTS_ESCAPE_APOS = XML_ENTS_NO_QUOTES.copy() +XML_ENTS_ESCAPE_APOS["'"] = ''' +XML_ENTS_ESCAPE_QUOT = XML_ENTS_NO_QUOTES.copy() +XML_ENTS_ESCAPE_QUOT['"'] = '"' + +# Used to split values where "x y" or "x,y" or a mix of the two is allowed +RE_COMMA_WSP = re.compile(r"\s*[\s,]\s*") + NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -547,7 +556,7 @@ Returns IDs of all referenced elements - node is the node at which to start the search. - returns a map which has the id as key and - each value is is a list of nodes + each value is is a set of nodes Currently looks at 'xlink:href' and all attributes in 'referencingProps' """ @@ -562,7 +571,7 @@ # 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]) + stylesheet = "".join(child.nodeValue for child in node.childNodes) if stylesheet != '': cssRules = parseCssString(stylesheet) for rule in cssRules: @@ -577,9 +586,9 @@ # we remove the hash mark from the beginning of the id id = href[1:] if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} # now get all style properties and the fill, stroke, filter attributes styles = node.getAttribute('style').split(';') @@ -610,9 +619,9 @@ if len(val) >= 7 and val[0:5] == 'url(#': id = val[5:val.find(')')] if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} # if the url has a quote in it, we need to compensate elif len(val) >= 8: id = None @@ -624,9 +633,9 @@ id = val[6:val.find("')")] if id is not None: if id in ids: - ids[id].append(node) + ids[id].add(node) else: - ids[id] = [node] + ids[id] = {node} def removeUnusedDefs(doc, defElem, elemsToRemove=None, referencedIDs=None): @@ -641,8 +650,12 @@ keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] for elem in defElem.childNodes: # only look at it if an element and not referenced anywhere else - if elem.nodeType == Node.ELEMENT_NODE and (elem.getAttribute('id') == '' or - elem.getAttribute('id') not in referencedIDs): + if elem.nodeType != Node.ELEMENT_NODE: + continue + + elem_id = elem.getAttribute('id') + + if elem_id == '' or elem_id not in referencedIDs: # we only inspect the children of a group in a defs if the group # is not referenced anywhere else if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: @@ -668,6 +681,16 @@ identifiedElements = findElementsWithId(doc.documentElement) referencedIDs = findReferencedElements(doc.documentElement) + if not keepDefs: + # Remove most unreferenced elements inside defs + defs = doc.documentElement.getElementsByTagName('defs') + for aDef in defs: + elemsToRemove = removeUnusedDefs(doc, aDef, referencedIDs=referencedIDs) + for elem in elemsToRemove: + elem.parentNode.removeChild(elem) + _num_elements_removed += 1 + num += 1 + for id in identifiedElements: if id not in referencedIDs: goner = identifiedElements[id] @@ -678,31 +701,23 @@ 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): +def shortenIDs(doc, prefix, options): """ 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 + # This map contains maps the (original) ID to the nodes referencing it. + # At the end of this function, it will no longer be valid and while we + # could keep it up to date, it will complicate the code for no gain + # (as we do not reuse the data structure beyond this function). referencedIDs = findReferencedElements(doc.documentElement) # Make idList (list of idnames) sorted by reference count @@ -710,31 +725,99 @@ # First check that there's actually a defining element for the current ID name. # (Cyn: I've seen documents with #id references but no element with that ID!) idList = [(len(referencedIDs[rid]), rid) for rid in referencedIDs - if rid in unprotectedElements] + if rid in identifiedElements] idList.sort(reverse=True) idList = [rid for count, rid in idList] # Add unreferenced IDs to end of idList in arbitrary order - idList.extend([rid for rid in unprotectedElements if rid not in idList]) + idList.extend([rid for rid in identifiedElements if rid not in idList]) + # Ensure we do not reuse a protected ID by accident + protectedIDs = protected_ids(identifiedElements, options) + # IDs that have been allocated and should not be remapped. + consumedIDs = set() + + # List of IDs that need to be assigned a new ID. The list is ordered + # such that earlier entries will be assigned a shorter ID than those + # later in the list. IDs in this list *can* obtain an ID that is + # longer than they already are. + need_new_id = [] + + id_allocations = list(compute_id_lengths(len(idList) + 1)) + # Reverse so we can use it as a stack and still work from "shortest to + # longest" ID. + id_allocations.reverse() + + # Here we loop over all current IDs (that we /might/ want to remap) + # and group them into two. 1) The IDs that already have a perfect + # length (these are added to consumedIDs) and 2) the IDs that need + # to change length (these are appended to need_new_id). + optimal_id_length, id_use_limit = 0, 0 + for current_id in idList: + # If we are out of IDs of the current length, then move on + # to the next length + if id_use_limit < 1: + optimal_id_length, id_use_limit = id_allocations.pop() + # Reserve an ID from this length + id_use_limit -= 1 + # We check for strictly equal to optimal length because our ID + # remapping may have to assign one node a longer ID because + # another node needs a shorter ID. + if len(current_id) == optimal_id_length: + # This rid is already of optimal length - lets just keep it. + consumedIDs.add(current_id) + else: + # Needs a new (possibly longer) ID. + need_new_id.append(current_id) curIdNum = 1 - for 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) + for old_id in need_new_id: + new_id = intToID(curIdNum, prefix) + + # Skip ahead if the new ID has already been used or is protected. + while new_id in protectedIDs or new_id in consumedIDs: + curIdNum += 1 + new_id = intToID(curIdNum, prefix) + + # Now that we have found the first available ID, do the remap. + num += renameID(old_id, new_id, identifiedElements, referencedIDs.get(old_id)) curIdNum += 1 return num +def compute_id_lengths(highest): + """Compute how many IDs are available of a given size + + Example: + >>> lengths = list(compute_id_lengths(512)) + >>> lengths + [(1, 26), (2, 676)] + >>> total_limit = sum(x[1] for x in lengths) + >>> total_limit + 702 + >>> intToID(total_limit, '') + 'zz' + + Which tells us that we got 26 IDs of length 1 and up to 676 IDs of length two + if we need to allocate 512 IDs. + + :param highest: Highest ID that need to be allocated + :return: An iterator that returns tuples of (id-length, use-limit). The + use-limit applies only to the given id-length (i.e. it is excluding IDs + of shorter length). Note that the sum of the use-limit values is always + equal to or greater than the highest param. + """ + step = 26 + id_length = 0 + use_limit = 1 + while highest: + id_length += 1 + use_limit *= step + yield (id_length, use_limit) + highest = int((highest - 1) / step) + + def intToID(idnum, prefix): """ Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, @@ -750,14 +833,12 @@ return prefix + rid -def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): +def renameID(idFrom, idTo, identifiedElements, referringNodes): """ Changes the ID name from idFrom to idTo, on the declaring element - as well as all references in the document doc. + as well as all nodes in referringNodes. - Updates identifiedElements and referencedIDs. - Does not handle the case where idTo is already the ID name - of another element in doc. + Updates identifiedElements. Returns the number of bytes saved by this replacement. """ @@ -766,12 +847,9 @@ 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, @@ -787,7 +865,7 @@ # 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]) + 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 + ')') @@ -822,34 +900,39 @@ node.setAttribute(attr, newValue) num += len(oldValue) - len(newValue) - del referencedIDs[idFrom] - referencedIDs[idTo] = referringNodes - return num +def protected_ids(seenIDs, options): + """Return a list of protected IDs out of the seenIDs""" + protectedIDs = [] + if options.protect_ids_prefix or options.protect_ids_noninkscape or options.protect_ids_list: + protect_ids_prefixes = [] + protect_ids_list = [] + if options.protect_ids_list: + protect_ids_list = options.protect_ids_list.split(",") + if options.protect_ids_prefix: + protect_ids_prefixes = options.protect_ids_prefix.split(",") + for id in seenIDs: + protected = False + if options.protect_ids_noninkscape and not id[-1].isdigit(): + protected = True + elif protect_ids_list and id in protect_ids_list: + protected = True + elif protect_ids_prefixes: + if any(id.startswith(prefix) for prefix in protect_ids_prefixes): + protected = True + if protected: + protectedIDs.append(id) + return protectedIDs + + 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): - 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: + protectedIDs = protected_ids(identifiedElements, options) + if protectedIDs: + for id in protectedIDs: del identifiedElements[id] return identifiedElements @@ -873,7 +956,6 @@ def removeNamespacedAttributes(node, namespaces): - global _num_attributes_removed num = 0 if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd attributes from this element @@ -884,9 +966,8 @@ 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) + num += len(attrsToRemove) # now recurse for children for child in node.childNodes: @@ -895,7 +976,6 @@ def removeNamespacedElements(node, namespaces): - global _num_elements_removed num = 0 if node.nodeType == Node.ELEMENT_NODE: # remove all namespace'd child nodes from this element @@ -905,9 +985,8 @@ 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) + num += len(childrenToRemove) # now recurse for children for child in node.childNodes: @@ -1063,6 +1142,78 @@ return num +def mergeSiblingGroupsWithCommonAttributes(elem): + """ + Merge two or more sibling elements with the identical attributes. + + This function acts recursively on the given element. + """ + + num = 0 + i = elem.childNodes.length - 1 + while i >= 0: + currentNode = elem.childNodes.item(i) + if currentNode.nodeType != Node.ELEMENT_NODE or currentNode.nodeName != 'g' or \ + currentNode.namespaceURI != NS['SVG']: + i -= 1 + continue + attributes = {a.nodeName: a.nodeValue for a in currentNode.attributes.values()} + if not attributes: + i -= 1 + continue + runStart, runEnd = i, i + runElements = 1 + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == Node.ELEMENT_NODE: + if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: + break + nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} + hasNoMergeTags = (True for n in nextNode.childNodes + if n.nodeType == Node.ELEMENT_NODE + and n.nodeName in ('title', 'desc') + and n.namespaceURI == NS['SVG']) + if attributes != nextAttributes or any(hasNoMergeTags): + break + else: + runElements += 1 + runStart -= 1 + else: + runStart -= 1 + + # Next loop will start from here + i = runStart - 1 + + if runElements < 2: + continue + + # Find the entry that starts the run (we might have run + # past it into a text node or a comment node. + while True: + node = elem.childNodes.item(runStart) + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + break + runStart += 1 + primaryGroup = elem.childNodes.item(runStart) + runStart += 1 + nodes = elem.childNodes[runStart:runEnd+1] + for node in nodes: + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + # Merge + for child in node.childNodes[:]: + primaryGroup.appendChild(child) + elem.removeChild(node).unlink() + else: + primaryGroup.appendChild(node) + + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == Node.ELEMENT_NODE: + num += mergeSiblingGroupsWithCommonAttributes(childNode) + + return num + + def createGroupsForCommonAttributes(elem): """ Creates elements to contain runs of 3 or more @@ -1307,7 +1458,7 @@ elem.namespaceURI == NS['SVG'] ): # found a gradient that is referenced by only 1 other element - refElem = nodes[0] + refElem = nodes.pop() if refElem.nodeType == Node.ELEMENT_NODE and refElem.nodeName in ['linearGradient', 'radialGradient'] \ and refElem.namespaceURI == NS['SVG']: # elem is a gradient referenced by only one other gradient (refElem) @@ -1338,8 +1489,16 @@ 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') + target_href = elem.getAttributeNS(NS['XLINK'], 'href') + if target_href: + # If the elem node had an xlink:href, then the + # refElem have to point to it as well to + # perserve the semantics of the image. + refElem.setAttributeNS(NS['XLINK'], 'href', target_href) + else: + # The elem node had no xlink:href reference, + # so we can simply remove the attribute. + refElem.removeAttributeNS(NS['XLINK'], 'href') # now delete elem elem.parentNode.removeChild(elem) @@ -1374,77 +1533,137 @@ return "\x1e".join(subKeys) -def removeDuplicateGradients(doc): - global _num_elements_removed - num = 0 +def detect_duplicate_gradients(*grad_lists): + """Detects duplicate gradients from each iterable/generator given as argument - gradientsToRemove = {} - - for gradType in ['linearGradient', 'radialGradient']: - grads = doc.getElementsByTagName(gradType) - gradBuckets = defaultdict(list) + Yields (master, master_id, duplicates_id, duplicates) tuples where: + * master_id: The ID attribute of the master element. This will always be non-empty + and not None as long at least one of the gradients have a valid ID. + * duplicates_id: List of ID attributes of the duplicate gradients elements (can be + empty where the gradient had no ID attribute) + * duplicates: List of elements that are duplicates of the `master` element. Will + never include the `master` element. Has the same order as `duplicates_id` - i.e. + `duplicates[X].getAttribute("id") == duplicates_id[X]`. + """ + for grads in grad_lists: + grad_buckets = defaultdict(list) for grad in grads: key = computeGradientBucketKey(grad) - gradBuckets[key].append(grad) + grad_buckets[key].append(grad) - for bucket in six.itervalues(gradBuckets): + for bucket in six.itervalues(grad_buckets): if len(bucket) < 2: # The gradient must be unique if it is the only one in # this bucket. continue master = bucket[0] duplicates = bucket[1:] + duplicates_ids = [d.getAttribute('id') for d in duplicates] + master_id = master.getAttribute('id') + if not master_id: + # If our selected "master" copy does not have an ID, + # then replace it with one that does (assuming any of + # them has one). This avoids broken images like we + # saw in GH#203 + for i in range(len(duplicates_ids)): + dup_id = duplicates_ids[i] + if dup_id: + # We do not bother updating the master field + # as it is not used any more. + master_id = duplicates_ids[i] + duplicates[i] = master + # Clear the old id to avoid a redundant remapping + duplicates_ids[i] = "" + break - gradientsToRemove[master] = duplicates + yield master_id, duplicates_ids, duplicates - # get a collection of all elements that are referenced and their referencing elements - referencedIDs = findReferencedElements(doc.documentElement) - for masterGrad in gradientsToRemove: - 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: + +def dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids): + func_iri = None + for dup_id, dup_grad in zip(duplicates_ids, duplicates): + # if the duplicate gradient no longer has a parent that means it was + # already re-mapped to another master gradient + if not dup_grad.parentNode: + continue + + # With --keep-unreferenced-defs, we can end up with + # unreferenced gradients. See GH#156. + if dup_id in referenced_ids: + if func_iri is None: + # matches url(#), url('#') and url("#") + dup_id_regex = "|".join(duplicates_ids) + func_iri = re.compile('url\\([\'"]?#(?:' + dup_id_regex + ')[\'"]?\\)') + for elem in referenced_ids[dup_id]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + (v_new, n) = func_iri.subn('url(#' + master_id + ')', v) + if n > 0: + elem.setAttribute(attr, v_new) + if elem.getAttributeNS(NS['XLINK'], 'href') == '#' + dup_id: + elem.setAttributeNS(NS['XLINK'], 'href', '#' + master_id) + styles = _getStyle(elem) + for style in styles: + v = styles[style] + (v_new, n) = func_iri.subn('url(#' + master_id + ')', v) + if n > 0: + styles[style] = v_new + _setStyle(elem, styles) + + # now that all referencing elements have been re-mapped to the master + # it is safe to remove this gradient from the document + dup_grad.parentNode.removeChild(dup_grad) + + # If the gradients have an ID, we update referenced_ids to match the newly remapped IDs. + # This enable us to avoid calling findReferencedElements once per loop, which is helpful as it is + # one of the slowest functions in scour. + if master_id: + try: + master_references = referenced_ids[master_id] + except KeyError: + master_references = set() + + for dup_id in duplicates_ids: + references = referenced_ids.pop(dup_id, None) + if references is None: continue + master_references.update(references) + + # Only necessary but needed if the master gradient did + # not have any references originally + referenced_ids[master_id] = master_references + + +def removeDuplicateGradients(doc): + prev_num = -1 + num = 0 + + # get a collection of all elements that are referenced and their referencing elements + referenced_ids = findReferencedElements(doc.documentElement) + + while prev_num != num: + prev_num = num + + linear_gradients = doc.getElementsByTagName('linearGradient') + radial_gradients = doc.getElementsByTagName('radialGradient') + + for master_id, duplicates_ids, duplicates in detect_duplicate_gradients(linear_gradients, radial_gradients): + dedup_gradient(master_id, duplicates_ids, duplicates, referenced_ids) + num += len(duplicates) - # 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") - - # With --keep-unreferenced-defs, we can end up with - # unreferenced gradients. See GH#156. - if dup_id in referencedIDs: - for elem in referencedIDs[dup_id]: - # 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 == Node.ELEMENT_NODE and len(node.getAttribute('style')) > 0: + if node.nodeType != Node.ELEMENT_NODE: + return {} + style_attribute = node.getAttribute('style') + if style_attribute: styleMap = {} - rawStyles = node.getAttribute('style').split(';') + rawStyles = style_attribute.split(';') for style in rawStyles: propval = style.split(':') if len(propval) == 2: @@ -1456,7 +1675,7 @@ def _setStyle(node, styleMap): u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap]) + fixedStyle = ';'.join(prop + ':' + styleMap[prop] for prop in styleMap) if fixedStyle != '': node.setAttribute('style', fixedStyle) elif node.getAttribute('style'): @@ -2044,7 +2263,7 @@ newBytes = len(newColorValue) if oldBytes > newBytes: styles[attr] = newColorValue - numBytes += (oldBytes - len(element.getAttribute(attr))) + numBytes += (oldBytes - newBytes) _setStyle(element, styles) # now recurse for our child elements @@ -2567,7 +2786,7 @@ # 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()) + ws_nums = RE_COMMA_WSP.split(s.strip()) nums = [] # also, if 100-100 is found, split it into two also @@ -2676,18 +2895,18 @@ """ # elliptical arc commands must have comma/wsp separating the coordinates # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 - return ''.join([cmd + scourCoordinates(data, options, - control_points=controlPoints(cmd, data), - flags=flags(cmd, data)) - for cmd, data in pathObj]) + return ''.join(cmd + scourCoordinates(data, options, + control_points=controlPoints(cmd, data), + flags=flags(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]) + return ' '.join(command + '(' + ' '.join(scourUnitlessLength(number) for number in numbers) + ')' + for command, numbers in transformObj) def scourCoordinates(data, options, force_whitespace=False, control_points=[], flags=[]): @@ -3180,7 +3399,7 @@ # else we have a statically sized image and we should try to remedy that # parse viewBox attribute - vbSep = re.split('[, ]+', docElement.getAttribute('viewBox')) + vbSep = RE_COMMA_WSP.split(docElement.getAttribute('viewBox')) # if we have a valid viewBox we need to check it vbWidth, vbHeight = 0, 0 if len(vbSep) == 4: @@ -3227,7 +3446,7 @@ attrList = node.attributes for i in range(attrList.length): attr = attrList.item(i) - newNode.setAttributeNS(attr.namespaceURI, attr.localName, attr.nodeValue) + newNode.setAttributeNS(attr.namespaceURI, attr.name, attr.nodeValue) # clone and add all the child nodes for child in node.childNodes: @@ -3243,23 +3462,68 @@ remapNamespacePrefix(child, oldprefix, newprefix) -def makeWellFormed(str, quote=''): - xml_ents = {'<': '<', '>': '>', '&': '&'} - if quote: - xml_ents[quote] = ''' if (quote == "'") else """ - return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) +def make_well_formed(text, quote_dict=None): + if quote_dict is None: + quote_dict = XML_ENTS_NO_QUOTES + if not any(c in text for c in quote_dict): + # The quote-able characters are quite rare in SVG (they mostly only + # occur in text elements in practice). Therefore it make sense to + # optimize for this common case + return text + return ''.join(quote_dict[c] if c in quote_dict else c for c in text) + + +def choose_quote_character(value): + quot_count = value.count('"') + if quot_count == 0 or quot_count <= value.count("'"): + # Fewest "-symbols (if there are 0, we pick this to avoid spending + # time counting the '-symbols as it won't matter) + quote = '"' + xml_ent = XML_ENTS_ESCAPE_QUOT + else: + quote = "'" + xml_ent = XML_ENTS_ESCAPE_APOS + return quote, xml_ent + +TEXT_CONTENT_ELEMENTS = ['text', 'tspan', 'tref', 'textPath', 'altGlyph', + 'flowDiv', 'flowPara', 'flowSpan', 'flowTref', 'flowLine'] -def chooseQuoteCharacter(str): - quotCount = str.count('"') - aposCount = str.count("'") - if quotCount > aposCount: - quote = "'" - hasEmbeddedQuote = aposCount - else: - quote = '"' - hasEmbeddedQuote = quotCount - return (quote, hasEmbeddedQuote) + +KNOWN_ATTRS = [ + # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html + # (but should be kept intuitively ordered) + 'id', 'xml:id', 'class', + 'transform', + 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', + 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', + 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', + 'd', 'points', + ] + sorted(svgAttributes) + [ + 'style', + ] + +KNOWN_ATTRS_ORDER_BY_NAME = defaultdict(lambda: len(KNOWN_ATTRS), + {name: order for order, name in enumerate(KNOWN_ATTRS)}) + + +# use custom order for known attributes and alphabetical order for the rest +def _attribute_sort_key_function(attribute): + name = attribute.name + order_value = KNOWN_ATTRS_ORDER_BY_NAME[name] + return order_value, name + + +def attributes_ordered_for_output(element): + if not element.hasAttributes(): + return [] + attribute = element.attributes + # The .item(i) call is painfully slow (bpo#40689). Therefore we ensure we + # call it at most once per attribute. + # - it would be many times faster to use `attribute.values()` but sadly + # that is an "experimental" interface. + return sorted((attribute.item(i) for i in range(attribute.length)), + key=_attribute_sort_key_function) # hand-rolled serialization function that has the following benefits: @@ -3282,37 +3546,15 @@ outParts.extend([(indent_type * indent_depth), '<', element.nodeName]) # 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', 'xml:id', 'class', - 'transform', - 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', - 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', - 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', - 'd', 'points', - ] + sorted(svgAttributes) + [ - 'style', - ] - 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)] - for index in attrIndices: - attr = attrList.item(index) - + attrs = attributes_ordered_for_output(element) + for attr in attrs: attrValue = attr.nodeValue - (quote, hasEmbeddedQuote) = chooseQuoteCharacter(attrValue) - attrValue = makeWellFormed(attrValue, quote if hasEmbeddedQuote else '') + quote, xml_ent = choose_quote_character(attrValue) + attrValue = make_well_formed(attrValue, xml_ent) if attr.nodeName == 'style': # sort declarations - attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) + attrValue = ';'.join(sorted(attrValue.split(';'))) outParts.append(' ') # preserve xmlns: if it is a namespace prefix declaration @@ -3345,7 +3587,7 @@ # "text1\ntext2" and # "text1\n text2" # see https://www.w3.org/TR/SVG/text.html#WhiteSpace - if preserveWhitespace or element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + if preserveWhitespace or element.nodeName in TEXT_CONTENT_ELEMENTS: outParts.append(serializeXML(child, options, 0, preserveWhitespace)) else: outParts.extend([newline, serializeXML(child, options, indent_depth + 1, preserveWhitespace)]) @@ -3356,7 +3598,7 @@ if not preserveWhitespace: # strip / consolidate whitespace according to spec, see # https://www.w3.org/TR/SVG/text.html#WhiteSpace - if element.nodeName in ['text', 'tspan', 'tref', 'textPath', 'altGlyph']: + if element.nodeName in TEXT_CONTENT_ELEMENTS: text_content = text_content.replace('\n', '') text_content = text_content.replace('\t', ' ') if child == element.firstChild: @@ -3367,7 +3609,7 @@ text_content = text_content.replace(' ', ' ') else: text_content = text_content.strip() - outParts.append(makeWellFormed(text_content)) + outParts.append(make_well_formed(text_content)) # CDATA node elif child.nodeType == Node.CDATA_SECTION_NODE: outParts.extend(['']) @@ -3452,14 +3694,12 @@ # 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? + # remove unneeded namespaced elements/attributes added by common editors if options.keep_editor_data is False: - while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: - pass - while removeNamespacedAttributes(doc.documentElement, unwanted_ns) > 0: - pass + _num_elements_removed += removeNamespacedElements(doc.documentElement, + unwanted_ns) + _num_attributes_removed += removeNamespacedAttributes(doc.documentElement, + unwanted_ns) # remove the xmlns: declarations now xmlnsDeclsToRemove = [] @@ -3559,9 +3799,10 @@ pass # remove duplicate gradients - while removeDuplicateGradients(doc) > 0: - pass + _num_elements_removed += removeDuplicateGradients(doc) + if options.group_collapse: + _num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) # create elements if there are runs of elements with the same attributes. # this MUST be before moveCommonAttributesToParentGroup. if options.group_create: @@ -3602,7 +3843,7 @@ # 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)) + _num_bytes_saved_in_ids += shortenIDs(doc, options.shorten_ids_prefix, options) # scour lengths (including coordinates) for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', @@ -3614,7 +3855,7 @@ elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) viewBox = doc.documentElement.getAttribute('viewBox') if viewBox: - lengths = re.split('[, ]+', viewBox) + lengths = RE_COMMA_WSP.split(viewBox) lengths = [scourUnitlessLength(length) for length in lengths] doc.documentElement.setAttribute('viewBox', ' '.join(lengths)) diff -Nru scour-0.37/setup.py scour-0.38.2/setup.py --- scour-0.37/setup.py 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/setup.py 2020-11-22 14:05:13.000000000 +0000 @@ -40,7 +40,7 @@ Authors: - Jeff Schiller, Louis Simard (original authors) - Tobias Oberstein (maintainer) - - Eduard Braun (maintainer) + - Patrick Storz (maintainer) """ VERSIONFILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "scour", "__init__.py") diff -Nru scour-0.37/test_css.py scour-0.38.2/test_css.py --- scour-0.37/test_css.py 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/test_css.py 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Test Harness for Scour +# +# Copyright 2010 Jeff Schiller +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import unittest + +from scour.yocto_css import parseCssString + + +class Blank(unittest.TestCase): + + def runTest(self): + r = parseCssString('') + self.assertEqual(len(r), 0, 'Blank string returned non-empty list') + self.assertEqual(type(r), type([]), 'Blank string returned non list') + + +class ElementSelector(unittest.TestCase): + + def runTest(self): + r = parseCssString('foo {}') + self.assertEqual(len(r), 1, 'Element selector not returned') + self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEqual(len(r[0]['properties']), 0, 'Property list for foo not empty') + + +class ElementSelectorWithProperty(unittest.TestCase): + + def runTest(self): + r = parseCssString('foo { bar: baz}') + self.assertEqual(len(r), 1, 'Element selector not returned') + self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEqual(len(r[0]['properties']), 1, 'Property list for foo did not have 1') + self.assertEqual(r[0]['properties']['bar'], 'baz', 'Property bar did not have baz value') + + +if __name__ == '__main__': + unittest.main() diff -Nru scour-0.37/testcss.py scour-0.38.2/testcss.py --- scour-0.37/testcss.py 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/testcss.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Test Harness for Scour -# -# Copyright 2010 Jeff Schiller -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import - -import unittest - -from scour.yocto_css import parseCssString - - -class Blank(unittest.TestCase): - - def runTest(self): - r = parseCssString('') - self.assertEqual(len(r), 0, 'Blank string returned non-empty list') - self.assertEqual(type(r), type([]), 'Blank string returned non list') - - -class ElementSelector(unittest.TestCase): - - def runTest(self): - r = parseCssString('foo {}') - self.assertEqual(len(r), 1, 'Element selector not returned') - self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual(len(r[0]['properties']), 0, 'Property list for foo not empty') - - -class ElementSelectorWithProperty(unittest.TestCase): - - def runTest(self): - r = parseCssString('foo { bar: baz}') - self.assertEqual(len(r), 1, 'Element selector not returned') - self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual(len(r[0]['properties']), 1, 'Property list for foo did not have 1') - self.assertEqual(r[0]['properties']['bar'], 'baz', 'Property bar did not have baz value') - - -if __name__ == '__main__': - unittest.main() diff -Nru scour-0.37/test_scour.py scour-0.38.2/test_scour.py --- scour-0.37/test_scour.py 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/test_scour.py 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,2796 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Test Harness for Scour +# +# Copyright 2010 Jeff Schiller +# Copyright 2010 Louis Simard +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function # use print() as a function in Python 2 (see PEP 3105) +from __future__ import absolute_import # use absolute imports by default in Python 2 (see PEP 328) + +import os +import sys +import unittest + +import six +from six.moves import map, range + +from scour.scour import (make_well_formed, parse_args, scourString, scourXmlFile, start, run, + XML_ENTS_ESCAPE_APOS, XML_ENTS_ESCAPE_QUOT) +from scour.svg_regex import svg_parser +from scour import __version__ + + +SVGNS = 'http://www.w3.org/2000/svg' + + +# I couldn't figure out how to get ElementTree to work with the following XPath +# "//*[namespace-uri()='http://example.com']" +# so I decided to use minidom and this helper function that performs a test on a given node +# and all its children +# func must return either True (if pass) or False (if fail) +def walkTree(elem, func): + if func(elem) is False: + return False + for child in elem.childNodes: + if walkTree(child, func) is False: + return False + return True + + +class ScourOptions: + pass + + +class EmptyOptions(unittest.TestCase): + + MINIMAL_SVG = '\n' \ + '\n' + + def test_scourString(self): + options = ScourOptions + try: + scourString(self.MINIMAL_SVG, options) + fail = False + except Exception: + fail = True + self.assertEqual(fail, False, + 'Exception when calling "scourString" with empty options object') + + def test_scourXmlFile(self): + options = ScourOptions + try: + scourXmlFile('unittests/minimal.svg', options) + fail = False + except Exception: + fail = True + self.assertEqual(fail, False, + 'Exception when calling "scourXmlFile" with empty options object') + + def test_start(self): + options = ScourOptions + input = open('unittests/minimal.svg', 'rb') + output = open('testscour_temp.svg', 'wb') + + stdout_temp = sys.stdout + sys.stdout = None + try: + start(options, input, output) + fail = False + except Exception: + fail = True + sys.stdout = stdout_temp + + os.remove('testscour_temp.svg') + + self.assertEqual(fail, False, + 'Exception when calling "start" with empty options object') + + +class InvalidOptions(unittest.TestCase): + + def runTest(self): + options = ScourOptions + options.invalidOption = "invalid value" + try: + scourXmlFile('unittests/ids-to-strip.svg', options) + fail = False + except Exception: + fail = True + self.assertEqual(fail, False, + 'Exception when calling Scour with invalid options') + + +class GetElementById(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/ids.svg') + self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') + self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') + self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') + self.assertIsNotNone(doc.getElementById('rect1'), 'rect not found by ID') + self.assertIsNone(doc.getElementById('rect2'), 'Non-existing element found by ID') + + +class NoInkscapeElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, + lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), + False, + 'Found Inkscape elements') + + +class NoSodipodiElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, + lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), + False, + 'Found Sodipodi elements') + + +class NoAdobeIllustratorElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), + False, + 'Found Adobe Illustrator elements') + + +class NoAdobeGraphsElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), + False, + 'Found Adobe Graphs elements') + + +class NoAdobeSVGViewerElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), + False, + 'Found Adobe SVG Viewer elements') + + +class NoAdobeVariablesElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), + False, + 'Found Adobe Variables elements') + + +class NoAdobeSaveForWebElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), + False, + 'Found Adobe Save For Web elements') + + +class NoAdobeExtensibilityElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), + False, + 'Found Adobe Extensibility elements') + + +class NoAdobeFlowsElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), + False, + 'Found Adobe Flows elements') + + +class NoAdobeImageReplacementElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), + False, + 'Found Adobe Image Replacement elements') + + +class NoAdobeCustomElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), + False, + 'Found Adobe Custom elements') + + +class NoAdobeXPathElements(unittest.TestCase): + + def runTest(self): + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), + False, + 'Found Adobe XPath elements') + + +class DoNotRemoveTitleWithOnlyText(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Removed title element with only text child') + + +class RemoveEmptyTitleElement(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, + 'Did not remove empty title element') + + +class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, + 'Removed description element with only text child') + + +class RemoveEmptyDescriptionElement(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, + 'Did not remove empty description element') + + +class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, + 'Removed metadata element with only text child') + + +class RemoveEmptyMetadataElement(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, + 'Did not remove empty metadata element') + + +class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Removed title element with only text child') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, + 'Removed description element with only text child') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, + 'Removed metadata element with only text child') + + +class RemoveEmptyDescriptiveElements(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, + 'Did not remove empty title element') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, + 'Did not remove empty description element') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, + 'Did not remove empty metadata element') + + +class RemoveEmptyGElements(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/empty-g.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, + 'Did not remove empty g element') + + +class RemoveUnreferencedPattern(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/unreferenced-pattern.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, + 'Unreferenced pattern not removed') + + +class RemoveUnreferencedLinearGradient(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/unreferenced-linearGradient.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Unreferenced linearGradient not removed') + + +class RemoveUnreferencedRadialGradient(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/unreferenced-radialGradient.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, + 'Unreferenced radialGradient not removed') + + +class RemoveUnreferencedElementInDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/referenced-elements-1.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Unreferenced rect left in defs') + + +class RemoveUnreferencedDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/unreferenced-defs.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Referenced linearGradient removed from defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, + 'Unreferenced radialGradient left in defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, + 'Unreferenced pattern left in defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Referenced rect removed from defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 0, + 'Unreferenced circle left in defs') + + +class KeepUnreferencedDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/unreferenced-defs.svg', + parse_args(['--keep-unreferenced-defs'])) + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, + 'Unreferenced radialGradient removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 1, + 'Unreferenced pattern removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Referenced rect removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 1, + 'Unreferenced circle removed from defs with `--keep-unreferenced-defs`') + + +class DoNotRemoveChainedRefsInDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/refs-in-defs.svg') + g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] + self.assertEqual(g.childNodes.length >= 2, True, + 'Chained references not honored in defs') + + +class KeepTitleInDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/referenced-elements-1.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Title removed from in defs') + + +class RemoveNestedDefs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/nested-defs.svg') + allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') + self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') + + +class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/ids-to-strip.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', + ' 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'), '', + ' 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 ') + + +class ConvertStyleToAttrs(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', + 'style attribute not emptied') + + +class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when stroke opacity zero') + + +class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', + 'stroke-width attribute not emptied when stroke opacity zero') + + +class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when stroke opacity zero') + + +class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when stroke opacity zero') + + +class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when stroke opacity zero') + + +class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when stroke opacity zero') + + +class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when width zero') + + +class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', + 'stroke-opacity attribute not emptied when width zero') + + +class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when width zero') + + +class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when width zero') + + +class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when width zero') + + +class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when width zero') + + +class RemoveStrokeWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when no stroke') + + +class KeepStrokeWhenInheritedFromParent(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', + 'stroke attribute removed despite a different value being inherited from a parent') + + +class KeepStrokeWhenInheritedByChild(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', + 'stroke attribute removed despite it being inherited by a child') + + +class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', + 'stroke-width attribute not emptied when no stroke') + + +class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', + 'stroke-width attribute removed despite it being inherited by a child') + + +class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', + 'stroke-opacity attribute not emptied when no stroke') + + +class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when no stroke') + + +class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when no stroke') + + +class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when no stroke') + + +class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when no stroke') + + +class RemoveFillRuleWhenFillNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', + 'fill-rule attribute not emptied when no fill') + + +class RemoveFillOpacityWhenFillNone(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', + 'fill-opacity attribute not emptied when no fill') + + +class ConvertFillPropertyToAttr(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/fill-none.svg', + parse_args(['--disable-simplify-colors'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', + 'fill property not converted to XML attribute') + + +class ConvertFillOpacityPropertyToAttr(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', + 'fill-opacity property not converted to XML attribute') + + +class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', + 'fill-rule property not converted to XML attribute') + + +class CollapseSinglyReferencedGradients(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-gradients.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Singly-referenced linear gradient not collapsed') + + +class InheritGradientUnitsUponCollapsing(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-gradients.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), + 'userSpaceOnUse', + 'gradientUnits not properly inherited when collapsing gradients') + + +class OverrideGradientUnitsUponCollapsing(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', + 'gradientUnits not properly overrode when collapsing gradients') + + +class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/dont-collapse-gradients.svg') + self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Multiply-referenced linear gradient collapsed') + + +class PreserveXLinkHrefWhenCollapsingReferencedGradients(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-gradients-preserve-xlink-href.svg') + g1 = doc.getElementById("g1") + g2 = doc.getElementById("g2") + g3 = doc.getElementById("g3") + self.assertTrue(g1, 'g1 is still present') + self.assertTrue(g2 is None, 'g2 was removed') + self.assertTrue(g3, 'g3 is still present') + self.assertEqual(g3.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#g1', + 'g3 has a xlink:href to g1') + + +class RemoveTrailingZerosFromPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-truncate-zeros.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path[:4] == 'm300' and path[4] != '.', True, + 'Trailing zeros not removed from path data') + + +class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-truncate-zeros-calc.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path, 'm5.81 0h0.1', + 'Trailing zeros not removed from path data after calculation') + + +class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-truncate-zeros.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path[4], '-', + 'Delimiters not removed before negative coordinates in path data') + + +class UseScientificNotationToShortenCoordsInPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-use-scientific-notation.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path, 'm1e4 0', + 'Not using scientific notation for path coord when representation is shorter') + + +class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-abs-to-rel.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[1][0], 'v', + 'Absolute V command not converted to relative v command') + self.assertEqual(float(path[1][1][0]), -20.0, + 'Absolute V value not converted to relative v value') + + +class RoundPathData(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(float(path[0][1][0]), 100.0, + 'Not rounding down') + self.assertEqual(float(path[0][1][1]), 100.0, + 'Not rounding up') + + +class LimitPrecisionInPathData(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(float(path[1][1][0]), 100.01, + 'Not correctly limiting precision on path data') + + +class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=1'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 21 321 4e3 5e4 7e5", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4e3 -5e4 -7e5", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=1" ' + 'for path with ID ' + paths[5].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 6.5e5", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-6.5e5", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=2" ' + 'for path with ID ' + paths[5].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", + 'Precision not correctly reduced with "--set-precision=3" ' + 'for path with ID ' + paths[5].getAttribute('id')) + + doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=4'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths[1:3]: + self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + path.getAttribute('id')) + self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + paths[4].getAttribute('id')) + self.assertEqual(paths[5].getAttribute('d'), "m123.5 101-123.5-101", + 'Precision not correctly reduced with "--set-precision=4" ' + 'for path with ID ' + paths[5].getAttribute('id')) + + +class LimitPrecisionInControlPointPathData(unittest.TestCase): + + def runTest(self): + path_data = ("m1.1 2.2 3.3 4.4m-4.4-6.7" + "c1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7m-17-20" + "s1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4m-10-13" + "q1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4") + doc = scourXmlFile('unittests/path-precision-control-points.svg', + parse_args(['--set-precision=2', '--set-c-precision=1'])) + path_data2 = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path_data2, path_data, + 'Not correctly limiting precision on path data with --set-c-precision') + + +class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[4][0], 'z', + 'Did not remove an empty line segment from path') + + +class RemoveEmptySegmentsFromPathWithButtLineCaps(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) + for id in ['none', 'attr_butt', 'style_butt']: + path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) + self.assertEqual(len(path), 1, + 'Did not remove empty segments when path had butt linecaps') + + +class DoNotRemoveEmptySegmentsFromPathWithRoundSquareLineCaps(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) + for id in ['attr_round', 'attr_square', 'style_round', 'style_square']: + path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) + self.assertEqual(len(path), 2, + 'Did remove empty segments when path had round or square linecaps') + + +class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[1][0], 'h', + 'Did not change line to horizontal line segment in path') + self.assertEqual(float(path[1][1][0]), 200.0, + 'Did not calculate horizontal line segment in path correctly') + + +class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[2][0], 'v', + 'Did not change line to vertical line segment in path') + self.assertEqual(float(path[2][1][0]), 100.0, + 'Did not calculate vertical line segment in path correctly') + + +class ChangeBezierToShorthandInPath(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-bez-optimize.svg') + self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', + 'Did not change bezier curves into shorthand curve segments in path') + self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', + 'Did not change bezier curve into shorthand curve segment when first control point ' + 'is the current point and previous command was not a bezier curve') + self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', + 'Did change bezier curve into shorthand curve segment when first control point ' + 'is the current point but previous command was a bezier curve with a different control point') + + +class ChangeQuadToShorthandInPath(unittest.TestCase): + + def runTest(self): + path = scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', + 'Did not change quadratic curves into shorthand curve segments in path') + + +class BooleanFlagsInEllipticalPath(unittest.TestCase): + + def test_omit_spaces(self): + doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--no-renderer-workaround'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths: + self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 00100 50', + 'Did not ommit spaces after boolean flags in elliptical arg path command') + + def test_output_spaces_with_renderer_workaround(self): + doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--renderer-workaround'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + for path in paths: + self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 0 0 100 50', + 'Did not output spaces after boolean flags in elliptical arg path command ' + 'with renderer workaround') + + +class DoNotOptimzePathIfLarger(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertTrue(len(p.getAttribute('d')) <= + # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): + # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" + len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), + 'Made path data longer during optimization') + + +class HandleEncodingUTF8(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/encoding-utf8.svg') + text = u'Hello in many languages:\n' \ + u'ar: أهلا\n' \ + u'bn: হ্যালো\n' \ + u'el: Χαίρετε\n' \ + u'en: Hello\n' \ + u'hi: नमस्ते\n' \ + u'iw: שלום\n' \ + u'ja: こんにちは\n' \ + u'km: ជំរាបសួរ\n' \ + u'ml: ഹലോ\n' \ + u'ru: Здравствуйте\n' \ + u'ur: ہیلو\n' \ + u'zh: 您好' + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + self.assertEqual(desc, text, + 'Did not handle international UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() + self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', + 'Did not handle common UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() + self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', + 'Did not handle mathematical UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() + self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', + 'Did not handle superscript/subscript UTF8 characters') + + +class HandleEncodingISO_8859_15(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/encoding-iso-8859-15.svg') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + self.assertEqual(desc, u'áèîäöü߀ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters') + + +class HandleSciNoInPathData(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-sn.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, + 'Did not handle scientific notation in path data') + + +class TranslateRGBIntoHex(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(elem.getAttribute('fill'), '#0f1011', + 'Not converting rgb into hex') + + +class TranslateRGBPctIntoHex(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] + self.assertEqual(elem.getAttribute('stop-color'), '#7f0000', + 'Not converting rgb pct into hex') + + +class TranslateColorNamesIntoHex(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(elem.getAttribute('stroke'), '#a9a9a9', + 'Not converting standard color names into hex') + + +class TranslateExtendedColorNamesIntoHex(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] + self.assertEqual(elem.getAttribute('solid-color'), '#fafad2', + 'Not converting extended color names into hex') + + +class TranslateLongHexColorIntoShortHex(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] + self.assertEqual(elem.getAttribute('fill'), '#fff', + 'Not converting long hex color into short hex') + + +class DoNotConvertShortColorNames(unittest.TestCase): + + def runTest(self): + elem = scourXmlFile('unittests/dont-convert-short-color-names.svg') \ + .getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual('red', elem.getAttribute('fill'), + 'Converted short color name to longer hex string') + + +class AllowQuotEntitiesInUrl(unittest.TestCase): + + def runTest(self): + grads = scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(len(grads), 1, + 'Removed referenced gradient when " was in the url') + + +class RemoveFontStylesFromNonTextShapes(unittest.TestCase): + + def runTest(self): + r = scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(r.getAttribute('font-size'), '', + 'font-size not removed from rect') + + +class CollapseStraightPathSegments(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/collapse-straight-path-segments.svg', parse_args(['--disable-style-to-xml'])) + paths = doc.getElementsByTagNameNS(SVGNS, 'path') + path_data = [path.getAttribute('d') for path in paths] + path_data_expected = ['m0 0h30', + 'm0 0v30', + 'm0 0h10.5v10.5', + 'm0 0h10-1v10-1', + 'm0 0h30', + 'm0 0h30', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0h10 20', + 'm0 0 20 40v1l10 20', + 'm0 0 10 10-20-20 10 10-20-20', + 'm0 0 1 2m1 2 2 4m1 2 2 4', + 'm6.3228 7.1547 81.198 45.258'] + + self.assertEqual(path_data[0:3], path_data_expected[0:3], + 'Did not collapse h/v commands into a single h/v commands') + self.assertEqual(path_data[3], path_data_expected[3], + 'Collapsed h/v commands with different direction') + self.assertEqual(path_data[4:6], path_data_expected[4:6], + 'Did not collapse h/v commands with only start/end markers present') + self.assertEqual(path_data[6:10], path_data_expected[6:10], + 'Did not preserve h/v commands with intermediate markers present') + + self.assertEqual(path_data[10], path_data_expected[10], + 'Did not collapse lineto commands into a single (implicit) lineto command') + self.assertEqual(path_data[11], path_data_expected[11], + 'Collapsed lineto commands with different direction') + self.assertEqual(path_data[12], path_data_expected[12], + 'Collapsed first parameter pair of a moveto subpath') + self.assertEqual(path_data[13], path_data_expected[13], + 'Did not collapse the nodes of a straight real world path') + + +class ConvertStraightCurvesToLines(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(p.getAttribute('d'), 'm10 10 40 40 40-40z', + 'Did not convert straight curves into lines') + + +class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEqual(p.getAttribute('points'), '50 50 150 50 150 150 50 150', + 'Unnecessary polygon end point not removed') + + +class DoNotRemovePolgonLastPoint(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] + self.assertEqual(p.getAttribute('points'), '200 50 300 50 300 150 200 150', + 'Last point of polygon removed') + + +class ScourPolygonCoordsSciNo(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEqual(p.getAttribute('points'), '1e4 50', + 'Polygon coordinates not scoured') + + +class ScourPolylineCoordsSciNo(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(p.getAttribute('points'), '1e4 50', + 'Polyline coordinates not scoured') + + +class ScourPolygonNegativeCoords(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + # points="100,-100,100-100,100-100-100,-100-100,200" /> + self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polygon coordinates not properly parsed') + + +class ScourPolylineNegativeCoords(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polyline coordinates not properly parsed') + + +class ScourPolygonNegativeCoordFirst(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + # points="-100,-100,100-100,100-100-100,-100-100,200" /> + self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polygon coordinates not properly parsed') + + +class ScourPolylineNegativeCoordFirst(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polyline coordinates not properly parsed') + + +class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): + + def runTest(self): + f = scourXmlFile('unittests/important-groups-in-defs.svg') + self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Group in defs with id\'ed element removed') + + +class AlwaysKeepClosePathSegments(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', + 'Path with closepath not preserved') + + +class RemoveDuplicateLinearGradients(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') + lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(1, lingrads.length, + 'Duplicate linear gradient not removed') + + +class RereferenceForLinearGradient(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), + 'Reference not updated after removing duplicate linear gradient') + self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), + 'Reference not updated after removing duplicate linear gradient') + + +class RemoveDuplicateRadialGradients(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') + radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') + self.assertEqual(1, radgrads.length, + 'Duplicate radial gradient not removed') + + +class RemoveDuplicateRadialGradientsEnsureMasterHasID(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients-master-without-id.svg') + lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') + rect = svgdoc.getElementById('r1') + self.assertEqual(1, lingrads.length, + 'Duplicate linearGradient not removed') + self.assertEqual(lingrads[0].getAttribute("id"), "g1", + "linearGradient has a proper ID") + self.assertNotEqual(rect.getAttribute("fill"), "url(#)", + "linearGradient has a proper ID") + + +class RereferenceForRadialGradient(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), + 'Reference not updated after removing duplicate radial gradient') + + +class RereferenceForGradientWithFallback(unittest.TestCase): + + def runTest(self): + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[0].getAttribute('fill') + ' #fff', rects[5].getAttribute('fill'), + 'Reference (with fallback) not updated after removing duplicate linear gradient') + + +class CollapseSamePathPoints(unittest.TestCase): + + def runTest(self): + p = scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(p.getAttribute('d'), "m100 100 100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", + 'Did not collapse same path points') + + +class ScourUnitlessLengths(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/scour-lengths.svg') + r = doc.getElementsByTagNameNS(SVGNS, 'rect')[0] + svg = doc.documentElement + self.assertEqual(svg.getAttribute('x'), '1', + 'Did not scour x attribute of svg element with unitless number') + self.assertEqual(r.getAttribute('x'), '123.46', + 'Did not scour x attribute of rect with unitless number') + self.assertEqual(r.getAttribute('y'), '123', + 'Did not scour y attribute of rect unitless number') + self.assertEqual(r.getAttribute('width'), '300', + 'Did not scour width attribute of rect with unitless number') + self.assertEqual(r.getAttribute('height'), '100', + 'Did not scour height attribute of rect with unitless number') + + +class ScourLengthsWithUnits(unittest.TestCase): + + def runTest(self): + r = scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1] + self.assertEqual(r.getAttribute('x'), '123.46px', + 'Did not scour x attribute with unit') + self.assertEqual(r.getAttribute('y'), '35ex', + 'Did not scour y attribute with unit') + self.assertEqual(r.getAttribute('width'), '300pt', + 'Did not scour width attribute with unit') + self.assertEqual(r.getAttribute('height'), '50%', + 'Did not scour height attribute with unit') + + +class RemoveRedundantSvgNamespaceDeclaration(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + self.assertNotEqual(doc.getAttribute('xmlns:svg'), 'http://www.w3.org/2000/svg', + 'Redundant svg namespace declaration not removed') + + +class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] + self.assertEqual(r.tagName, 'rect', + 'Redundant svg: prefix not removed from rect') + t = doc.getElementsByTagNameNS(SVGNS, 'text')[0] + self.assertEqual(t.tagName, 'text', + 'Redundant svg: prefix not removed from text') + + # Regression test for #239 + self.assertEqual(t.getAttribute('xml:space'), 'preserve', + 'Required xml: prefix removed in error') + self.assertEqual(t.getAttribute("space"), '', + 'Required xml: prefix removed in error') + + +class RemoveDefaultGradX1Value(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('x1'), '', + 'x1="0" not removed') + + +class RemoveDefaultGradY1Value(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('y1'), '', + 'y1="0" not removed') + + +class RemoveDefaultGradX2Value(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/gradient-default-attrs.svg') + self.assertEqual(doc.getElementById('grad1').getAttribute('x2'), '', + 'x2="100%" not removed') + self.assertEqual(doc.getElementById('grad1b').getAttribute('x2'), '', + 'x2="1" not removed, ' + 'which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') + self.assertNotEqual(doc.getElementById('grad1c').getAttribute('x2'), '', + 'x2="1" removed, ' + 'which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') + + +class RemoveDefaultGradY2Value(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('y2'), '', + 'y2="0" not removed') + + +class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('gradientUnits'), '', + 'gradientUnits="objectBoundingBox" not removed') + + +class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('spreadMethod'), '', + 'spreadMethod="pad" not removed') + + +class RemoveDefaultGradCXValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('cx'), '', + 'cx="50%" not removed') + + +class RemoveDefaultGradCYValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('cy'), '', + 'cy="50%" not removed') + + +class RemoveDefaultGradRValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('r'), '', + 'r="50%" not removed') + + +class RemoveDefaultGradFXValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('fx'), '', + 'fx matching cx not removed') + + +class RemoveDefaultGradFYValue(unittest.TestCase): + + def runTest(self): + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('fy'), '', + 'fy matching cy not removed') + + +class RemoveDefaultAttributeOrderSVGLengthCrash(unittest.TestCase): + + # Triggered a crash in v0.36 + def runTest(self): + try: + scourXmlFile('unittests/remove-default-attr-order.svg') + except AttributeError: + self.fail("Processing the order attribute triggered an AttributeError") + + +class RemoveDefaultAttributeStdDeviationSVGLengthCrash(unittest.TestCase): + + # Triggered a crash in v0.36 + def runTest(self): + try: + scourXmlFile('unittests/remove-default-attr-std-deviation.svg') + except AttributeError: + self.fail("Processing the order attribute triggered an AttributeError") + + +class CDATAInXml(unittest.TestCase): + + def runTest(self): + with open('unittests/cdata.svg') as f: + lines = scourString(f.read()).splitlines() + self.assertEqual(lines[3], + " alert('pb&j');", + 'CDATA did not come out correctly') + + +class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('unicode="<"') != -1, + "Improperly serialized < in attribute value") + + +class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('unicode="&"') != -1, + 'Improperly serialized & in attribute value') + + +class WellFormedXMLLesserThanInTextContent(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('2 < 5') != -1, + 'Improperly serialized < in text content') + + +class WellFormedXMLAmpersandInTextContent(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('Peanut Butter & Jelly') != -1, + 'Improperly serialized & in text content') + + +class WellFormedXMLNamespacePrefixRemoveUnused(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:foo=') == -1, + 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') + + +class WellFormedXMLNamespacePrefixKeepUsedElementPrefix(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:bar=') != -1, + 'Improperly serialized namespace prefix declarations: Used element prefix removed') + + +class WellFormedXMLNamespacePrefixKeepUsedAttributePrefix(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:baz=') != -1, + 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') + + +class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-ns-decl.svg') as f: + xmlstring = scourString(f.read()) + self.assertTrue(xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, + 'Improperly serialized namespace prefix declarations when not in default namespace') + + +class MoveSVGElementsToDefaultNamespace(unittest.TestCase): + + def runTest(self): + with open('unittests/xml-ns-decl.svg') as f: + xmlstring = scourString(f.read()) + self.assertTrue(xmlstring.find(' does not inherit xml:space="preserve" of parent text element') + text = self.doc.getElementById('txt_c2') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="default" of does not overwrite xml:space="preserve" of parent text element') + text = self.doc.getElementById('txt_c3') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="preserve" of does not overwrite xml:space="default" of parent text element') + text = self.doc.getElementById('txt_c4') + self.assertIn('text1 text2', text.toxml(), + ' does not inherit xml:space="preserve" of parent group') + text = self.doc.getElementById('txt_c5') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="default" of text element does not overwrite xml:space="preserve" of parent group') + text = self.doc.getElementById('txt_c6') + self.assertIn('text1 text2', text.toxml(), + 'xml:space="preserve" of text element does not overwrite xml:space="default" of parent group') + + def test_important_whitespace(self): + text = self.doc.getElementById('txt_d1') + self.assertIn('text1 text2', text.toxml(), + 'Newline with whitespace collapsed in text element') + text = self.doc.getElementById('txt_d2') + self.assertIn('text1 tspan1 text2', text.toxml(), + 'Whitespace stripped from the middle of a text element') + text = self.doc.getElementById('txt_d3') + self.assertIn('text1 tspan1 tspan2 text2', text.toxml(), + 'Whitespace stripped from the middle of a text element') + + def test_incorrect_whitespace(self): + text = self.doc.getElementById('txt_e1') + self.assertIn('text1text2', text.toxml(), + 'Whitespace introduced in text element with newline') + text = self.doc.getElementById('txt_e2') + self.assertIn('text1tspantext2', text.toxml(), + 'Whitespace introduced in text element with ') + text = self.doc.getElementById('txt_e3') + self.assertIn('text1tspantext2', text.toxml(), + 'Whitespace introduced in text element with and newlines') + + +class GetAttrPrefixRight(unittest.TestCase): + + def runTest(self): + grad = scourXmlFile('unittests/xml-namespace-attrs.svg') \ + .getElementsByTagNameNS(SVGNS, 'linearGradient')[1] + self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', + 'Did not get xlink:href prefix right') + + +class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): + + def runTest(self): + with open('unittests/no-collapse-lines.svg') as f: + s = scourString(f.read()) + self.assertEqual(len(s.splitlines()), 6, + 'Did not properly preserve whitespace on elements even if they were not textual') + + +class HandleEmptyStyleElement(unittest.TestCase): + + def runTest(self): + try: + styles = scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') + fail = len(styles) != 1 + except AttributeError: + fail = True + self.assertEqual(fail, False, + 'Could not handle an empty style element') + + +class EnsureLineEndings(unittest.TestCase): + + def runTest(self): + with open('unittests/newlines.svg') as f: + s = scourString(f.read()) + self.assertEqual(len(s.splitlines()), 24, + 'Did handle reading or outputting line ending characters correctly') + + +class XmlEntities(unittest.TestCase): + + def runTest(self): + self.assertEqual(make_well_formed('<>&'), '<>&', + 'Incorrectly translated unquoted XML entities') + self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_APOS), '<>&', + 'Incorrectly translated single-quoted XML entities') + self.assertEqual(make_well_formed('<>&', XML_ENTS_ESCAPE_QUOT), '<>&', + 'Incorrectly translated double-quoted XML entities') + + self.assertEqual(make_well_formed("'"), "'", + 'Incorrectly translated unquoted single quote') + self.assertEqual(make_well_formed('"'), '"', + 'Incorrectly translated unquoted double quote') + + self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_QUOT), "'", + 'Incorrectly translated double-quoted single quote') + self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_APOS), '"', + 'Incorrectly translated single-quoted double quote') + + self.assertEqual(make_well_formed("'", XML_ENTS_ESCAPE_APOS), ''', + 'Incorrectly translated single-quoted single quote') + self.assertEqual(make_well_formed('"', XML_ENTS_ESCAPE_QUOT), '"', + 'Incorrectly translated double-quoted double quote') + + +class HandleQuotesInAttributes(unittest.TestCase): + + def runTest(self): + with open('unittests/entities.svg', "rb") as f: + output = scourString(f.read()) + self.assertTrue('a="\'"' in output, + 'Failed on attribute value with non-double quote') + self.assertTrue("b='\"'" in output, + 'Failed on attribute value with non-single quote') + self.assertTrue("c=\"''"\"" in output, + 'Failed on attribute value with more single quotes than double quotes') + self.assertTrue('d=\'""'\'' in output, + 'Failed on attribute value with more double quotes than single quotes') + self.assertTrue("e=\"''""\"" in output, + 'Failed on attribute value with the same number of double quotes as single quotes') + + +class PreserveQuotesInStyles(unittest.TestCase): + + def runTest(self): + with open('unittests/quotes-in-styles.svg', "rb") as f: + output = scourString(f.read()) + self.assertTrue('use[id="t"]' in output, + 'Failed to preserve quote characters in a style element') + self.assertTrue("'Times New Roman'" in output, + 'Failed to preserve quote characters in a style attribute') + + +class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/comments.svg') + self.assertEqual(doc.childNodes.length, 4, + 'Did not include all comment children outside of root') + self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEqual(doc.childNodes[1].nodeType, 8, 'Second node not a comment') + self.assertEqual(doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') + + +class DoNotStripDoctype(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/doctype.svg') + self.assertEqual(doc.childNodes.length, 3, + 'Did not include the DOCROOT') + self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEqual(doc.childNodes[1].nodeType, 10, 'Second node not a doctype') + self.assertEqual(doc.childNodes[2].nodeType, 1, 'Third node not the root node') + + +class PathImplicitLineWithMoveCommands(unittest.TestCase): + + def runTest(self): + path = scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", + "Implicit line segments after move not preserved") + + +class RemoveTitlesOption(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-titles'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove tag with --remove-titles') + + +class RemoveDescriptionsOption(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-descriptions'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove 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 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 , <desc> and <metadata> tags with --remove-descriptive-elements') + + +class EnableCommentStrippingOption(unittest.TestCase): + + def runTest(self): + with open('unittests/comment-beside-xml-decl.svg') as f: + docStr = f.read() + docStr = scourString(docStr, + parse_args(['--enable-comment-stripping'])) + self.assertEqual(docStr.find('<!--'), -1, + 'Did not remove document-level comment with --enable-comment-stripping') + + +class StripXmlPrologOption(unittest.TestCase): + + def runTest(self): + with open('unittests/comment-beside-xml-decl.svg') as f: + docStr = f.read() + docStr = scourString(docStr, + parse_args(['--strip-xml-prolog'])) + self.assertEqual(docStr.find('<?xml'), -1, + 'Did not remove <?xml?> with --strip-xml-prolog') + + +class ShortenIDsOption(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/shorten-ids.svg', + parse_args(['--shorten-ids'])) + gradientTag = doc.getElementsByTagName('linearGradient')[0] + self.assertEqual(gradientTag.getAttribute('id'), 'a', + "Did not shorten a linear gradient's ID with --shorten-ids") + rectTag = doc.getElementsByTagName('rect')[0] + self.assertEqual(rectTag.getAttribute('fill'), 'url(#a)', + 'Did not update reference to shortened ID') + + +class ShortenIDsStableOutput(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/shorten-ids-stable-output.svg', + parse_args(['--shorten-ids'])) + use_tags = doc.getElementsByTagName('use') + hrefs_ordered = [x.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + for x in use_tags] + expected = ['#a', '#b', '#b'] + self.assertEqual(hrefs_ordered, expected, + '--shorten-ids pointlessly reassigned ids') + + +class MustKeepGInSwitch(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/groups-in-switch.svg') + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Erroneously removed a <g> in a <switch>') + + +class MustKeepGInSwitch2(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/groups-in-switch-with-id.svg', + parse_args(['--enable-id-stripping'])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Erroneously removed a <g> in a <switch>') + + +class GroupSiblingMerge(unittest.TestCase): + + def test_sibling_merge(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args([])) + self.assertEqual(doc.getElementsByTagName('g').length, 5, + 'Merged sibling <g> tags with similar values') + + def test_sibling_merge_disabled(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args(['--disable-group-collapsing'])) + self.assertEqual(doc.getElementsByTagName('g').length, 8, + 'Sibling merging is disabled by --disable-group-collapsing') + + def test_sibling_merge_crash(self): + doc = scourXmlFile('unittests/group-sibling-merge-crash.svg', + parse_args([''])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Sibling merge should work without causing crashes') + + +class GroupCreation(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/group-creation.svg', + parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Did not create a <g> for a run of elements having similar attributes') + + +class GroupCreationForInheritableAttributesOnly(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/group-creation.svg', + parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').item(0).getAttribute('y'), '', + 'Promoted the uninheritable attribute y to a <g>') + + +class GroupNoCreation(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/group-no-creation.svg', + parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 0, + 'Created a <g> for a run of elements having dissimilar attributes') + + +class GroupNoCreationForTspan(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/group-no-creation-tspan.svg', + parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 0, + 'Created a <g> for a run of <tspan>s ' + 'that are not allowed as children according to content model') + + +class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/commonized-referenced-elements.svg') + self.assertEqual(doc.getElementsByTagName('circle')[0].getAttribute('fill'), '#0f0', + 'Grouped an element referenced elsewhere into a <g>') + + +class DoNotRemoveOverflowVisibleOnMarker(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/overflow-marker.svg') + self.assertEqual(doc.getElementById('m1').getAttribute('overflow'), 'visible', + 'Removed the overflow attribute when it was not using the default value') + self.assertEqual(doc.getElementById('m2').getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + + +class DoNotRemoveOrientAutoOnMarker(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/orient-marker.svg') + self.assertEqual(doc.getElementById('m1').getAttribute('orient'), 'auto', + 'Removed the orient attribute when it was not using the default value') + self.assertEqual(doc.getElementById('m2').getAttribute('orient'), '', + 'Did not remove the orient attribute when it was using the default value') + + +class MarkerOnSvgElements(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/overflow-svg.svg') + self.assertEqual(doc.getElementsByTagName('svg')[0].getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + self.assertEqual(doc.getElementsByTagName('svg')[1].getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + self.assertEqual(doc.getElementsByTagName('svg')[2].getAttribute('overflow'), 'visible', + 'Removed the overflow attribute when it was not using the default value') + + +class GradientReferencedByStyleCDATA(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/style-cdata.svg') + self.assertEqual(len(doc.getElementsByTagName('linearGradient')), 1, + 'Removed a gradient referenced by an internal stylesheet') + + +class ShortenIDsInStyleCDATA(unittest.TestCase): + + def runTest(self): + with open('unittests/style-cdata.svg') as f: + docStr = f.read() + docStr = scourString(docStr, + parse_args(['--shorten-ids'])) + self.assertEqual(docStr.find('somethingreallylong'), -1, + 'Did not shorten IDs in the internal stylesheet') + + +class StyleToAttr(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/style-to-attr.svg') + line = doc.getElementsByTagName('line')[0] + self.assertEqual(line.getAttribute('stroke'), '#000') + self.assertEqual(line.getAttribute('marker-start'), 'url(#m)') + self.assertEqual(line.getAttribute('marker-mid'), 'url(#m)') + self.assertEqual(line.getAttribute('marker-end'), 'url(#m)') + + +class PathCommandRewrites(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/path-command-rewrites.svg') + paths = doc.getElementsByTagName('path') + expected_paths = [ + ('m100 100 200 100', "Trailing m0 0z not removed"), + ('m100 100v200m0 0 100 100z', "Mangled m0 0 100 100"), + ("m100 100v200m0 0 2-1-2 1z", "Should have removed empty m0 0"), + ("m100 100v200l3-5-5 3m0 0 2-1-2 1z", "Rewrite m0 0 3-5-5 3 ... -> l3-5-5 3 ..."), + ("m100 100v200m0 0 3-5-5 3zm0 0 2-1-2 1z", "No rewrite of m0 0 3-5-5 3z"), + ] + self.assertEqual(len(paths), len(expected_paths), "len(actual_paths) != len(expected_paths)") + for i in range(len(paths)): + actual_path = paths[i].getAttribute('d') + expected_path, message = expected_paths[i] + self.assertEqual(actual_path, + expected_path, + '%s: "%s" != "%s"' % (message, actual_path, expected_path)) + + +class DefaultsRemovalToplevel(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', + 'Default attribute fill-rule:nonzero not removed') + + +class DefaultsRemovalToplevelInverse(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', + 'Non-Default attribute fill-rule:evenodd removed') + + +class DefaultsRemovalToplevelFormat(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', + 'Default attribute stroke-width:1.00 not removed') + + +class DefaultsRemovalInherited(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', + 'Default attribute fill-rule:nonzero not removed in child') + + +class DefaultsRemovalInheritedInverse(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', + 'Non-Default attribute fill-rule:evenodd removed in child') + + +class DefaultsRemovalInheritedFormat(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', + 'Default attribute stroke-width:1.00 not removed in child') + + +class DefaultsRemovalOverwrite(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', + 'Default attribute removed, although it overwrites parent element') + + +class DefaultsRemovalOverwriteMarker(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', + 'Default marker attribute removed, although it overwrites parent element') + + +class DefaultsRemovalNonOverwrite(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', + 'Default attribute not removed, although its parent used default') + + +class RemoveDefsWithUnreferencedElements(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/useless-defs.svg') + self.assertEqual(doc.getElementsByTagName('defs').length, 0, + 'Kept defs, although it contains only unreferenced elements') + + +class RemoveDefsWithWhitespace(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/whitespace-defs.svg') + self.assertEqual(doc.getElementsByTagName('defs').length, 0, + 'Kept defs, although it contains only whitespace or is <defs/>') + + +class TransformIdentityMatrix(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity matrix not removed') + + +class TransformRotate135(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', + 'Rotation matrix not converted to rotate(135)') + + +class TransformRotate45(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', + 'Rotation matrix not converted to rotate(45)') + + +class TransformRotate90(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', + 'Rotation matrix not converted to rotate(90)') + + +class TransformRotateCCW135(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', + 'Counter-clockwise rotation matrix not converted to rotate(225)') + + +class TransformRotateCCW45(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', + 'Counter-clockwise rotation matrix not converted to rotate(-45)') + + +class TransformRotateCCW90(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', + 'Counter-clockwise rotation matrix not converted to rotate(-90)') + + +class TransformScale2by3(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', + 'Scaling matrix not converted to scale(2 3)') + + +class TransformScaleMinus1(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', + 'Scaling matrix not converted to scale(-1)') + + +class TransformTranslate(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-matrix-is-translate.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', + 'Translation matrix not converted to translate(2 3)') + + +class TransformRotationRange719_5(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', + 'Transform containing rotate(719.5) not shortened to rotate(-.5)') + + +class TransformRotationRangeCCW540_0(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', + 'Transform containing rotate(-540.0) not shortened to rotate(180)') + + +class TransformRotation3Args(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-rotate-fold-3args.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', + 'Optional zeroes in rotate(angle 0 0) not removed') + + +class TransformIdentityRotation(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-rotate-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity rotation not removed') + + +class TransformIdentitySkewX(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-skewX-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity X-axis skew not removed') + + +class TransformIdentitySkewY(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-skewY-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity Y-axis skew not removed') + + +class TransformIdentityTranslate(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/transform-translate-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity translation not removed') + + +class TransformIdentityScale(unittest.TestCase): + + def runTest(self): + try: + doc = scourXmlFile('unittests/transform-scale-is-identity.svg') + except IndexError: + self.fail("scour failed to handled scale(1) [See GH#190]") + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('scale'), '', + 'Transform containing identity translation not removed') + + +class DuplicateGradientsUpdateStyle(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/duplicate-gradients-update-style.svg', + parse_args(['--disable-style-to-xml'])) + gradient = doc.getElementsByTagName('linearGradient')[0] + rects = doc.getElementsByTagName('rect') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" was not updated to reflect this') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" was not updated to reflect this') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" (with fallback) was not updated to reflect this') + + +class DocWithFlowtext(unittest.TestCase): + + def runTest(self): + with self.assertRaises(Exception): + scourXmlFile('unittests/flowtext.svg', + parse_args(['--error-on-flowtext'])) + + +class DocWithNoFlowtext(unittest.TestCase): + + def runTest(self): + try: + scourXmlFile('unittests/flowtext-less.svg', + parse_args(['--error-on-flowtext'])) + except Exception as e: + self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) + + +class ParseStyleAttribute(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/style.svg') + self.assertEqual(doc.documentElement.getAttribute('style'), + 'property1:value1;property2:value2;property3:value3', + "Style attribute not properly parsed and/or serialized") + + +class StripXmlSpaceAttribute(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/xml-space.svg', + parse_args(['--strip-xml-space'])) + self.assertEqual(doc.documentElement.getAttribute('xml:space'), '', + "'xml:space' attribute not removed from root SVG element" + "when '--strip-xml-space' was specified") + self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', + "'xml:space' attribute removed from a child element " + "when '--strip-xml-space' was specified (should only operate on root SVG element)") + + +class DoNotStripXmlSpaceAttribute(unittest.TestCase): + + def runTest(self): + doc = scourXmlFile('unittests/xml-space.svg') + self.assertNotEqual(doc.documentElement.getAttribute('xml:space'), '', + "'xml:space' attribute removed from root SVG element" + "when '--strip-xml-space' was NOT specified") + self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', + "'xml:space' attribute removed from a child element " + "when '--strip-xml-space' was NOT specified (should never be removed!)") + + +class CommandLineUsage(unittest.TestCase): + + USAGE_STRING = "Usage: scour [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]" + MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<svg xmlns="http://www.w3.org/2000/svg"/>\n' + TEMP_SVG_FILE = 'testscour_temp.svg' + + # wrapper function for scour.run() to emulate command line usage + # + # returns an object with the following attributes: + # status: the exit status + # stdout: a string representing the combined output to 'stdout' + # stderr: a string representing the combined output to 'stderr' + def _run_scour(self): + class Result(object): + pass + + result = Result() + try: + run() + result.status = 0 + except SystemExit as exception: # catch any calls to sys.exit() + result.status = exception.code + result.stdout = self.temp_stdout.getvalue() + result.stderr = self.temp_stderr.getvalue() + + return result + + def setUp(self): + # store current values of 'argv', 'stdin', 'stdout' and 'stderr' + self.argv = sys.argv + self.stdin = sys.stdin + self.stdout = sys.stdout + self.stderr = sys.stderr + + # start with a fresh 'argv' + sys.argv = ['scour'] # TODO: Do we need a (more) valid 'argv[0]' for anything? + + # create 'stdin', 'stdout' and 'stderr' with behavior close to the original + # TODO: can we create file objects that behave *exactly* like the original? + # this is a mess since we have to ensure compatibility across Python 2 and 3 and it seems impossible + # to replicate all the details of 'stdin', 'stdout' and 'stderr' + class InOutBuffer(six.StringIO, object): + def write(self, string): + try: + return super(InOutBuffer, self).write(string) + except TypeError: + return super(InOutBuffer, self).write(string.decode()) + + sys.stdin = self.temp_stdin = InOutBuffer() + sys.stdout = self.temp_stdout = InOutBuffer() + sys.stderr = self.temp_stderr = InOutBuffer() + + self.temp_stdin.name = '<stdin>' # Scour wants to print the name of the input file... + + def tearDown(self): + # restore previous values of 'argv', 'stdin', 'stdout' and 'stderr' + sys.argv = self.argv + sys.stdin = self.stdin + sys.stdout = self.stdout + sys.stderr = self.stderr + + # clean up + self.temp_stdin.close() + self.temp_stdout.close() + self.temp_stderr.close() + + def test_no_arguments(self): + # we have to pretend that our input stream is a TTY, otherwise Scour waits for input from stdin + self.temp_stdin.isatty = lambda: True + + result = self._run_scour() + + self.assertEqual(result.status, 2, "Execution of 'scour' without any arguments should exit with status '2'") + self.assertTrue(self.USAGE_STRING in result.stderr, + "Usage information not displayed when calling 'scour' without any arguments") + + def test_version(self): + sys.argv.append('--version') + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Execution of 'scour --version' erorred'") + self.assertEqual(__version__ + "\n", result.stdout, "Unexpected output of 'scour --version'") + + def test_help(self): + sys.argv.append('--help') + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Execution of 'scour --help' erorred'") + self.assertTrue(self.USAGE_STRING in result.stdout and 'Options:' in result.stdout, + "Unexpected output of 'scour --help'") + + def test_stdin_stdout(self): + sys.stdin.write(self.MINIMAL_SVG) + sys.stdin.seek(0) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour via 'stdin' / 'stdout' erorred'") + self.assertEqual(result.stdout, self.MINIMAL_SVG, "Unexpected SVG output via 'stdout'") + + def test_filein_fileout_named(self): + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as named parameters errored'") + with open(self.TEMP_SVG_FILE) as file: + file_content = file.read() + self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") + os.remove(self.TEMP_SVG_FILE) + + def test_filein_fileout_positional(self): + sys.argv.extend(['unittests/minimal.svg', self.TEMP_SVG_FILE]) + + result = self._run_scour() + + self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as positional parameters errored'") + with open(self.TEMP_SVG_FILE) as file: + file_content = file.read() + self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") + os.remove(self.TEMP_SVG_FILE) + + def test_quiet(self): + sys.argv.append('-q') + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + os.remove(self.TEMP_SVG_FILE) + + self.assertEqual(result.status, 0, "Execution of 'scour -q ...' erorred'") + self.assertEqual(result.stdout, '', "Output writtent to 'stdout' when '--quiet' options was used") + self.assertEqual(result.stderr, '', "Output writtent to 'stderr' when '--quiet' options was used") + + def test_verbose(self): + sys.argv.append('-v') + sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) + + result = self._run_scour() + os.remove(self.TEMP_SVG_FILE) + + self.assertEqual(result.status, 0, "Execution of 'scour -v ...' erorred'") + self.assertEqual(result.stdout.count('Number'), 14, + "Statistics output not as expected when '--verbose' option was used") + self.assertEqual(result.stdout.count(': 0'), 14, + "Statistics output not as expected when '--verbose' option was used") + + +class EmbedRasters(unittest.TestCase): + + # quick way to ping a host using the OS 'ping' command and return the execution result + def _ping(host): + import os + import platform + + # work around https://github.com/travis-ci/travis-ci/issues/3080 as pypy throws if 'ping' can't be executed + import distutils.spawn + if not distutils.spawn.find_executable('ping'): + return -1 + + system = platform.system().lower() + ping_count = '-n' if system == 'windows' else '-c' + dev_null = 'NUL' if system == 'windows' else '/dev/null' + + return os.system('ping ' + ping_count + ' 1 ' + host + ' > ' + dev_null) + + def test_disable_embed_rasters(self): + doc = scourXmlFile('unittests/raster-formats.svg', + parse_args(['--disable-embed-rasters'])) + self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), 'raster.png', + "Raster image embedded when '--disable-embed-rasters' was specified") + + def test_raster_formats(self): + doc = scourXmlFile('unittests/raster-formats.svg') + self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAABAgMAAABmjvwnAAAAC' + 'VBMVEUAAP//AAAA/wBmtfVOAAAACklEQVQI12NIAAAAYgBhGxZhsAAAAABJRU5ErkJggg==', + "Raster image (PNG) not correctly embedded.") + self.assertEqual(doc.getElementById('gif').getAttribute('xlink:href'), + 'data:image/gif;base64,R0lGODdhAwABAKEDAAAA//8AAAD/AP///ywAAAAAAwABAAACAoxQADs=', + "Raster image (GIF) not correctly embedded.") + self.assertEqual(doc.getElementById('jpg').getAttribute('xlink:href'), + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/' + '2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' + '2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' + 'wAARCAABAAMDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABoQAAEFAQAAAAAAAAAAAAAAAAgABQc3d7j/' + 'xAAVAQEBAAAAAAAAAAAAAAAAAAAHCv/EABwRAAEDBQAAAAAAAAAAAAAAAAgAB7gJODl2eP/aAAwDAQACEQMRAD8AMeaF' + '/u2aj5z1Fqp7oN4rxx2kn5cPuhV6LkzG7qOyYL2r/9k=', + "Raster image (JPG) not correctly embedded.") + + def test_raster_paths_local(self): + doc = scourXmlFile('unittests/raster-paths-local.svg') + images = doc.getElementsByTagName('image') + for image in images: + href = image.getAttribute('xlink:href') + self.assertTrue(href.startswith('data:image/'), + "Raster image from local path '" + href + "' not embedded.") + + def test_raster_paths_local_absolute(self): + with open('unittests/raster-formats.svg', 'r') as f: + svg = f.read() + + # create a reference string by scouring the original file with relative links + options = ScourOptions + options.infilename = 'unittests/raster-formats.svg' + reference_svg = scourString(svg, options) + + # this will not always create formally valid paths but it'll check how robust our implementation is + # (the third path is invalid for sure because file: needs three slashes according to URI spec) + svg = svg.replace('raster.png', + '/' + os.path.abspath(os.path.dirname(__file__)) + '\\unittests\\raster.png') + svg = svg.replace('raster.gif', + 'file:///' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.gif') + svg = svg.replace('raster.jpg', + 'file:/' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.jpg') + + svg = scourString(svg) + + self.assertEqual(svg, reference_svg, + "Raster images from absolute local paths not properly embedded.") + + @unittest.skipIf(_ping('raw.githubusercontent.com') != 0, "Remote server not reachable.") + def test_raster_paths_remote(self): + doc = scourXmlFile('unittests/raster-paths-remote.svg') + images = doc.getElementsByTagName('image') + for image in images: + href = image.getAttribute('xlink:href') + self.assertTrue(href.startswith('data:image/'), + "Raster image from remote path '" + href + "' not embedded.") + + +class ViewBox(unittest.TestCase): + + def test_viewbox_create(self): + doc = scourXmlFile('unittests/viewbox-create.svg', parse_args(['--enable-viewboxing'])) + viewBox = doc.documentElement.getAttribute('viewBox') + self.assertEqual(viewBox, '0 0 123.46 654.32', "viewBox not properly created with '--enable-viewboxing'.") + + def test_viewbox_remove_width_and_height(self): + doc = scourXmlFile('unittests/viewbox-remove.svg', parse_args(['--enable-viewboxing'])) + width = doc.documentElement.getAttribute('width') + height = doc.documentElement.getAttribute('height') + self.assertEqual(width, '', "width not removed with '--enable-viewboxing'.") + self.assertEqual(height, '', "height not removed with '--enable-viewboxing'.") + + +# TODO: write tests for --keep-editor-data + +if __name__ == '__main__': + testcss = __import__('test_css') + scour = __import__('__main__') + suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) + unittest.main(defaultTest="suite") diff -Nru scour-0.37/testscour.py scour-0.38.2/testscour.py --- scour-0.37/testscour.py 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/testscour.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,2724 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Test Harness for Scour -# -# Copyright 2010 Jeff Schiller -# Copyright 2010 Louis Simard -# -# This file is part of Scour, http://www.codedread.com/scour/ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import print_function # use print() as a function in Python 2 (see PEP 3105) -from __future__ import absolute_import # use absolute imports by default in Python 2 (see PEP 328) - -import os -import sys -import unittest - -import six -from six.moves import map, range - -from scour.scour import 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 Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling "scourString" with empty options object') - - def test_scourXmlFile(self): - options = ScourOptions - try: - scourXmlFile('unittests/minimal.svg', options) - fail = False - except Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling "scourXmlFile" with empty options object') - - def test_start(self): - options = ScourOptions - input = open('unittests/minimal.svg', 'rb') - output = open('testscour_temp.svg', 'wb') - - stdout_temp = sys.stdout - sys.stdout = None - try: - start(options, input, output) - fail = False - except Exception: - fail = True - sys.stdout = stdout_temp - - os.remove('testscour_temp.svg') - - self.assertEqual(fail, False, - 'Exception when calling "start" with empty options object') - - -class InvalidOptions(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.invalidOption = "invalid value" - try: - scourXmlFile('unittests/ids-to-strip.svg', options) - fail = False - except Exception: - fail = True - self.assertEqual(fail, False, - 'Exception when calling Scour with invalid options') - - -class GetElementById(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids.svg') - self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') - self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') - self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') - self.assertIsNotNone(doc.getElementById('rect1'), 'rect not found by ID') - self.assertIsNone(doc.getElementById('rect2'), 'Non-existing element found by ID') - - -class NoInkscapeElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), - False, - 'Found Inkscape elements') - - -class NoSodipodiElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), - False, - 'Found Sodipodi elements') - - -class NoAdobeIllustratorElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), - False, - 'Found Adobe Illustrator elements') - - -class NoAdobeGraphsElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), - False, - 'Found Adobe Graphs elements') - - -class NoAdobeSVGViewerElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), - False, - 'Found Adobe SVG Viewer elements') - - -class NoAdobeVariablesElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), - False, - 'Found Adobe Variables elements') - - -class NoAdobeSaveForWebElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), - False, - 'Found Adobe Save For Web elements') - - -class NoAdobeExtensibilityElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), - False, - 'Found Adobe Extensibility elements') - - -class NoAdobeFlowsElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), - False, - 'Found Adobe Flows elements') - - -class NoAdobeImageReplacementElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), - False, - 'Found Adobe Image Replacement elements') - - -class NoAdobeCustomElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), - False, - 'Found Adobe Custom elements') - - -class NoAdobeXPathElements(unittest.TestCase): - - def runTest(self): - self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), - False, - 'Found Adobe XPath elements') - - -class DoNotRemoveTitleWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child') - - -class RemoveEmptyTitleElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element') - - -class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child') - - -class RemoveEmptyDescriptionElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element') - - -class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child') - - -class RemoveEmptyMetadataElement(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element') - - -class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child') - - -class RemoveEmptyDescriptiveElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element') - - -class RemoveEmptyGElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/empty-g.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Did not remove empty g element') - - -class RemoveUnreferencedPattern(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-pattern.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern not removed') - - -class RemoveUnreferencedLinearGradient(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-linearGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Unreferenced linearGradient not removed') - - -class RemoveUnreferencedRadialGradient(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-radialGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, - 'Unreferenced radialGradient not removed') - - -class RemoveUnreferencedElementInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Unreferenced rect left in defs') - - -class RemoveUnreferencedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-defs.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, - 'Unreferenced radialGradient left in defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern left in defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 0, - 'Unreferenced circle left in defs') - - -class KeepUnreferencedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/unreferenced-defs.svg', - parse_args(['--keep-unreferenced-defs'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, - 'Unreferenced radialGradient removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 1, - 'Unreferenced pattern removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs with `--keep-unreferenced-defs`') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 1, - 'Unreferenced circle removed from defs with `--keep-unreferenced-defs`') - - -class DoNotRemoveChainedRefsInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/refs-in-defs.svg') - g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] - self.assertEqual(g.childNodes.length >= 2, True, - 'Chained references not honored in defs') - - -class KeepTitleInDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Title removed from in defs') - - -class RemoveNestedDefs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-defs.svg') - allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') - self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') - - -class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids-to-strip.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', - '<svg> ID stripped when it should be disabled') - - -class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/ids-to-strip.svg', - parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', - '<svg> ID not stripped') - - -class ProtectIDs(unittest.TestCase): - - def test_protect_none(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' not stripped when none of the '--protect-ids-_' options was specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' not stripped when none of the '--protect-ids-_' options was specified") - - def test_protect_ids_noninkscape(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-noninkscape'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' should have been stripped despite '--protect-ids-noninkscape' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), 'text_custom', - "ID 'text_custom' should NOT have been stripped because of '--protect-ids-noninkscape'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' should have been stripped despite '--protect-ids-noninkscape' being specified") - - def test_protect_ids_list(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-list=text2,text3'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-list' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), 'text2', - "ID 'text2' should NOT have been stripped because of '--protect-ids-list'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), 'text3', - "ID 'text3' should NOT have been stripped because of '--protect-ids-list'") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' should have been stripped despite '--protect-ids-list' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), '', - "ID 'my_text1' should have been stripped despite '--protect-ids-list' being specified") - - def test_protect_ids_prefix(self): - doc = scourXmlFile('unittests/ids-protect.svg', - parse_args(['--enable-id-stripping', '--protect-ids-prefix=my'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[0].getAttribute('id'), '', - "ID 'text1' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[1].getAttribute('id'), '', - "ID 'text2' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[2].getAttribute('id'), '', - "ID 'text3' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[3].getAttribute('id'), '', - "ID 'text_custom' should have been stripped despite '--protect-ids-prefix' being specified") - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'text')[4].getAttribute('id'), 'my_text1', - "ID 'my_text1' should NOT have been stripped because of '--protect-ids-prefix'") - - -class RemoveUselessNestedGroups(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-useless-groups.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Useless nested groups not removed') - - -class DoNotRemoveUselessNestedGroups(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/nested-useless-groups.svg', - parse_args(['--disable-group-collapsing'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Useless nested groups were removed despite --disable-group-collapsing') - - -class DoNotRemoveNestedGroupsWithTitle(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with title was removed') - - -class DoNotRemoveNestedGroupsWithDesc(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with desc was removed') - - -class RemoveDuplicateLinearGradientStops(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops not removed') - - -class RemoveDuplicateLinearGradientStopsPct(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops with percentages not removed') - - -class RemoveDuplicateRadialGradientStops(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate radial gradient stops not removed') - - -class NoSodipodiNamespaceDecl(unittest.TestCase): - - def runTest(self): - attrs = scourXmlFile('unittests/sodipodi.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'Sodipodi namespace declaration found') - - -class NoInkscapeNamespaceDecl(unittest.TestCase): - - def runTest(self): - attrs = scourXmlFile('unittests/inkscape.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://www.inkscape.org/namespaces/inkscape', - 'Inkscape namespace declaration found') - - -class NoSodipodiAttributes(unittest.TestCase): - - def runTest(self): - def findSodipodiAttr(elem): - attrs = elem.attributes - if attrs is None: - return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - return False - return True - self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), - False, - 'Found Sodipodi attributes') - - -class NoInkscapeAttributes(unittest.TestCase): - - def runTest(self): - def findInkscapeAttr(elem): - attrs = elem.attributes - if attrs is None: - return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': - return False - return True - self.assertNotEqual(walkTree(scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), - False, - 'Found Inkscape attributes') - - -class KeepInkscapeNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Inkscape namespace declaration when using --keep-editor-data") - return False - - -class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Sodipodi namespace declaration when using --keep-editor-data") - return False - - -class KeepReferencedFonts(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/referenced-font.svg') - fonts = doc.documentElement.getElementsByTagNameNS(SVGNS, 'font') - self.assertEqual(len(fonts), 1, - 'Font wrongly removed from <defs>') - - -class ConvertStyleToAttrs(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', - 'style attribute not emptied') - - -class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when stroke opacity zero') - - -class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when stroke opacity zero') - - -class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when stroke opacity zero') - - -class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when stroke opacity zero') - - -class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when stroke opacity zero') - - -class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when stroke opacity zero') - - -class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when width zero') - - -class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when width zero') - - -class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when width zero') - - -class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when width zero') - - -class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when width zero') - - -class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when width zero') - - -class RemoveStrokeWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when no stroke') - - -class KeepStrokeWhenInheritedFromParent(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', - 'stroke attribute removed despite a different value being inherited from a parent') - - -class KeepStrokeWhenInheritedByChild(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', - 'stroke attribute removed despite it being inherited by a child') - - -class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when no stroke') - - -class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', - 'stroke-width attribute removed despite it being inherited by a child') - - -class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when no stroke') - - -class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when no stroke') - - -class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when no stroke') - - -class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when no stroke') - - -class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when no stroke') - - -class RemoveFillRuleWhenFillNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', - 'fill-rule attribute not emptied when no fill') - - -class RemoveFillOpacityWhenFillNone(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', - 'fill-opacity attribute not emptied when no fill') - - -class ConvertFillPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg', - parse_args(['--disable-simplify-colors'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', - 'fill property not converted to XML attribute') - - -class ConvertFillOpacityPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', - 'fill-opacity property not converted to XML attribute') - - -class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', - 'fill-rule property not converted to XML attribute') - - -class CollapseSinglyReferencedGradients(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Singly-referenced linear gradient not collapsed') - - -class InheritGradientUnitsUponCollapsing(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), - 'userSpaceOnUse', - 'gradientUnits not properly inherited when collapsing gradients') - - -class OverrideGradientUnitsUponCollapsing(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', - 'gradientUnits not properly overrode when collapsing gradients') - - -class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/dont-collapse-gradients.svg') - self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Multiply-referenced linear gradient collapsed') - - -class RemoveTrailingZerosFromPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[:4] == 'm300' and path[4] != '.', True, - 'Trailing zeros not removed from path data') - - -class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros-calc.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path, 'm5.81 0h0.1', - 'Trailing zeros not removed from path data after calculation') - - -class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[4], '-', - 'Delimiters not removed before negative coordinates in path data') - - -class UseScientificNotationToShortenCoordsInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-use-scientific-notation.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path, 'm1e4 0', - 'Not using scientific notation for path coord when representation is shorter') - - -class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-abs-to-rel.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[1][0], 'v', - 'Absolute V command not converted to relative v command') - self.assertEqual(float(path[1][1][0]), -20.0, - 'Absolute V value not converted to relative v value') - - -class RoundPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(float(path[0][1][0]), 100.0, - 'Not rounding down') - self.assertEqual(float(path[0][1][1]), 100.0, - 'Not rounding up') - - -class LimitPrecisionInPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(float(path[1][1][0]), 100.01, - 'Not correctly limiting precision on path data') - - -class KeepPrecisionInPathDataIfSameLength(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=1'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4e3 5e4 7e5", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4e3 -5e4 -7e5", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=1" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=2'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 6.5e5", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-6.5e5", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=2" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=3'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123 101-123-101", - 'Precision not correctly reduced with "--set-precision=3" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - doc = scourXmlFile('unittests/path-precision.svg', parse_args(['--set-precision=4'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths[1:3]: - self.assertEqual(path.getAttribute('d'), "m1 21 321 4321 54321 654321", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + path.getAttribute('id')) - self.assertEqual(paths[4].getAttribute('d'), "m-1-21-321-4321-54321-654321", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + paths[4].getAttribute('id')) - self.assertEqual(paths[5].getAttribute('d'), "m123.5 101-123.5-101", - 'Precision not correctly reduced with "--set-precision=4" ' - 'for path with ID ' + paths[5].getAttribute('id')) - - -class LimitPrecisionInControlPointPathData(unittest.TestCase): - - def runTest(self): - path_data = ("m1.1 2.2 3.3 4.4m-4.4-6.7" - "c1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7 1 2 3 4 5.6 6.7m-17-20" - "s1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4m-10-13" - "q1 2 3.3 4.4 1 2 3.3 4.4 1 2 3.3 4.4") - doc = scourXmlFile('unittests/path-precision-control-points.svg', - parse_args(['--set-precision=2', '--set-c-precision=1'])) - path_data2 = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path_data2, path_data, - 'Not correctly limiting precision on path data with --set-c-precision') - - -class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[4][0], 'z', - 'Did not remove an empty line segment from path') - - -class RemoveEmptySegmentsFromPathWithButtLineCaps(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) - for id in ['none', 'attr_butt', 'style_butt']: - path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) - self.assertEqual(len(path), 1, - 'Did not remove empty segments when path had butt linecaps') - - -class DoNotRemoveEmptySegmentsFromPathWithRoundSquareLineCaps(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-with-caps.svg', parse_args(['--disable-style-to-xml'])) - for id in ['attr_round', 'attr_square', 'style_round', 'style_square']: - path = svg_parser.parse(doc.getElementById(id).getAttribute('d')) - self.assertEqual(len(path), 2, - 'Did remove empty segments when path had round or square linecaps') - - -class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[1][0], 'h', - 'Did not change line to horizontal line segment in path') - self.assertEqual(float(path[1][1][0]), 200.0, - 'Did not calculate horizontal line segment in path correctly') - - -class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-line-optimize.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[2][0], 'v', - 'Did not change line to vertical line segment in path') - self.assertEqual(float(path[2][1][0]), 100.0, - 'Did not calculate vertical line segment in path correctly') - - -class ChangeBezierToShorthandInPath(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-bez-optimize.svg') - self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', - 'Did not change bezier curves into shorthand curve segments in path') - self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', - 'Did not change bezier curve into shorthand curve segment when first control point ' - 'is the current point and previous command was not a bezier curve') - self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', - 'Did change bezier curve into shorthand curve segment when first control point ' - 'is the current point but previous command was a bezier curve with a different control point') - - -class ChangeQuadToShorthandInPath(unittest.TestCase): - - def runTest(self): - path = scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', - 'Did not change quadratic curves into shorthand curve segments in path') - - -class BooleanFlagsInEllipticalPath(unittest.TestCase): - - def test_omit_spaces(self): - doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--no-renderer-workaround'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths: - self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 00100 50', - 'Did not ommit spaces after boolean flags in elliptical arg path command') - - def test_output_spaces_with_renderer_workaround(self): - doc = scourXmlFile('unittests/path-elliptical-flags.svg', parse_args(['--renderer-workaround'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - for path in paths: - self.assertEqual(path.getAttribute('d'), 'm0 0a100 50 0 0 0 100 50', - 'Did not output spaces after boolean flags in elliptical arg path command ' - 'with renderer workaround') - - -class DoNotOptimzePathIfLarger(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertTrue(len(p.getAttribute('d')) <= - # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): - # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" - len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), - 'Made path data longer during optimization') - - -class HandleEncodingUTF8(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/encoding-utf8.svg') - text = u'Hello in many languages:\n' \ - u'ar: أهلا\n' \ - u'bn: হ্যালো\n' \ - u'el: Χαίρετε\n' \ - u'en: Hello\n' \ - u'hi: नमस्ते\n' \ - u'iw: שלום\n' \ - u'ja: こんにちは\n' \ - u'km: ជំរាបសួរ\n' \ - u'ml: ഹലോ\n' \ - u'ru: Здравствуйте\n' \ - u'ur: ہیلو\n' \ - u'zh: 您好' - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual(desc, text, - 'Did not handle international UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() - self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', - 'Did not handle common UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() - self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', - 'Did not handle mathematical UTF8 characters') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() - self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', - 'Did not handle superscript/subscript UTF8 characters') - - -class HandleEncodingISO_8859_15(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/encoding-iso-8859-15.svg') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual(desc, u'áèîäöü߀ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters') - - -class HandleSciNoInPathData(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-sn.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, - 'Did not handle scientific notation in path data') - - -class TranslateRGBIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(elem.getAttribute('fill'), '#0f1011', - 'Not converting rgb into hex') - - -class TranslateRGBPctIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] - self.assertEqual(elem.getAttribute('stop-color'), '#7f0000', - 'Not converting rgb pct into hex') - - -class TranslateColorNamesIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(elem.getAttribute('stroke'), '#a9a9a9', - 'Not converting standard color names into hex') - - -class TranslateExtendedColorNamesIntoHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] - self.assertEqual(elem.getAttribute('solid-color'), '#fafad2', - 'Not converting extended color names into hex') - - -class TranslateLongHexColorIntoShortHex(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] - self.assertEqual(elem.getAttribute('fill'), '#fff', - 'Not converting long hex color into short hex') - - -class DoNotConvertShortColorNames(unittest.TestCase): - - def runTest(self): - elem = scourXmlFile('unittests/dont-convert-short-color-names.svg') \ - .getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual('red', elem.getAttribute('fill'), - 'Converted short color name to longer hex string') - - -class AllowQuotEntitiesInUrl(unittest.TestCase): - - def runTest(self): - grads = scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grads), 1, - 'Removed referenced gradient when " was in the url') - - -class RemoveFontStylesFromNonTextShapes(unittest.TestCase): - - def runTest(self): - r = scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual(r.getAttribute('font-size'), '', - 'font-size not removed from rect') - - -class CollapseStraightPathSegments(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/collapse-straight-path-segments.svg', parse_args(['--disable-style-to-xml'])) - paths = doc.getElementsByTagNameNS(SVGNS, 'path') - path_data = [path.getAttribute('d') for path in paths] - path_data_expected = ['m0 0h30', - 'm0 0v30', - 'm0 0h10.5v10.5', - 'm0 0h10-1v10-1', - 'm0 0h30', - 'm0 0h30', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0h10 20', - 'm0 0 20 40v1l10 20', - 'm0 0 10 10-20-20 10 10-20-20', - 'm0 0 1 2m1 2 2 4m1 2 2 4', - 'm6.3228 7.1547 81.198 45.258'] - - self.assertEqual(path_data[0:3], path_data_expected[0:3], - 'Did not collapse h/v commands into a single h/v commands') - self.assertEqual(path_data[3], path_data_expected[3], - 'Collapsed h/v commands with different direction') - self.assertEqual(path_data[4:6], path_data_expected[4:6], - 'Did not collapse h/v commands with only start/end markers present') - self.assertEqual(path_data[6:10], path_data_expected[6:10], - 'Did not preserve h/v commands with intermediate markers present') - - self.assertEqual(path_data[10], path_data_expected[10], - 'Did not collapse lineto commands into a single (implicit) lineto command') - self.assertEqual(path_data[11], path_data_expected[11], - 'Collapsed lineto commands with different direction') - self.assertEqual(path_data[12], path_data_expected[12], - 'Collapsed first parameter pair of a moveto subpath') - self.assertEqual(path_data[13], path_data_expected[13], - 'Did not collapse the nodes of a straight real world path') - - -class ConvertStraightCurvesToLines(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10 40 40 40-40z', - 'Did not convert straight curves into lines') - - -class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - self.assertEqual(p.getAttribute('points'), '50 50 150 50 150 150 50 150', - 'Unnecessary polygon end point not removed') - - -class DoNotRemovePolgonLastPoint(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] - self.assertEqual(p.getAttribute('points'), '200 50 300 50 300 150 200 150', - 'Last point of polygon removed') - - -class ScourPolygonCoordsSciNo(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - self.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polygon coordinates not scoured') - - -class ScourPolylineCoordsSciNo(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polyline coordinates not scoured') - - -class ScourPolygonNegativeCoords(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - # points="100,-100,100-100,100-100-100,-100-100,200" /> - self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') - - -class ScourPolylineNegativeCoords(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') - - -class ScourPolygonNegativeCoordFirst(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - # points="-100,-100,100-100,100-100-100,-100-100,200" /> - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') - - -class ScourPolylineNegativeCoordFirst(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') - - -class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): - - def runTest(self): - f = scourXmlFile('unittests/important-groups-in-defs.svg') - self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Group in defs with id\'ed element removed') - - -class AlwaysKeepClosePathSegments(unittest.TestCase): - - def runTest(self): - p = scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', - 'Path with closepath not preserved') - - -class RemoveDuplicateLinearGradients(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(1, lingrads.length, - 'Duplicate linear gradient not removed') - - -class RereferenceForLinearGradient(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), - 'Reference not updated after removing duplicate linear gradient') - self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), - 'Reference not updated after removing duplicate linear gradient') - - -class RemoveDuplicateRadialGradients(unittest.TestCase): - - def runTest(self): - svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') - radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(1, radgrads.length, - 'Duplicate radial gradient not removed') - - -class 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 RemoveDefaultAttributeOrderSVGLengthCrash(unittest.TestCase): - - # Triggered a crash in v0.36 - def runTest(self): - try: - scourXmlFile('unittests/remove-default-attr-order.svg') - except AttributeError: - self.fail("Processing the order attribute triggered an AttributeError") - - -class RemoveDefaultAttributeStdDeviationSVGLengthCrash(unittest.TestCase): - - # Triggered a crash in v0.36 - def runTest(self): - try: - scourXmlFile('unittests/remove-default-attr-std-deviation.svg') - except AttributeError: - self.fail("Processing the order attribute triggered an AttributeError") - - -class CDATAInXml(unittest.TestCase): - - def runTest(self): - with open('unittests/cdata.svg') as f: - lines = scourString(f.read()).splitlines() - self.assertEqual(lines[3], - " alert('pb&j');", - 'CDATA did not come out correctly') - - -class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('unicode="<"') != -1, - "Improperly serialized < in attribute value") - - -class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('unicode="&"') != -1, - 'Improperly serialized & in attribute value') - - -class WellFormedXMLLesserThanInTextContent(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('<title>2 < 5') != -1, - 'Improperly serialized < in text content') - - -class WellFormedXMLAmpersandInTextContent(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('Peanut Butter & Jelly') != -1, - 'Improperly serialized & in text content') - - -class WellFormedXMLNamespacePrefixRemoveUnused(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:foo=') == -1, - 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') - - -class WellFormedXMLNamespacePrefixKeepUsedElementPrefix(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:bar=') != -1, - 'Improperly serialized namespace prefix declarations: Used element prefix removed') - - -class WellFormedXMLNamespacePrefixKeepUsedAttributePrefix(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scourString(f.read()) - self.assertTrue(wellformed.find('xmlns:baz=') != -1, - 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') - - -class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scourString(f.read()) - self.assertTrue(xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, - 'Improperly serialized namespace prefix declarations when not in default namespace') - - -class MoveSVGElementsToDefaultNamespace(unittest.TestCase): - - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scourString(f.read()) - self.assertTrue(xmlstring.find(' does not inherit xml:space="preserve" of parent text element') - text = self.doc.getElementById('txt_c2') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="default" of does not overwrite xml:space="preserve" of parent text element') - text = self.doc.getElementById('txt_c3') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="preserve" of does not overwrite xml:space="default" of parent text element') - text = self.doc.getElementById('txt_c4') - self.assertIn('text1 text2', text.toxml(), - ' does not inherit xml:space="preserve" of parent group') - text = self.doc.getElementById('txt_c5') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="default" of text element does not overwrite xml:space="preserve" of parent group') - text = self.doc.getElementById('txt_c6') - self.assertIn('text1 text2', text.toxml(), - 'xml:space="preserve" of text element does not overwrite xml:space="default" of parent group') - - def test_important_whitespace(self): - text = self.doc.getElementById('txt_d1') - self.assertIn('text1 text2', text.toxml(), - 'Newline with whitespace collapsed in text element') - text = self.doc.getElementById('txt_d2') - self.assertIn('text1 tspan1 text2', text.toxml(), - 'Whitespace stripped from the middle of a text element') - text = self.doc.getElementById('txt_d3') - self.assertIn('text1 tspan1 tspan2 text2', text.toxml(), - 'Whitespace stripped from the middle of a text element') - - def test_incorrect_whitespace(self): - text = self.doc.getElementById('txt_e1') - self.assertIn('text1text2', text.toxml(), - 'Whitespace introduced in text element with newline') - text = self.doc.getElementById('txt_e2') - self.assertIn('text1tspantext2', text.toxml(), - 'Whitespace introduced in text element with ') - text = self.doc.getElementById('txt_e3') - self.assertIn('text1tspantext2', text.toxml(), - 'Whitespace introduced in text element with and newlines') - - -class GetAttrPrefixRight(unittest.TestCase): - - def runTest(self): - grad = scourXmlFile('unittests/xml-namespace-attrs.svg') \ - .getElementsByTagNameNS(SVGNS, 'linearGradient')[1] - self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', - 'Did not get xlink:href prefix right') - - -class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): - - def runTest(self): - with open('unittests/no-collapse-lines.svg') as f: - s = scourString(f.read()) - self.assertEqual(len(s.splitlines()), 6, - 'Did not properly preserve whitespace on elements even if they were not textual') - - -class HandleEmptyStyleElement(unittest.TestCase): - - def runTest(self): - try: - styles = scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') - fail = len(styles) != 1 - except AttributeError: - fail = True - self.assertEqual(fail, False, - 'Could not handle an empty style element') - - -class EnsureLineEndings(unittest.TestCase): - - def runTest(self): - with open('unittests/newlines.svg') as f: - s = scourString(f.read()) - self.assertEqual(len(s.splitlines()), 24, - 'Did handle reading or outputting line ending characters correctly') - - -class XmlEntities(unittest.TestCase): - - def runTest(self): - self.assertEqual(makeWellFormed('<>&'), '<>&', - 'Incorrectly translated unquoted XML entities') - self.assertEqual(makeWellFormed('<>&', "'"), '<>&', - 'Incorrectly translated single-quoted XML entities') - self.assertEqual(makeWellFormed('<>&', '"'), '<>&', - 'Incorrectly translated double-quoted XML entities') - - self.assertEqual(makeWellFormed("'"), "'", - 'Incorrectly translated unquoted single quote') - self.assertEqual(makeWellFormed('"'), '"', - 'Incorrectly translated unquoted double quote') - - self.assertEqual(makeWellFormed("'", '"'), "'", - 'Incorrectly translated double-quoted single quote') - self.assertEqual(makeWellFormed('"', "'"), '"', - 'Incorrectly translated single-quoted double quote') - - self.assertEqual(makeWellFormed("'", "'"), ''', - 'Incorrectly translated single-quoted single quote') - self.assertEqual(makeWellFormed('"', '"'), '"', - 'Incorrectly translated double-quoted double quote') - - -class HandleQuotesInAttributes(unittest.TestCase): - - def runTest(self): - with open('unittests/entities.svg', "rb") as f: - output = scourString(f.read()) - self.assertTrue('a="\'"' in output, - 'Failed on attribute value with non-double quote') - self.assertTrue("b='\"'" in output, - 'Failed on attribute value with non-single quote') - self.assertTrue("c=\"''"\"" in output, - 'Failed on attribute value with more single quotes than double quotes') - self.assertTrue('d=\'""'\'' in output, - 'Failed on attribute value with more double quotes than single quotes') - self.assertTrue("e=\"''""\"" in output, - 'Failed on attribute value with the same number of double quotes as single quotes') - - -class PreserveQuotesInStyles(unittest.TestCase): - - def runTest(self): - with open('unittests/quotes-in-styles.svg', "rb") as f: - output = scourString(f.read()) - self.assertTrue('use[id="t"]' in output, - 'Failed to preserve quote characters in a style element') - self.assertTrue("'Times New Roman'" in output, - 'Failed to preserve quote characters in a style attribute') - - -class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/comments.svg') - self.assertEqual(doc.childNodes.length, 4, - 'Did not include all comment children outside of root') - self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual(doc.childNodes[1].nodeType, 8, 'Second node not a comment') - self.assertEqual(doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') - - -class DoNotStripDoctype(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/doctype.svg') - self.assertEqual(doc.childNodes.length, 3, - 'Did not include the DOCROOT') - self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual(doc.childNodes[1].nodeType, 10, 'Second node not a doctype') - self.assertEqual(doc.childNodes[2].nodeType, 1, 'Third node not the root node') - - -class PathImplicitLineWithMoveCommands(unittest.TestCase): - - def runTest(self): - path = scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", - "Implicit line segments after move not preserved") - - -class RemoveTitlesOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-titles'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove tag with --remove-titles') - - -class RemoveDescriptionsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-descriptions'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <desc> tag with --remove-descriptions') - - -class RemoveMetadataOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-metadata'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <metadata> tag with --remove-metadata') - - -class RemoveDescriptiveElementsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/full-descriptive-elements.svg', - parse_args(['--remove-descriptive-elements'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <title>, <desc> and <metadata> tags with --remove-descriptive-elements') - - -class EnableCommentStrippingOption(unittest.TestCase): - - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scourString(docStr, - parse_args(['--enable-comment-stripping'])) - self.assertEqual(docStr.find('<!--'), -1, - 'Did not remove document-level comment with --enable-comment-stripping') - - -class StripXmlPrologOption(unittest.TestCase): - - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scourString(docStr, - parse_args(['--strip-xml-prolog'])) - self.assertEqual(docStr.find('<?xml'), -1, - 'Did not remove <?xml?> with --strip-xml-prolog') - - -class ShortenIDsOption(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/shorten-ids.svg', - parse_args(['--shorten-ids'])) - gradientTag = doc.getElementsByTagName('linearGradient')[0] - self.assertEqual(gradientTag.getAttribute('id'), 'a', - "Did not shorten a linear gradient's ID with --shorten-ids") - rectTag = doc.getElementsByTagName('rect')[0] - self.assertEqual(rectTag.getAttribute('fill'), 'url(#a)', - 'Did not update reference to shortened ID') - - -class 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 PathCommandRewrites(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/path-command-rewrites.svg') - paths = doc.getElementsByTagName('path') - expected_paths = [ - ('m100 100 200 100', "Trailing m0 0z not removed"), - ('m100 100v200m0 0 100 100z', "Mangled m0 0 100 100"), - ("m100 100v200m0 0 2-1-2 1z", "Should have removed empty m0 0"), - ("m100 100v200l3-5-5 3m0 0 2-1-2 1z", "Rewrite m0 0 3-5-5 3 ... -> l3-5-5 3 ..."), - ("m100 100v200m0 0 3-5-5 3zm0 0 2-1-2 1z", "No rewrite of m0 0 3-5-5 3z"), - ] - self.assertEqual(len(paths), len(expected_paths), "len(actual_paths) != len(expected_paths)") - for i in range(len(paths)): - actual_path = paths[i].getAttribute('d') - expected_path, message = expected_paths[i] - self.assertEqual(actual_path, - expected_path, - '%s: "%s" != "%s"' % (message, actual_path, expected_path)) - - -class DefaultsRemovalToplevel(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed') - - -class DefaultsRemovalToplevelInverse(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed') - - -class DefaultsRemovalToplevelFormat(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed') - - -class DefaultsRemovalInherited(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed in child') - - -class DefaultsRemovalInheritedInverse(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed in child') - - -class DefaultsRemovalInheritedFormat(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed in child') - - -class DefaultsRemovalOverwrite(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', - 'Default attribute removed, although it overwrites parent element') - - -class DefaultsRemovalOverwriteMarker(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', - 'Default marker attribute removed, although it overwrites parent element') - - -class DefaultsRemovalNonOverwrite(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', - 'Default attribute not removed, although its parent used default') - - -class RemoveDefsWithUnreferencedElements(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/useless-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only unreferenced elements') - - -class RemoveDefsWithWhitespace(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/whitespace-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only whitespace or is <defs/>') - - -class TransformIdentityMatrix(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity matrix not removed') - - -class TransformRotate135(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', - 'Rotation matrix not converted to rotate(135)') - - -class TransformRotate45(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', - 'Rotation matrix not converted to rotate(45)') - - -class TransformRotate90(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Rotation matrix not converted to rotate(90)') - - -class TransformRotateCCW135(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', - 'Counter-clockwise rotation matrix not converted to rotate(225)') - - -class TransformRotateCCW45(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', - 'Counter-clockwise rotation matrix not converted to rotate(-45)') - - -class TransformRotateCCW90(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', - 'Counter-clockwise rotation matrix not converted to rotate(-90)') - - -class TransformScale2by3(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', - 'Scaling matrix not converted to scale(2 3)') - - -class TransformScaleMinus1(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', - 'Scaling matrix not converted to scale(-1)') - - -class TransformTranslate(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-matrix-is-translate.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', - 'Translation matrix not converted to translate(2 3)') - - -class TransformRotationRange719_5(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', - 'Transform containing rotate(719.5) not shortened to rotate(-.5)') - - -class TransformRotationRangeCCW540_0(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', - 'Transform containing rotate(-540.0) not shortened to rotate(180)') - - -class TransformRotation3Args(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-fold-3args.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Optional zeroes in rotate(angle 0 0) not removed') - - -class TransformIdentityRotation(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-rotate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity rotation not removed') - - -class TransformIdentitySkewX(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-skewX-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity X-axis skew not removed') - - -class TransformIdentitySkewY(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-skewY-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity Y-axis skew not removed') - - -class TransformIdentityTranslate(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/transform-translate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity translation not removed') - - -class TransformIdentityScale(unittest.TestCase): - - def runTest(self): - try: - doc = scourXmlFile('unittests/transform-scale-is-identity.svg') - except IndexError: - self.fail("scour failed to handled scale(1) [See GH#190]") - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('scale'), '', - 'Transform containing identity translation not removed') - - -class DuplicateGradientsUpdateStyle(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/duplicate-gradients-update-style.svg', - parse_args(['--disable-style-to-xml'])) - gradient = doc.getElementsByTagName('linearGradient')[0] - rects = doc.getElementsByTagName('rect') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, ' - 'but style="fill:" (with fallback) was not updated to reflect this') - - -class DocWithFlowtext(unittest.TestCase): - - def runTest(self): - with self.assertRaises(Exception): - scourXmlFile('unittests/flowtext.svg', - parse_args(['--error-on-flowtext'])) - - -class DocWithNoFlowtext(unittest.TestCase): - - def runTest(self): - try: - scourXmlFile('unittests/flowtext-less.svg', - parse_args(['--error-on-flowtext'])) - except Exception as e: - self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) - - -class ParseStyleAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/style.svg') - self.assertEqual(doc.documentElement.getAttribute('style'), - 'property1:value1;property2:value2;property3:value3', - "Style attribute not properly parsed and/or serialized") - - -class StripXmlSpaceAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/xml-space.svg', - parse_args(['--strip-xml-space'])) - self.assertEqual(doc.documentElement.getAttribute('xml:space'), '', - "'xml:space' attribute not removed from root SVG element" - "when '--strip-xml-space' was specified") - self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element " - "when '--strip-xml-space' was specified (should only operate on root SVG element)") - - -class DoNotStripXmlSpaceAttribute(unittest.TestCase): - - def runTest(self): - doc = scourXmlFile('unittests/xml-space.svg') - self.assertNotEqual(doc.documentElement.getAttribute('xml:space'), '', - "'xml:space' attribute removed from root SVG element" - "when '--strip-xml-space' was NOT specified") - self.assertNotEqual(doc.getElementById('text1').getAttribute('xml:space'), '', - "'xml:space' attribute removed from a child element " - "when '--strip-xml-space' was NOT specified (should never be removed!)") - - -class CommandLineUsage(unittest.TestCase): - - USAGE_STRING = "Usage: scour [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]" - MINIMAL_SVG = '<?xml version="1.0" encoding="UTF-8"?>\n' \ - '<svg xmlns="http://www.w3.org/2000/svg"/>\n' - TEMP_SVG_FILE = 'testscour_temp.svg' - - # wrapper function for scour.run() to emulate command line usage - # - # returns an object with the following attributes: - # status: the exit status - # stdout: a string representing the combined output to 'stdout' - # stderr: a string representing the combined output to 'stderr' - def _run_scour(self): - class Result(object): - pass - - result = Result() - try: - run() - result.status = 0 - except SystemExit as exception: # catch any calls to sys.exit() - result.status = exception.code - result.stdout = self.temp_stdout.getvalue() - result.stderr = self.temp_stderr.getvalue() - - return result - - def setUp(self): - # store current values of 'argv', 'stdin', 'stdout' and 'stderr' - self.argv = sys.argv - self.stdin = sys.stdin - self.stdout = sys.stdout - self.stderr = sys.stderr - - # start with a fresh 'argv' - sys.argv = ['scour'] # TODO: Do we need a (more) valid 'argv[0]' for anything? - - # create 'stdin', 'stdout' and 'stderr' with behavior close to the original - # TODO: can we create file objects that behave *exactly* like the original? - # this is a mess since we have to ensure compatibility across Python 2 and 3 and it seems impossible - # to replicate all the details of 'stdin', 'stdout' and 'stderr' - class InOutBuffer(six.StringIO, object): - def write(self, string): - try: - return super(InOutBuffer, self).write(string) - except TypeError: - return super(InOutBuffer, self).write(string.decode()) - - sys.stdin = self.temp_stdin = InOutBuffer() - sys.stdout = self.temp_stdout = InOutBuffer() - sys.stderr = self.temp_stderr = InOutBuffer() - - self.temp_stdin.name = '<stdin>' # Scour wants to print the name of the input file... - - def tearDown(self): - # restore previous values of 'argv', 'stdin', 'stdout' and 'stderr' - sys.argv = self.argv - sys.stdin = self.stdin - sys.stdout = self.stdout - sys.stderr = self.stderr - - # clean up - self.temp_stdin.close() - self.temp_stdout.close() - self.temp_stderr.close() - - def test_no_arguments(self): - # we have to pretend that our input stream is a TTY, otherwise Scour waits for input from stdin - self.temp_stdin.isatty = lambda: True - - result = self._run_scour() - - self.assertEqual(result.status, 2, "Execution of 'scour' without any arguments should exit with status '2'") - self.assertTrue(self.USAGE_STRING in result.stderr, - "Usage information not displayed when calling 'scour' without any arguments") - - def test_version(self): - sys.argv.append('--version') - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Execution of 'scour --version' erorred'") - self.assertEqual(__version__ + "\n", result.stdout, "Unexpected output of 'scour --version'") - - def test_help(self): - sys.argv.append('--help') - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Execution of 'scour --help' erorred'") - self.assertTrue(self.USAGE_STRING in result.stdout and 'Options:' in result.stdout, - "Unexpected output of 'scour --help'") - - def test_stdin_stdout(self): - sys.stdin.write(self.MINIMAL_SVG) - sys.stdin.seek(0) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour via 'stdin' / 'stdout' erorred'") - self.assertEqual(result.stdout, self.MINIMAL_SVG, "Unexpected SVG output via 'stdout'") - - def test_filein_fileout_named(self): - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as named parameters errored'") - with open(self.TEMP_SVG_FILE) as file: - file_content = file.read() - self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") - os.remove(self.TEMP_SVG_FILE) - - def test_filein_fileout_positional(self): - sys.argv.extend(['unittests/minimal.svg', self.TEMP_SVG_FILE]) - - result = self._run_scour() - - self.assertEqual(result.status, 0, "Usage of Scour with filenames specified as positional parameters errored'") - with open(self.TEMP_SVG_FILE) as file: - file_content = file.read() - self.assertEqual(file_content, self.MINIMAL_SVG, "Unexpected SVG output in generated file") - os.remove(self.TEMP_SVG_FILE) - - def test_quiet(self): - sys.argv.append('-q') - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - os.remove(self.TEMP_SVG_FILE) - - self.assertEqual(result.status, 0, "Execution of 'scour -q ...' erorred'") - self.assertEqual(result.stdout, '', "Output writtent to 'stdout' when '--quiet' options was used") - self.assertEqual(result.stderr, '', "Output writtent to 'stderr' when '--quiet' options was used") - - def test_verbose(self): - sys.argv.append('-v') - sys.argv.extend(['-i', 'unittests/minimal.svg', '-o', self.TEMP_SVG_FILE]) - - result = self._run_scour() - os.remove(self.TEMP_SVG_FILE) - - self.assertEqual(result.status, 0, "Execution of 'scour -v ...' erorred'") - self.assertEqual(result.stdout.count('Number'), 14, - "Statistics output not as expected when '--verbose' option was used") - self.assertEqual(result.stdout.count(': 0'), 14, - "Statistics output not as expected when '--verbose' option was used") - - -class EmbedRasters(unittest.TestCase): - - # quick way to ping a host using the OS 'ping' command and return the execution result - def _ping(host): - import os - import platform - - # work around https://github.com/travis-ci/travis-ci/issues/3080 as pypy throws if 'ping' can't be executed - import distutils.spawn - if not distutils.spawn.find_executable('ping'): - return -1 - - system = platform.system().lower() - ping_count = '-n' if system == 'windows' else '-c' - dev_null = 'NUL' if system == 'windows' else '/dev/null' - - return os.system('ping ' + ping_count + ' 1 ' + host + ' > ' + dev_null) - - def test_disable_embed_rasters(self): - doc = scourXmlFile('unittests/raster-formats.svg', - parse_args(['--disable-embed-rasters'])) - self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), 'raster.png', - "Raster image embedded when '--disable-embed-rasters' was specified") - - def test_raster_formats(self): - doc = scourXmlFile('unittests/raster-formats.svg') - self.assertEqual(doc.getElementById('png').getAttribute('xlink:href'), - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAABAgMAAABmjvwnAAAAC' - 'VBMVEUAAP//AAAA/wBmtfVOAAAACklEQVQI12NIAAAAYgBhGxZhsAAAAABJRU5ErkJggg==', - "Raster image (PNG) not correctly embedded.") - self.assertEqual(doc.getElementById('gif').getAttribute('xlink:href'), - 'data:image/gif;base64,R0lGODdhAwABAKEDAAAA//8AAAD/AP///ywAAAAAAwABAAACAoxQADs=', - "Raster image (GIF) not correctly embedded.") - self.assertEqual(doc.getElementById('jpg').getAttribute('xlink:href'), - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/' - '2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' - '2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/' - 'wAARCAABAAMDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABoQAAEFAQAAAAAAAAAAAAAAAAgABQc3d7j/' - 'xAAVAQEBAAAAAAAAAAAAAAAAAAAHCv/EABwRAAEDBQAAAAAAAAAAAAAAAAgAB7gJODl2eP/aAAwDAQACEQMRAD8AMeaF' - '/u2aj5z1Fqp7oN4rxx2kn5cPuhV6LkzG7qOyYL2r/9k=', - "Raster image (JPG) not correctly embedded.") - - def test_raster_paths_local(self): - doc = scourXmlFile('unittests/raster-paths-local.svg') - images = doc.getElementsByTagName('image') - for image in images: - href = image.getAttribute('xlink:href') - self.assertTrue(href.startswith('data:image/'), - "Raster image from local path '" + href + "' not embedded.") - - def test_raster_paths_local_absolute(self): - with open('unittests/raster-formats.svg', 'r') as f: - svg = f.read() - - # create a reference string by scouring the original file with relative links - options = ScourOptions - options.infilename = 'unittests/raster-formats.svg' - reference_svg = scourString(svg, options) - - # this will not always create formally valid paths but it'll check how robust our implementation is - # (the third path is invalid for sure because file: needs three slashes according to URI spec) - svg = svg.replace('raster.png', - '/' + os.path.abspath(os.path.dirname(__file__)) + '\\unittests\\raster.png') - svg = svg.replace('raster.gif', - 'file:///' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.gif') - svg = svg.replace('raster.jpg', - 'file:/' + os.path.abspath(os.path.dirname(__file__)) + '/unittests/raster.jpg') - - svg = scourString(svg) - - self.assertEqual(svg, reference_svg, - "Raster images from absolute local paths not properly embedded.") - - @unittest.skipIf(_ping('raw.githubusercontent.com') != 0, "Remote server not reachable.") - def test_raster_paths_remote(self): - doc = scourXmlFile('unittests/raster-paths-remote.svg') - images = doc.getElementsByTagName('image') - for image in images: - href = image.getAttribute('xlink:href') - self.assertTrue(href.startswith('data:image/'), - "Raster image from remote path '" + href + "' not embedded.") - - -class ViewBox(unittest.TestCase): - - def test_viewbox_create(self): - doc = scourXmlFile('unittests/viewbox-create.svg', parse_args(['--enable-viewboxing'])) - viewBox = doc.documentElement.getAttribute('viewBox') - self.assertEqual(viewBox, '0 0 123.46 654.32', "viewBox not properly created with '--enable-viewboxing'.") - - def test_viewbox_remove_width_and_height(self): - doc = scourXmlFile('unittests/viewbox-remove.svg', parse_args(['--enable-viewboxing'])) - width = doc.documentElement.getAttribute('width') - height = doc.documentElement.getAttribute('height') - self.assertEqual(width, '', "width not removed with '--enable-viewboxing'.") - self.assertEqual(height, '', "height not removed with '--enable-viewboxing'.") - - -# TODO: write tests for --keep-editor-data - -if __name__ == '__main__': - testcss = __import__('testcss') - scour = __import__('__main__') - suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) - unittest.main(defaultTest="suite") diff -Nru scour-0.37/tox.ini scour-0.38.2/tox.ini --- scour-0.37/tox.ini 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/tox.ini 2020-11-22 14:05:13.000000000 +0000 @@ -5,6 +5,10 @@ py34 py35 py36 + py37 + py38 + py39 + py310 flake8 @@ -16,7 +20,7 @@ commands = scour --version - coverage run --parallel-mode --source=scour testscour.py + coverage run --parallel-mode --source=scour test_scour.py [testenv:flake8] @@ -24,4 +28,4 @@ flake8 commands = - flake8 --max-line-length=119 \ No newline at end of file + flake8 --max-line-length=119 diff -Nru scour-0.37/.travis.yml scour-0.38.2/.travis.yml --- scour-0.37/.travis.yml 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/.travis.yml 2020-11-22 14:05:13.000000000 +0000 @@ -8,6 +8,10 @@ - 3.4 - 3.5 - 3.6 + - 3.7 + - 3.8 + - 3.9-dev + - 3.10-dev install: - pip install tox-travis codecov @@ -18,9 +22,9 @@ fast_finish: true include: - - python: 3.6 + - python: 3.8 env: - TOXENV=flake8 after_success: - - coverage combine && codecov \ No newline at end of file + - coverage combine && codecov diff -Nru scour-0.37/unittests/collapse-gradients-preserve-xlink-href.svg scour-0.38.2/unittests/collapse-gradients-preserve-xlink-href.svg --- scour-0.37/unittests/collapse-gradients-preserve-xlink-href.svg 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/unittests/collapse-gradients-preserve-xlink-href.svg 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<defs> + <linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="blue" /> + <stop offset="1" stop-color="yellow" /> + </linearGradient> + <radialGradient id="g2" xlink:href="#g1" cx="100" cy="100" r="70"/> + <radialGradient id="g3" xlink:href="#g2" cx="100" cy="100" r="70"/> +</defs> +<rect fill="url(#g1)" width="200" height="200"/> +<rect fill="url(#g3)" width="200" height="200" y="200"/> +</svg> diff -Nru scour-0.37/unittests/group-sibling-merge-crash.svg scour-0.38.2/unittests/group-sibling-merge-crash.svg --- scour-0.37/unittests/group-sibling-merge-crash.svg 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/unittests/group-sibling-merge-crash.svg 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" viewBox="0 0 141.732 141.732" xml:space="preserve"> + <g> + <g clip-path="url(#SVGID_2_)"> + <path d="M1,1" fill="#fdebc8" stroke="#000" stroke-width=".5" stroke-miterlimit="10"/> + </g> + <g clip-path="url(#SVGID_2_)"> + <g> + <path opacity=".5" clip-path="url(#SVGID_4_)" fill="#fff" d="M1,1"/> + </g> + </g> + </g> +</svg> diff -Nru scour-0.37/unittests/group-sibling-merge.svg scour-0.38.2/unittests/group-sibling-merge.svg --- scour-0.37/unittests/group-sibling-merge.svg 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/unittests/group-sibling-merge.svg 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <desc>Produced by GNUPLOT 5.2 patchlevel 8</desc> + <rect width="900" height="600" fill="none"/> + <g color="black" fill="none"> + <path d="m88.5 564h9m777.5 0h-9" stroke="#000"/> + <g transform="translate(80.2,567.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> + <text><tspan font-family="Arial">0</tspan></text> + </g> + </g> + <g color="black" fill="none"> + <path d="m88.5 473h9m777.5 0h-9" stroke="#000"/> + <g transform="translate(80.2,476.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> + <text><tspan font-family="Arial">5000</tspan></text> + </g> + </g> + <g color="black" fill="none"> + <path d="m88.5 382h9m777.5 0h-9" stroke="#000"/> + <g transform="translate(80.2,385.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> + <text><tspan font-family="Arial">10000</tspan></text> + </g> + </g> + <g color="black" fill="none"> + <path d="m88.5 291h9m777.5 0h-9" stroke="#000"/> + <g transform="translate(80.2,294.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end"> + <text><tspan font-family="Arial">15000</tspan></text> + </g> + </g> +</svg> diff -Nru scour-0.37/unittests/redundant-svg-namespace.svg scour-0.38.2/unittests/redundant-svg-namespace.svg --- scour-0.37/unittests/redundant-svg-namespace.svg 2018-07-04 17:16:55.000000000 +0000 +++ scour-0.38.2/unittests/redundant-svg-namespace.svg 2020-11-22 14:05:13.000000000 +0000 @@ -5,4 +5,5 @@ <title>Test + Hallo World diff -Nru scour-0.37/unittests/remove-duplicate-gradients-master-without-id.svg scour-0.38.2/unittests/remove-duplicate-gradients-master-without-id.svg --- scour-0.37/unittests/remove-duplicate-gradients-master-without-id.svg 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/unittests/remove-duplicate-gradients-master-without-id.svg 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff -Nru scour-0.37/unittests/shorten-ids-stable-output.svg scour-0.38.2/unittests/shorten-ids-stable-output.svg --- scour-0.37/unittests/shorten-ids-stable-output.svg 1970-01-01 00:00:00.000000000 +0000 +++ scour-0.38.2/unittests/shorten-ids-stable-output.svg 2020-11-22 14:05:13.000000000 +0000 @@ -0,0 +1,11 @@ + + + + + + + + + + +