Collapse consecutive h,v segments/coords where possible
This commit is contained in:
parent
49b35bf6d1
commit
56cc8fd15a
4 changed files with 106 additions and 21 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
<li>remove font/text styles from shape elements (font-weight, font-size, line-height, etc)</li>
|
<li>remove font/text styles from shape elements (font-weight, font-size, line-height, etc)</li>
|
||||||
<li>remove -inkscape-font-specification styles</li>
|
<li>remove -inkscape-font-specification styles</li>
|
||||||
<li>added --set-precision argument to set the number of significant digits (defaults to 6)</li>
|
<li>added --set-precision argument to set the number of significant digits (defaults to 6)</li>
|
||||||
|
<li>collapse consecutive h,v coords/segments that go in the same direction</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
102
scour.py
102
scour.py
|
|
@ -46,6 +46,7 @@
|
||||||
# + remove font/text styles from non-text elements
|
# + remove font/text styles from non-text elements
|
||||||
# + remove -inkscape-font-specification styles
|
# + remove -inkscape-font-specification styles
|
||||||
# + added --set-precision argument to set the number of significant digits (defaults to 6)
|
# + added --set-precision argument to set the number of significant digits (defaults to 6)
|
||||||
|
# + collapse unnecessary consecutive horizontal/vertical line segments
|
||||||
# - prevent elements from being stripped if they are referenced in a <style> element
|
# - prevent elements from being stripped if they are referenced in a <style> element
|
||||||
# (for instance, filter, marker, pattern) - need a crude CSS parser
|
# (for instance, filter, marker, pattern) - need a crude CSS parser
|
||||||
# - Remove any unused glyphs from font elements?
|
# - Remove any unused glyphs from font elements?
|
||||||
|
|
@ -272,6 +273,8 @@ colors = {
|
||||||
'yellowgreen': 'rgb(154, 205, 50)',
|
'yellowgreen': 'rgb(154, 205, 50)',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0)
|
||||||
|
|
||||||
coord = re.compile("\\-?\\d+\\.?\\d*")
|
coord = re.compile("\\-?\\d+\\.?\\d*")
|
||||||
scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+")
|
scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+")
|
||||||
number = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+")
|
number = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+")
|
||||||
|
|
@ -711,6 +714,7 @@ def repairStyle(node, options):
|
||||||
# opacity='1.0' is useless, remove it
|
# opacity='1.0' is useless, remove it
|
||||||
if opacity == 1.0 :
|
if opacity == 1.0 :
|
||||||
del styleMap['opacity']
|
del styleMap['opacity']
|
||||||
|
num += 1
|
||||||
|
|
||||||
# if opacity='0' then all fill and stroke properties are useless, remove them
|
# if opacity='0' then all fill and stroke properties are useless, remove them
|
||||||
elif opacity == 0.0 :
|
elif opacity == 0.0 :
|
||||||
|
|
@ -722,7 +726,7 @@ def repairStyle(node, options):
|
||||||
num += 1
|
num += 1
|
||||||
|
|
||||||
# if stroke:none, then remove all stroke-related properties (stroke-width, etc)
|
# if stroke:none, then remove all stroke-related properties (stroke-width, etc)
|
||||||
# TODO: should also detect if the computed value of this element is fill="none"
|
# TODO: should also detect if the computed value of this element is stroke="none"
|
||||||
if styleMap.has_key('stroke') and styleMap['stroke'] == 'none' :
|
if styleMap.has_key('stroke') and styleMap['stroke'] == 'none' :
|
||||||
for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit',
|
for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit',
|
||||||
'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] :
|
'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity'] :
|
||||||
|
|
@ -730,10 +734,10 @@ def repairStyle(node, options):
|
||||||
del styleMap[strokestyle]
|
del styleMap[strokestyle]
|
||||||
num += 1
|
num += 1
|
||||||
# TODO: This is actually a problem if a parent element has a specified stroke
|
# TODO: This is actually a problem if a parent element has a specified stroke
|
||||||
|
# we need to properly calculate computed values
|
||||||
del styleMap['stroke']
|
del styleMap['stroke']
|
||||||
|
|
||||||
# if fill:none, then remove all fill-related properties (fill-rule, etc)
|
# if fill:none, then remove all fill-related properties (fill-rule, etc)
|
||||||
# TODO: should also detect if fill-opacity=0
|
|
||||||
if styleMap.has_key('fill') and styleMap['fill'] == 'none' :
|
if styleMap.has_key('fill') and styleMap['fill'] == 'none' :
|
||||||
for fillstyle in [ 'fill-rule', 'fill-opacity' ] :
|
for fillstyle in [ 'fill-rule', 'fill-opacity' ] :
|
||||||
if styleMap.has_key(fillstyle) :
|
if styleMap.has_key(fillstyle) :
|
||||||
|
|
@ -749,7 +753,7 @@ def repairStyle(node, options):
|
||||||
# fill-opacity: 1 or 0
|
# fill-opacity: 1 or 0
|
||||||
if styleMap.has_key('fill-opacity') :
|
if styleMap.has_key('fill-opacity') :
|
||||||
fillOpacity = float(styleMap['fill-opacity'])
|
fillOpacity = float(styleMap['fill-opacity'])
|
||||||
# TODO: This is actually a problem is the parent element does not have fill-opacity = 1
|
# TODO: This is actually a problem if the parent element does not have fill-opacity=1
|
||||||
if fillOpacity == 1.0 :
|
if fillOpacity == 1.0 :
|
||||||
del styleMap['fill-opacity']
|
del styleMap['fill-opacity']
|
||||||
num += 1
|
num += 1
|
||||||
|
|
@ -762,7 +766,7 @@ def repairStyle(node, options):
|
||||||
# stroke-opacity: 1 or 0
|
# stroke-opacity: 1 or 0
|
||||||
if styleMap.has_key('stroke-opacity') :
|
if styleMap.has_key('stroke-opacity') :
|
||||||
strokeOpacity = float(styleMap['stroke-opacity'])
|
strokeOpacity = float(styleMap['stroke-opacity'])
|
||||||
# TODO: This is actually a problem is the parent element does not have stroke-opacity = 1
|
# TODO: This is actually a problem if the parent element does not have stroke-opacity=1
|
||||||
if strokeOpacity == 1.0 :
|
if strokeOpacity == 1.0 :
|
||||||
del styleMap['stroke-opacity']
|
del styleMap['stroke-opacity']
|
||||||
num += 1
|
num += 1
|
||||||
|
|
@ -784,6 +788,7 @@ def repairStyle(node, options):
|
||||||
num += 1
|
num += 1
|
||||||
|
|
||||||
# remove font properties for non-text elements
|
# remove font properties for non-text elements
|
||||||
|
# I've actually observed this in real SVG content
|
||||||
if node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']:
|
if node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']:
|
||||||
for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust',
|
for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust',
|
||||||
'font-style', 'font-variant', 'font-weight',
|
'font-style', 'font-variant', 'font-weight',
|
||||||
|
|
@ -795,13 +800,12 @@ def repairStyle(node, options):
|
||||||
num += 1
|
num += 1
|
||||||
|
|
||||||
# remove inkscape-specific styles
|
# remove inkscape-specific styles
|
||||||
|
# TODO: need to get a full list of these
|
||||||
for inkscapeStyle in ['-inkscape-font-specification']:
|
for inkscapeStyle in ['-inkscape-font-specification']:
|
||||||
if styleMap.has_key(inkscapeStyle):
|
if styleMap.has_key(inkscapeStyle):
|
||||||
del styleMap[inkscapeStyle]
|
del styleMap[inkscapeStyle]
|
||||||
num += 1
|
num += 1
|
||||||
|
|
||||||
# TODO: what else?
|
|
||||||
|
|
||||||
# visibility: visible
|
# visibility: visible
|
||||||
if styleMap.has_key('visibility') :
|
if styleMap.has_key('visibility') :
|
||||||
if styleMap['visibility'] == 'visible':
|
if styleMap['visibility'] == 'visible':
|
||||||
|
|
@ -834,7 +838,7 @@ def repairStyle(node, options):
|
||||||
node.setAttribute(propName, styleMap[propName])
|
node.setAttribute(propName, styleMap[propName])
|
||||||
del styleMap[propName]
|
del styleMap[propName]
|
||||||
|
|
||||||
# sew our style back together
|
# sew our remaining style properties back together into a style attribute
|
||||||
fixedStyle = ''
|
fixedStyle = ''
|
||||||
for prop in styleMap.keys() :
|
for prop in styleMap.keys() :
|
||||||
fixedStyle += prop + ':' + styleMap[prop] + ';'
|
fixedStyle += prop + ':' + styleMap[prop] + ';'
|
||||||
|
|
@ -853,7 +857,9 @@ def repairStyle(node, options):
|
||||||
rgb = re.compile("\\s*rgb\\(\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\)\\s*")
|
rgb = re.compile("\\s*rgb\\(\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\)\\s*")
|
||||||
rgbp = re.compile("\\s*rgb\\(\\s*(\\d*\\.?\\d+)\\%\\s*\\,\\s*(\\d*\\.?\\d+)\\%\\s*\\,\\s*(\\d*\\.?\\d+)\\%\\s*\\)\\s*")
|
rgbp = re.compile("\\s*rgb\\(\\s*(\\d*\\.?\\d+)\\%\\s*\\,\\s*(\\d*\\.?\\d+)\\%\\s*\\,\\s*(\\d*\\.?\\d+)\\%\\s*\\)\\s*")
|
||||||
def convertColor(value):
|
def convertColor(value):
|
||||||
|
"""
|
||||||
|
Converts the input color string and returns a #RRGGBB (or #RGB if possible) string
|
||||||
|
"""
|
||||||
s = value
|
s = value
|
||||||
|
|
||||||
if s in colors.keys():
|
if s in colors.keys():
|
||||||
|
|
@ -883,6 +889,9 @@ def convertColor(value):
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def convertColors(element) :
|
def convertColors(element) :
|
||||||
|
"""
|
||||||
|
Recursively converts all color properties into #RRGGBB format
|
||||||
|
"""
|
||||||
numBytes = 0
|
numBytes = 0
|
||||||
|
|
||||||
if element.nodeType != 1: return 0
|
if element.nodeType != 1: return 0
|
||||||
|
|
@ -912,6 +921,9 @@ def convertColors(element) :
|
||||||
return numBytes
|
return numBytes
|
||||||
|
|
||||||
def cleanPath(element) :
|
def cleanPath(element) :
|
||||||
|
"""
|
||||||
|
Cleans the path string (d attribute) of the element
|
||||||
|
"""
|
||||||
global numBytesSavedInPathData
|
global numBytesSavedInPathData
|
||||||
global numPathSegmentsReduced
|
global numPathSegmentsReduced
|
||||||
|
|
||||||
|
|
@ -994,13 +1006,17 @@ def cleanPath(element) :
|
||||||
# take the last pair of coordinates for the starting point
|
# take the last pair of coordinates for the starting point
|
||||||
x = path[0][1][N-2]
|
x = path[0][1][N-2]
|
||||||
y = path[0][1][N-1]
|
y = path[0][1][N-1]
|
||||||
else: # m, accumulate coordinates for the starting point
|
else: # relative move, accumulate coordinates for the starting point
|
||||||
(x,y) = path[0][1][0],path[0][1][1]
|
(x,y) = path[0][1][0],path[0][1][1]
|
||||||
n = 2
|
n = 2
|
||||||
while n < N:
|
while n < N:
|
||||||
x += path[0][1][n]
|
x += path[0][1][n]
|
||||||
y += path[0][1][n+1]
|
y += path[0][1][n+1]
|
||||||
n += 2
|
n += 2
|
||||||
|
|
||||||
|
# now we have the starting point at x,y so let's save it
|
||||||
|
(startx,starty) = (x,y)
|
||||||
|
|
||||||
i = 1
|
i = 1
|
||||||
for (cmd,data) in path[1:]:
|
for (cmd,data) in path[1:]:
|
||||||
# adjust abs to rel
|
# adjust abs to rel
|
||||||
|
|
@ -1095,7 +1111,6 @@ def cleanPath(element) :
|
||||||
newPath.append( (cmd,newData) )
|
newPath.append( (cmd,newData) )
|
||||||
else:
|
else:
|
||||||
newPath.append( (cmd,data) )
|
newPath.append( (cmd,data) )
|
||||||
|
|
||||||
path = newPath
|
path = newPath
|
||||||
|
|
||||||
# convert line segments into h,v where possible
|
# convert line segments into h,v where possible
|
||||||
|
|
@ -1108,7 +1123,7 @@ def cleanPath(element) :
|
||||||
if data[i] == 0:
|
if data[i] == 0:
|
||||||
# vertical
|
# vertical
|
||||||
if lineTuples:
|
if lineTuples:
|
||||||
# append the line command
|
# flush the existing line command
|
||||||
newPath.append( ('l', lineTuples) )
|
newPath.append( ('l', lineTuples) )
|
||||||
lineTuples = []
|
lineTuples = []
|
||||||
# append the v and then the remaining line coords
|
# append the v and then the remaining line coords
|
||||||
|
|
@ -1116,7 +1131,7 @@ def cleanPath(element) :
|
||||||
numPathSegmentsReduced += 1
|
numPathSegmentsReduced += 1
|
||||||
elif data[i+1] == 0:
|
elif data[i+1] == 0:
|
||||||
if lineTuples:
|
if lineTuples:
|
||||||
# change the line command, then append the h and then the remaining line coords
|
# flush the line command, then append the h and then the remaining line coords
|
||||||
newPath.append( ('l', lineTuples) )
|
newPath.append( ('l', lineTuples) )
|
||||||
lineTuples = []
|
lineTuples = []
|
||||||
newPath.append( ('h', [data[i]]) )
|
newPath.append( ('h', [data[i]]) )
|
||||||
|
|
@ -1131,19 +1146,66 @@ def cleanPath(element) :
|
||||||
newPath.append( (cmd, data) )
|
newPath.append( (cmd, data) )
|
||||||
path = newPath
|
path = newPath
|
||||||
|
|
||||||
# TODO: collapse adjacent H or V segments that have coords in the same direction
|
# collapse all consecutive h or v commands together into one command
|
||||||
|
prevCmd = ''
|
||||||
|
prevData = []
|
||||||
|
newPath = [path[0]]
|
||||||
|
for (cmd,data) in path[1:]:
|
||||||
|
# flush the previous command if it is not the same type as the current command
|
||||||
|
# or it is not an h or v line
|
||||||
|
if prevCmd != '':
|
||||||
|
if cmd != prevCmd or not prevCmd in ['h','v']:
|
||||||
|
newPath.append( (prevCmd, prevData) )
|
||||||
|
prevCmd = ''
|
||||||
|
prevData = []
|
||||||
|
|
||||||
|
# if the previous and current commands are the same type and a h/v line, collapse
|
||||||
|
if cmd == prevCmd and cmd in ['h','v']:
|
||||||
|
for coord in data:
|
||||||
|
prevData.append(coord)
|
||||||
|
numPathSegmentsReduced += 1
|
||||||
|
# save last command and data
|
||||||
|
else:
|
||||||
|
prevCmd = cmd
|
||||||
|
prevData = data
|
||||||
|
# flush last command and data
|
||||||
|
if prevCmd != '':
|
||||||
|
newPath.append( (prevCmd, prevData) )
|
||||||
|
path = newPath
|
||||||
|
|
||||||
|
# for each h or v, collapse unnecessary coordinates that run in the same direction
|
||||||
|
# i.e. "h-100-100" becomes "h-200" but "h300-100" does not change
|
||||||
|
newPath = [path[0]]
|
||||||
|
for (cmd,data) in path[1:]:
|
||||||
|
if cmd in ['h','v'] and len(data) > 1:
|
||||||
|
newData = []
|
||||||
|
prevCoord = data[0]
|
||||||
|
for coord in data[1:]:
|
||||||
|
if isSameSign(prevCoord, coord):
|
||||||
|
prevCoord += coord
|
||||||
|
numPathSegmentsReduced += 1
|
||||||
|
else:
|
||||||
|
newData.append(prevCoord)
|
||||||
|
prevCoord = coord
|
||||||
|
newData.append(prevCoord)
|
||||||
|
newPath.append( (cmd, newData) )
|
||||||
|
else:
|
||||||
|
newPath.append( (cmd, data) )
|
||||||
|
path = newPath
|
||||||
|
|
||||||
newPathStr = serializePath(path)
|
newPathStr = serializePath(path)
|
||||||
numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) )
|
numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) )
|
||||||
element.setAttribute('d', newPathStr)
|
element.setAttribute('d', newPathStr)
|
||||||
|
|
||||||
|
|
||||||
# - reserialize the path data with some cleanups:
|
|
||||||
# - removes scientific notation (exponents)
|
|
||||||
# - removes all trailing zeros after the decimal
|
|
||||||
# - removes extraneous whitespace
|
|
||||||
# - adds commas between all values in a subcommand
|
|
||||||
def serializePath(pathObj):
|
def serializePath(pathObj):
|
||||||
|
"""
|
||||||
|
Reserializes the path data with some cleanups:
|
||||||
|
- removes scientific notation (exponents)
|
||||||
|
- removes all trailing zeros after the decimal
|
||||||
|
- removes extraneous whitespace
|
||||||
|
- adds commas between values in a subcommand if required
|
||||||
|
"""
|
||||||
pathStr = ""
|
pathStr = ""
|
||||||
for (cmd,data) in pathObj:
|
for (cmd,data) in pathObj:
|
||||||
pathStr += cmd
|
pathStr += cmd
|
||||||
|
|
@ -1154,12 +1216,10 @@ def serializePath(pathObj):
|
||||||
if int(coord) == coord: pathStr += str(int(coord))
|
if int(coord) == coord: pathStr += str(int(coord))
|
||||||
else: pathStr += str(coord)
|
else: pathStr += str(coord)
|
||||||
|
|
||||||
# only need the comma if the next number if non-negative
|
# only need the comma if the next number is non-negative
|
||||||
if c < len(data)-1 and data[c+1] >= 0:
|
if c < len(data)-1 and data[c+1] >= 0:
|
||||||
pathStr += ','
|
pathStr += ','
|
||||||
c += 1
|
c += 1
|
||||||
# we do not even bother with spaces to separate commands
|
|
||||||
# pathStr += ' '
|
|
||||||
return pathStr
|
return pathStr
|
||||||
|
|
||||||
def embedRasters(element) :
|
def embedRasters(element) :
|
||||||
|
|
|
||||||
19
testscour.py
19
testscour.py
|
|
@ -548,5 +548,24 @@ class RemoveFontStylesFromNonTextShapes(unittest.TestCase):
|
||||||
r = scour.scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
|
r = scour.scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
|
||||||
self.assertEquals( r.getAttribute('font-size'), '',
|
self.assertEquals( r.getAttribute('font-size'), '',
|
||||||
'font-size not removed from rect' )
|
'font-size not removed from rect' )
|
||||||
|
|
||||||
|
class CollapseConsecutiveHLinesSegments(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
||||||
|
self.assertEquals( p.getAttribute('d'), 'M100,100h200v100h-200z',
|
||||||
|
'Did not collapse consecutive hlines segments')
|
||||||
|
|
||||||
|
class CollapseConsecutiveHLinesCoords(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1]
|
||||||
|
self.assertEquals( p.getAttribute('d'), 'M100,300h200v100h-200z',
|
||||||
|
'Did not collapse consecutive hlines coordinates')
|
||||||
|
|
||||||
|
class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2]
|
||||||
|
self.assertEquals( p.getAttribute('d'), 'M100,500h300-100v100h-200z',
|
||||||
|
'Collapsed consecutive hlines segments with differing signs')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
5
unittests/consecutive-hlines.svg
Normal file
5
unittests/consecutive-hlines.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#F00" stroke="#0F0" d="M100,100h100h100v100h-200z"/>
|
||||||
|
<path fill="#F00" stroke="#0F0" d="M100,300h100,100v100h-200z"/>
|
||||||
|
<path fill="#F00" stroke="#0F0" d="M100,500h300h-100v100h-200z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 243 B |
Loading…
Add table
Add a link
Reference in a new issue