SVG: Refactor, move utilities to module
Also cover with unit test.
This commit is contained in:
parent
9d54d44eb9
commit
7f97ceb050
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue