summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 6f25800)
raw | patch | inline | side by side (parent: 6f25800)
author | jeff_schiller <jeff_schiller@users.sourceforge.net> | |
Thu, 10 Sep 2009 12:30:13 +0000 (12:30 +0000) | ||
committer | jeff_schiller <jeff_schiller@users.sourceforge.net> | |
Thu, 10 Sep 2009 12:30:13 +0000 (12:30 +0000) |
share/extensions/scour.inx | patch | blob | history | |
share/extensions/scour.py | patch | blob | history | |
share/extensions/yocto_css.py | [new file with mode: 0644] | patch | blob |
index 4a435f6391f320895848855270b4a89247bb555d..57884b6aaed48929dc85dbf80db76a211dd1ea16 100644 (file)
<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>
index e82df0bb608d783cdec364ba8de35ee369246371..d032bd6966dca37575d1c5d1af0ede2f2a8fd465 100755 (executable)
# 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
from svg_regex import svg_parser
import gzip
import optparse
+from yocto_css import parseCssString
# Python 2.3- did not have Decimal
try:
Decimal = FixedPoint
APP = 'scour'
-VER = '0.19'
+VER = '0.20'
COPYRIGHT = 'Copyright Jeff Schiller, 2009'
NS = { 'SVG': 'http://www.w3.org/2000/svg',
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
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:]
# 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)]))
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:
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
# 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
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):
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
--- /dev/null
@@ -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