diff --git a/scour/scour.py b/scour/scour.py
index 4a2c1b3..afee25d 100644
--- a/scour/scour.py
+++ b/scour/scour.py
@@ -64,12 +64,7 @@ import optparse
from scour.yocto_css import parseCssString
import six
from six.moves import range
-
-# Python 2.3- did not have Decimal
-try:
- from decimal import Decimal, InvalidOperation, getcontext
-except ImportError:
- sys.stderr.write("Scour requires at least Python 2.7 or Python 3.3+.")
+from decimal import Context, Decimal, InvalidOperation, getcontext
# select the most precise walltime measurement function available on the platform
if sys.platform.startswith('win'):
@@ -2291,8 +2286,12 @@ def cleanPath(element, options) :
path = newPath
newPathStr = serializePath(path, options)
- numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) )
- element.setAttribute('d', newPathStr)
+
+ # if for whatever reason we actually made the path longer don't use it
+ # TODO: maybe we could compare path lengths after each optimization step and use the shortest
+ if len(newPathStr) <= len(oldPathStr):
+ numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) )
+ element.setAttribute('d', newPathStr)
@@ -2474,13 +2473,19 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a
This is faster than scourLength on elements guaranteed not to
contain units.
"""
- # reduce to the proper number of digits
if not isinstance(length, Decimal):
length = getcontext().create_decimal(str(length))
- # if the value is an integer, it may still have .0[...] attached to it for some reason
- # remove those
- if int(length) == length:
- length = getcontext().create_decimal(int(length))
+
+ # reduce numeric precision
+ # plus() corresponds to the unary prefix plus operator and applies context precision and rounding
+ length = scouringContext.plus(length)
+
+ # remove trailing zeroes as we do not care for significance
+ intLength = length.to_integral_value()
+ if length == intLength:
+ length = Decimal(intLength)
+ else:
+ length = length.normalize()
# gather the non-scientific notation version of the coordinate.
# this may actually be in scientific notation if the value is
@@ -2492,14 +2497,22 @@ def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a
elif len(nonsci) > 3 and nonsci[:3] == '-0.':
nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot
- if len(nonsci) > 3: # avoid calling normalize unless strictly necessary
- # and then the scientific notation version, with E+NUMBER replaced with
- # just eNUMBER, since SVG accepts this.
- sci = six.text_type(length.normalize()).lower().replace("e+", "e")
+ # Gather the scientific notation version of the coordinate which
+ # can only be shorter if the length of the number is at least 4 characters (e.g. 1000 = 1e3).
+ if len(nonsci) > 3:
+ # We have to implement this ourselves since both 'normalize()' and 'to_sci_string()'
+ # don't handle negative exponents in a reasonable way (e.g. 0.000001 remains unchanged)
+ exponent = length.adjusted() # how far do we have to shift the dot?
+ length = length.scaleb(-exponent).normalize() # shift the dot and remove potential trailing zeroes
- if len(sci) < len(nonsci): return sci
- else: return nonsci
- else: return nonsci
+ sci = six.text_type(length) + 'e' + six.text_type(exponent)
+
+ if len(sci) < len(nonsci):
+ return sci
+ else:
+ return nonsci
+ else:
+ return nonsci
@@ -3071,7 +3084,11 @@ def scourString(in_string, options=None):
# sanitize options (take missing attributes from defaults, discard unknown attributes)
options = sanitizeOptions(options)
- getcontext().prec = options.digits
+ # create decimal context with reduced precision for scouring numbers
+ # calculations should be done in the default context (precision defaults to 28 significant digits) to minimize errors
+ global scouringContext
+ scouringContext = Context(prec = options.digits)
+
global numAttrsRemoved
global numStylePropsFixed
global numElemsRemoved
diff --git a/testscour.py b/testscour.py
index ba0ceba..f409225 100755
--- a/testscour.py
+++ b/testscour.py
@@ -685,6 +685,14 @@ class ChangeQuadToShorthandInPath(unittest.TestCase):
self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0',
'Did not change quadratic curves into shorthand curve segments in path')
+class DoNotOptimzePathIfLarger(unittest.TestCase):
+ def runTest(self):
+ p = scour.scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0];
+ self.assertTrue(len(p.getAttribute('d')) <= len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"),
+ 'Made path data longer during optimization')
+ # 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"
+
class HandleEncodingUTF8(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/encoding-utf8.svg')
@@ -877,7 +885,7 @@ class RereferenceForRadialGradient(unittest.TestCase):
class CollapseSamePathPoints(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0];
- self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.88 4.88-15.12-5.12 0 0z",
+ self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z",
'Did not collapse same path points')
class ScourUnitlessLengths(unittest.TestCase):
diff --git a/unittests/collapse-same-path-points.svg b/unittests/collapse-same-path-points.svg
index bda0fff..b05f4d1 100644
--- a/unittests/collapse-same-path-points.svg
+++ b/unittests/collapse-same-path-points.svg
@@ -1,4 +1,4 @@
diff --git a/unittests/path-no-optimize.svg b/unittests/path-no-optimize.svg
new file mode 100644
index 0000000..bda0fff
--- /dev/null
+++ b/unittests/path-no-optimize.svg
@@ -0,0 +1,4 @@
+
+