diff options
| author | Jeff Schiller <codedread@gmail.com> | 2009-09-10 12:30:13 +0000 |
|---|---|---|
| committer | jeff_schiller <jeff_schiller@users.sourceforge.net> | 2009-09-10 12:30:13 +0000 |
| commit | aa73b5ab139ffd8c060c6a4bc8e69ea97789661b (patch) | |
| tree | 8042979c5588602142054c4d6043af269c85fa90 | |
| parent | giving svg_file property to Effect class. Make more readable code. (diff) | |
| download | inkscape-aa73b5ab139ffd8c060c6a4bc8e69ea97789661b.tar.gz inkscape-aa73b5ab139ffd8c060c6a4bc8e69ea97789661b.zip | |
Update to scour 0.20 extension
(bzr r8574)
| -rw-r--r-- | share/extensions/scour.inx | 1 | ||||
| -rwxr-xr-x | share/extensions/scour.py | 150 | ||||
| -rw-r--r-- | share/extensions/yocto_css.py | 72 |
3 files changed, 179 insertions, 44 deletions
diff --git a/share/extensions/scour.inx b/share/extensions/scour.inx index 4a435f639..57884b6aa 100644 --- a/share/extensions/scour.inx +++ b/share/extensions/scour.inx @@ -4,6 +4,7 @@ <id>org.inkscape.output.scour</id> <dependency type="executable" location="extensions">scour.py</dependency> <dependency type="executable" location="extensions">svg_regex.py</dependency> + <dependency type="executable" location="extensions">yocto_css.py</dependency> <output> <extension>.svg</extension> <mimetype>image/svg+xml</mimetype> diff --git a/share/extensions/scour.py b/share/extensions/scour.py index e82df0bb6..d032bd696 100755 --- a/share/extensions/scour.py +++ b/share/extensions/scour.py @@ -32,26 +32,16 @@ # Even more ideas here: http://esw.w3.org/topic/SvgTidy # * analysis of path elements to see if rect can be used instead? (must also need to look # at rounded corners) -# * removal of unused attributes in groups: -# <g fill="blue" ...> -# <rect fill="red" ... /> -# <rect fill="red" ... /> -# <rect fill="red" ... /> -# </g> -# in this case, fill="blue" should be removed # Next Up: -# + analyze all children of a group, if they have common inheritable attributes, then move them to the group -# + scour lengths for *opacity, svg:x,y,width,height, stroke-miterlimit, stroke-width -# - analyze a group and its children, if a group's attribute is not being used by any children -# (or descendants?) then remove it +# + remove unused attributes in parent elements +# + prevent elements from being stripped if they are referenced in a <style> element +# (for instance, filter, marker, pattern) - need a crude CSS parser # - add an option to remove ids if they match the Inkscape-style of IDs # - investigate point-reducing algorithms # - parse transform attribute # - if a <g> has only one element in it, collapse the <g> (ensure transform, etc are carried down) # - option to remove metadata -# - prevent elements from being stripped if they are referenced in a <style> element -# (for instance, filter, marker, pattern) - need a crude CSS parser # necessary to get true division from __future__ import division @@ -66,6 +56,7 @@ import urllib from svg_regex import svg_parser import gzip import optparse +from yocto_css import parseCssString # Python 2.3- did not have Decimal try: @@ -75,7 +66,7 @@ except ImportError: Decimal = FixedPoint APP = 'scour' -VER = '0.19' +VER = '0.20' COPYRIGHT = 'Copyright Jeff Schiller, 2009' NS = { 'SVG': 'http://www.w3.org/2000/svg', @@ -411,6 +402,9 @@ def findElementsWithId(node, elems=None): findElementsWithId(child, elems) return elems +referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', + 'marker-end', 'marker-mid'] + def findReferencedElements(node, ids=None): """ Returns the number of times an ID is referenced as well as all elements @@ -419,13 +413,25 @@ def findReferencedElements(node, ids=None): Currently looks at fill, stroke, clip-path, mask, marker, and xlink:href attributes. """ + global referencingProps if ids is None: ids = {} # TODO: input argument ids is clunky here (see below how it is called) # GZ: alternative to passing dict, use **kwargs - href = node.getAttributeNS(NS['XLINK'],'href') + + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # node.firstChild will be either a CDATA or a Text node + cssRules = parseCssString(node.firstChild.nodeValue) + for rule in cssRules: + for propname in rule['properties']: + propval = rule['properties'][propname] + findReferencingProperty(node, propname, propval, ids) + + return ids - # if xlink:href is set, then grab the id + # else if xlink:href is set, then grab the id + href = node.getAttributeNS(NS['XLINK'],'href') if href != '' and len(href) > 1 and href[0] == '#': # we remove the hash mark from the beginning of the id id = href[1:] @@ -437,8 +443,6 @@ def findReferencedElements(node, ids=None): # now get all style properties and the fill, stroke, filter attributes styles = node.getAttribute('style').split(';') - referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', - 'marker-end', 'marker-mid'] for attr in referencingProps: styles.append(':'.join([attr, node.getAttribute(attr)])) @@ -447,29 +451,7 @@ def findReferencedElements(node, ids=None): if len(propval) == 2 : prop = propval[0].strip() val = propval[1].strip() - if prop in referencingProps and val != '' : - if len(val) >= 7 and val[0:5] == 'url(#' : - id = val[5:val.find(')')] - if ids.has_key(id) : - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] - # if the url has a quote in it, we need to compensate - elif len(val) >= 8 : - id = None - # double-quote - if val[0:6] == 'url("#' : - id = val[6:val.find('")')] - # single-quote - elif val[0:6] == "url('#" : - id = val[6:val.find("')")] - if id != None: - if ids.has_key(id) : - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] + findReferencingProperty(node, prop, val, ids) if node.hasChildNodes() : for child in node.childNodes: @@ -477,6 +459,32 @@ def findReferencedElements(node, ids=None): findReferencedElements(child, ids) return ids +def findReferencingProperty(node, prop, val, ids): + global referencingProps + if prop in referencingProps and val != '' : + if len(val) >= 7 and val[0:5] == 'url(#' : + id = val[5:val.find(')')] + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + # if the url has a quote in it, we need to compensate + elif len(val) >= 8 : + id = None + # double-quote + if val[0:6] == 'url("#' : + id = val[6:val.find('")')] + # single-quote + elif val[0:6] == "url('#" : + id = val[6:val.find("')")] + if id != None: + if ids.has_key(id) : + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1,[node]] + numIDsRemoved = 0 numElemsRemoved = 0 numAttrsRemoved = 0 @@ -701,6 +709,59 @@ def moveCommonAttributesToParentGroup(elem): # update our statistic (we remove N*M attributes and add back in M attributes) num += (len(childElements)-1) * len(commonAttrs) return num + +def removeUnusedAttributesOnParent(elem): + """ + This recursively calls this function on all children of the element passed in, + then removes any unused attributes on this elem if none of the children inherit it + """ + num = 0 + + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + childElements.append(child) + num += removeUnusedAttributesOnParent(child) + + # only process the children if there are more than one element + if len(childElements) <= 1: return num + + # get all attribute values on this parent + attrList = elem.attributes + unusedAttrs = {} + for num in range(attrList.length): + attr = attrList.item(num) + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + unusedAttrs[attr.nodeName] = attr.nodeValue + + # for each child, if at least one child inherits the parent's attribute, then remove + for childNum in range(len(childElements)): + child = childElements[childNum] + inheritedAttrs = [] + for name in unusedAttrs.keys(): + val = child.getAttribute(name) + if val == '' or val == None or val == 'inherit': + inheritedAttrs.append(name) + for a in inheritedAttrs: + del unusedAttrs[a] + + # unusedAttrs now has all the parent attributes that are unused + for name in unusedAttrs.keys(): + elem.removeAttribute(name) + num += 1 + + return num def removeDuplicateGradientStops(doc): global numElemsRemoved @@ -1707,11 +1768,10 @@ def parseListOfPoints(s): Returns a list of containing an even number of coordinate strings """ - # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? # coordinate-pair = coordinate comma-or-wsp coordinate # coordinate = sign? integer - nums = re.split("\\s*\\,?\\s*", s) + nums = re.split("\\s*\\,?\\s*", s.strip()) i = 0 points = [] while i < len(nums): @@ -2137,8 +2197,10 @@ def scourString(in_string, options=None): pass # move common attributes to parent group - # TODO: should make sure this is called with most-nested groups first numAttrsRemoved += moveCommonAttributesToParentGroup(doc.documentElement) + + # remove unused attributes from parent + numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) while removeDuplicateGradientStops(doc) > 0: pass diff --git a/share/extensions/yocto_css.py b/share/extensions/yocto_css.py new file mode 100644 index 000000000..a73e5f2a5 --- /dev/null +++ b/share/extensions/yocto_css.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# yocto-css, an extremely bare minimum CSS parser +# +# Copyright 2009 Jeff Schiller +# +# This file is part of Scour, http://www.codedread.com/scour/ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# In order to resolve Bug 368716 (https://bugs.launchpad.net/scour/+bug/368716) +# scour needed a bare-minimum CSS parser in order to determine if some elements +# were still referenced by CSS properties. + +# I looked at css-py (a CSS parser built in Python), but that library +# is about 35k of Python and requires ply to be installed. I just need +# something very basic to suit scour's needs. + +# yocto-css takes a string of CSS and tries to spit out a list of rules +# A rule is an associative array (dictionary) with the following keys: +# - selector: contains the string of the selector (see CSS grammar) +# - properties: contains an associative array of CSS properties for this rule + +# TODO: need to build up some unit tests for yocto_css + +# stylesheet : [ CDO | CDC | S | statement ]*; +# statement : ruleset | at-rule; +# at-rule : ATKEYWORD S* any* [ block | ';' S* ]; +# block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; +# ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; +# selector : any+; +# declaration : property S* ':' S* value; +# property : IDENT; +# value : [ any | block | ATKEYWORD S* ]+; +# any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING +# | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES +# | DASHMATCH | FUNCTION S* any* ')' +# | '(' S* any* ')' | '[' S* any* ']' ] S*; + +def parseCssString(str): + rules = [] + # first, split on } to get the rule chunks + chunks = str.split('}') + for chunk in chunks: + # second, split on { to get the selector and the list of properties + bits = chunk.split('{') + if len(bits) != 2: continue + rule = {} + rule['selector'] = bits[0].strip() + # third, split on ; to get the property declarations + bites = bits[1].strip().split(';') + if len(bites) < 1: continue + props = {} + for bite in bites: + # fourth, split on : to get the property name and value + nibbles = bite.strip().split(':') + if len(nibbles) != 2: continue + props[nibbles[0].strip()] = nibbles[1].strip() + rule['properties'] = props + rules.append(rule) + return rules |
