Implement a basic rewrite of redundant commands

This basic implementation can drop and rewrite some cases of "m0 0"
and "z" without triggering the issues experienced in #163.  It works
by analysing the path backwards and tracking "z" and "m" commands.

Signed-off-by: Niels Thykier <niels@thykier.net>
This commit is contained in:
Niels Thykier 2018-03-11 08:22:27 +00:00
parent a2c94c96fb
commit 38274f75bc
4 changed files with 68 additions and 13 deletions

View file

@ -2181,10 +2181,11 @@ def cleanPath(element, options):
x, y = startx, starty
path[pathIndex] = ('z', data)
# remove empty segments
# remove empty segments and redundant commands
# Reuse the data structure 'path' and the coordinate lists, even if we're
# deleting items, because these deletions are relatively cheap.
if not has_round_or_square_linecaps:
# remove empty path segments
for pathIndex in range(len(path)):
cmd, data = path[pathIndex]
i = 0
@ -2196,8 +2197,8 @@ def cleanPath(element, options):
# different from "l...z".
#
# To do such a rewrite, we need to understand the
# full subpath, so for now just leave the first
# two coordinates of "m" alone.
# full subpath. This logic happens after this
# loop.
i = 2
while i < len(data):
if data[i] == data[i + 1] == 0:
@ -2231,6 +2232,45 @@ def cleanPath(element, options):
path[pathIndex] = (cmd, [coord for coord in data if coord != 0])
_num_path_segments_removed += len(path[pathIndex][1]) - oldLen
# remove no-op commands
pathIndex = len(path)
subpath_needs_anchor = False
# NB: We can never rewrite the first m/M command (expect if it
# is the only command)
while pathIndex > 1:
pathIndex -= 1
cmd, data = path[pathIndex]
if cmd == 'z':
next_cmd, next_data = path[pathIndex - 1]
if next_cmd == 'm' and len(next_data) == 2:
# mX Yz -> mX Y
# note the len check on next_data as it is not
# safe to rewrite "m0 0 1 1z" in general (it is a
# question of where the "pen" ends - you can
# continue a draw on the same subpath after a
# "z").
del path[pathIndex]
_num_path_segments_removed += 1
else:
# it is not safe to rewrite "m0 0 ..." to "l..."
# because of this "z" command.
subpath_needs_anchor = True
elif cmd == 'm':
if len(path) - 1 == pathIndex and len(data) == 2:
# Ends with an empty move (but no line/draw
# following it)
del path[pathIndex]
_num_path_segments_removed += 1
continue
if subpath_needs_anchor:
subpath_needs_anchor = False
elif data[0] == data[1] == 0:
# unanchored, i.e. we can replace "m0 0 ..." with
# "l..." as there is no "z" after it.
path[pathIndex] = ('l', data[2:])
_num_path_segments_removed += 1
# fixup: Delete subcommands having no coordinates.
path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z']

View file

@ -2054,13 +2054,25 @@ class StyleToAttr(unittest.TestCase):
self.assertEqual(line.getAttribute('marker-end'), 'url(#m)')
class PathEmptyMove(unittest.TestCase):
class PathCommandRewrites(unittest.TestCase):
def runTest(self):
doc = scourXmlFile('unittests/path-empty-move.svg')
# This path can actually be optimized to avoid the "m0 0z".
self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100 200 100m0 0z')
self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200m0 0 100 100z')
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):

View file

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="m100 100 l200 100 m0 0z" />
<path d="m100 100 v200 m0 0 100 100z" />
<path d="m100 100 v200 m0 0m0 0 2-1-2 1z" />
<path d="m100 100 v200 m0 0 3-5-5 3m0 0 2-1-2 1z" />
<path d="m100 100 v200 m0 0 3-5-5 3zm0 0 2-1-2 1z" />
</svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -1,5 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="m100 100 l200 100 m0 0z" />
<path d="m100 100 v200 m0 0 100 100z" />
</svg>

Before

Width:  |  Height:  |  Size: 152 B