diff options
author | Sergey Sharybin <sergey.vfx@gmail.com> | 2019-09-19 13:50:09 +0300 |
---|---|---|
committer | Sergey Sharybin <sergey.vfx@gmail.com> | 2019-09-19 16:33:28 +0300 |
commit | 7f97ceb05061fd45e49ae53e71ba9b7656cc8aab (patch) | |
tree | 1b6cb19ba565c51976fe143e2b3688a18d8fc9da | |
parent | 9d54d44eb9a326e1f57be396a2825f0712a35fda (diff) |
SVG: Refactor, move utilities to module
Also cover with unit test.
-rw-r--r-- | io_curve_svg/import_svg.py | 94 | ||||
-rw-r--r-- | io_curve_svg/svg_util.py | 99 | ||||
-rwxr-xr-x | io_curve_svg/svg_util_test.py | 72 |
3 files changed, 177 insertions, 88 deletions
diff --git a/io_curve_svg/import_svg.py b/io_curve_svg/import_svg.py index c4949013..0742bc4e 100644 --- a/io_curve_svg/import_svg.py +++ b/io_curve_svg/import_svg.py @@ -26,93 +26,17 @@ import bpy from mathutils import Vector, Matrix from . import svg_colors -from .svg_util import (srgb_to_linearrgb, +from .svg_util import (units, + srgb_to_linearrgb, check_points_equal, - parse_array_of_floats) + parse_array_of_floats, + read_float) #### Common utilities #### -# TODO: "em" and "ex" aren't actually supported -SVGUnits = {"": 1.0, - "px": 1.0, - "in": 90.0, - "mm": 90.0 / 25.4, - "cm": 90.0 / 2.54, - "pt": 1.25, - "pc": 15.0, - "em": 1.0, - "ex": 1.0, - "INVALID": 1.0, # some DocBook files contain this - } - SVGEmptyStyles = {'useFill': None, 'fill': None} -def SVGParseFloat(s, i=0): - """ - Parse first float value from string - - Returns value as string - """ - - start = i - n = len(s) - token = '' - - # Skip leading whitespace characters - while i < n and (s[i].isspace() or s[i] == ','): - i += 1 - - if i == n: - return None, i - - # Read sign - if s[i] == '-': - token += '-' - i += 1 - elif s[i] == '+': - i += 1 - - # Read integer part - if s[i].isdigit(): - while i < n and s[i].isdigit(): - token += s[i] - i += 1 - - # Fractional part - if i < n and s[i] == '.': - token += '.' - i += 1 - - if s[i].isdigit(): - while i < n and s[i].isdigit(): - token += s[i] - i += 1 - elif s[i].isspace() or s[i] == ',': - # Inkscape sometimes uses weird float format with missed - # fractional part after dot. Suppose zero fractional part - # for this case - pass - else: - raise Exception('Invalid float value near ' + s[start:start + 10]) - - # Degree - if i < n and (s[i] == 'e' or s[i] == 'E'): - token += s[i] - i += 1 - if s[i] == '+' or s[i] == '-': - token += s[i] - i += 1 - - if s[i].isdigit(): - while i < n and s[i].isdigit(): - token += s[i] - i += 1 - else: - raise Exception('Invalid float value near ' + s[start:start + 10]) - - return token, i - def SVGCreateCurve(context): """ @@ -153,14 +77,14 @@ def SVGParseCoord(coord, size): Needed to handle coordinates set in cm, mm, inches. """ - token, last_char = SVGParseFloat(coord) + token, last_char = read_float(coord) val = float(token) unit = coord[last_char:].strip() # strip() in case there is a space if unit == '%': return float(size) / 100.0 * val else: - return val * SVGUnits[unit] + return val * units[unit] return val @@ -493,7 +417,7 @@ class SVGPathData: elif c.lower() in commands: tokens.append(c) elif c in ['-', '.'] or c.isdigit(): - token, last_char = SVGParseFloat(d, i) + token, last_char = read_float(d, i) tokens.append(token) # in most cases len(token) and (last_char - i) are the same @@ -1824,7 +1748,7 @@ class SVGGeometrySVG(SVGGeometryContainer): if self._node.getAttribute('height'): raw_height = self._node.getAttribute('height') - token, last_char = SVGParseFloat(raw_height) + token, last_char = read_float(raw_height) document_height = float(token) unit = raw_height[last_char:].strip() @@ -1837,7 +1761,7 @@ class SVGGeometrySVG(SVGGeometryContainer): unitscale = document_height / (viewbox[3] - viewbox[1]) #convert units to BU: - unitscale = unitscale * SVGUnits[unit] / 90 * 1000 / 39.3701 + unitscale = unitscale * units[unit] / 90 * 1000 / 39.3701 #apply blender unit scale: unitscale = unitscale / bpy.context.scene.unit_settings.scale_length diff --git a/io_curve_svg/svg_util.py b/io_curve_svg/svg_util.py index 0aeb2018..42e900b4 100644 --- a/io_curve_svg/svg_util.py +++ b/io_curve_svg/svg_util.py @@ -20,6 +20,20 @@ import re + +units = {"": 1.0, + "px": 1.0, + "in": 90.0, + "mm": 90.0 / 25.4, + "cm": 90.0 / 2.54, + "pt": 1.25, + "pc": 15.0, + "em": 1.0, + "ex": 1.0, + "INVALID": 1.0, # some DocBook files contain this + } + + def srgb_to_linearrgb(c): if c < 0.04045: return 0.0 if c < 0.0 else c * (1.0 / 12.92) @@ -48,6 +62,90 @@ def parse_array_of_floats(text): return [value_to_float(v[0]) for v in elements] +def read_float(s: str, i: int = 0): + """ + Reads floating point value from a string. Parsing starts at the given index. + + Returns the value itself (as a string) and index of first character after the value. + """ + start = i + n = len(s) + token = '' + + # Skip leading whitespace characters + while i < n and (s[i].isspace() or s[i] == ','): + i += 1 + + if i == n: + return "0", i + + # Read sign + if s[i] == '-': + token += '-' + i += 1 + elif s[i] == '+': + i += 1 + + # Read integer part + if s[i].isdigit(): + while i < n and s[i].isdigit(): + token += s[i] + i += 1 + + # Fractional part + if i < n and s[i] == '.': + token += '.' + i += 1 + + if i < n and s[i].isdigit(): + while i < n and s[i].isdigit(): + token += s[i] + i += 1 + elif i == n or s[i].isspace() or s[i] == ',': + # Inkscape sometimes uses weird float format with missed + # fractional part after dot. Suppose zero fractional part + # for this case + pass + else: + raise Exception('Invalid float value near ' + s[start:start + 10]) + + # Degree + if i < n and (s[i] == 'e' or s[i] == 'E'): + token += s[i] + i += 1 + if s[i] == '+' or s[i] == '-': + token += s[i] + i += 1 + + if s[i].isdigit(): + while i < n and s[i].isdigit(): + token += s[i] + i += 1 + else: + raise Exception('Invalid float value near ' + s[start:start + 10]) + + return token, i + + +def parse_coord(coord, size): + """ + Parse coordinate component to common basis + + Needed to handle coordinates set in cm, mm, inches. + """ + + token, last_char = read_float(coord) + val = float(token) + unit = coord[last_char:].strip() # strip() in case there is a space + + if unit == '%': + return float(size) / 100.0 * val + else: + return val * units[unit] + + return val + + def value_to_float(value_encoded: str): """ A simple wrapper around float() which supports empty strings (which are converted to 0). @@ -55,4 +153,3 @@ def value_to_float(value_encoded: str): if len(value_encoded) == 0: return 0 return float(value_encoded) - diff --git a/io_curve_svg/svg_util_test.py b/io_curve_svg/svg_util_test.py index 68f3a8b0..6f54d5f3 100755 --- a/io_curve_svg/svg_util_test.py +++ b/io_curve_svg/svg_util_test.py @@ -24,9 +24,9 @@ # XXX Not really nice, but that hack is needed to allow execution of that test # from both automated CTest and by directly running the file manually... if __name__ == '__main__': - from svg_util import parse_array_of_floats + from svg_util import (parse_array_of_floats, read_float, parse_coord,) else: - from .svg_util import parse_array_of_floats + from .svg_util import (parse_array_of_floats, read_float, parse_coord,) import unittest @@ -79,5 +79,73 @@ class ParseArrayOfFloatsTest(unittest.TestCase): self.assertEqual(parse_array_of_floats("2.75,8.5"), [2.75, 8.5]) +class ReadFloatTest(unittest.TestCase): + def test_empty(self): + value, endptr = read_float("", 0) + self.assertEqual(value, "0") + self.assertEqual(endptr, 0) + + def test_empty_spaces(self): + value, endptr = read_float(" ", 0) + self.assertEqual(value, "0") + self.assertEqual(endptr, 4) + + def test_single_value(self): + value, endptr = read_float("1.2", 0) + self.assertEqual(value, "1.2") + self.assertEqual(endptr, 3) + + def test_scientific_value(self): + value, endptr = read_float("1.2e+3", 0) + self.assertEqual(value, "1.2e+3") + self.assertEqual(endptr, 6) + + def test_scientific_value_no_sign(self): + value, endptr = read_float("1.2e3", 0) + self.assertEqual(value, "1.2e3") + self.assertEqual(endptr, 5) + + def test_middle(self): + value, endptr = read_float("1.2 3.4 5.6", 3) + self.assertEqual(value, "3.4") + self.assertEqual(endptr, 8) + + def test_comma(self): + value, endptr = read_float("1.2 ,,3.4 5.6", 3) + self.assertEqual(value, "3.4") + self.assertEqual(endptr, 10) + + def test_not_a_number(self): + # TODO(sergey): Make this more concrete. + with self.assertRaises(Exception): + value, endptr = read_float("1.2eV", 3) + + def test_missing_fractional(self): + value, endptr = read_float("1.", 0) + self.assertEqual(value, "1.") + self.assertEqual(endptr, 2) + + value, endptr = read_float("2. 3", 0) + self.assertEqual(value, "2.") + self.assertEqual(endptr, 2) + + +class ParseCoordTest(unittest.TestCase): + def test_empty(self): + self.assertEqual(parse_coord("", 200), 0) + + def test_empty_spaces(self): + self.assertEqual(parse_coord(" ", 200), 0) + + def test_no_units(self): + self.assertEqual(parse_coord("1.2", 200), 1.2) + + def test_unit_cm(self): + self.assertAlmostEqual(parse_coord("1.2cm", 200), 42.51968503937008) + + def test_unit_percentage(self): + self.assertEqual(parse_coord("1.2%", 200), 2.4) + + if __name__ == '__main__': unittest.main(verbosity=2) |