diff --git a/Patro/Common/Xml/XmlFile.py b/Patro/Common/Xml/XmlFile.py
index beb6d71d07430ad5d0c5f6cec07757d15d6b8473..5a9ea66e90e4fb14d15a571acc63a1038bec4816 100644
--- a/Patro/Common/Xml/XmlFile.py
+++ b/Patro/Common/Xml/XmlFile.py
@@ -39,8 +39,13 @@ class XmlFileMixin:
##############################################
- def __init__(self, path):
- self._path = Path(path)
+ def __init__(self, path, data=None):
+
+ if path is not None:
+ self._path = Path(path)
+ else:
+ self._path = None
+ self._data = data
##############################################
@@ -52,8 +57,17 @@ class XmlFileMixin:
def parse(self):
"""Parse a XML file and return the etree"""
- with open(str(self._path), 'rb') as f:
- source = f.read()
+
+ data = self._data
+ if data is None:
+ with open(str(self._path), 'rb') as f:
+ source = f.read()
+ else:
+ if isinstance(data, bytes):
+ source = data
+ else:
+ source = bytes(str(self._data).strip(), 'utf-8')
+
return etree.fromstring(source)
##############################################
diff --git a/Patro/FileFormat/Svg/SvgFile.py b/Patro/FileFormat/Svg/SvgFile.py
index 676f9d29acbeb4fb458da91468df97ac192781a6..5d630cd2444cdbccd650c74dedf55669dab1f377 100644
--- a/Patro/FileFormat/Svg/SvgFile.py
+++ b/Patro/FileFormat/Svg/SvgFile.py
@@ -36,6 +36,8 @@ import logging
from lxml import etree
+from IntervalArithmetic import Interval2D
+
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from . import SvgFormat
@@ -308,9 +310,9 @@ class SvgFileInternal(XmlFileMixin, SvgFileMixin):
##############################################
- def __init__(self, path=None):
+ def __init__(self, path, data=None):
- super().__init__(path)
+ super().__init__(path, data)
# Fixme: API
# purpose of dispatcher, where must be state ???
@@ -330,10 +332,36 @@ class SvgFileInternal(XmlFileMixin, SvgFileMixin):
# >
tree = self.parse()
+
+ svg_root = self._dispatcher.from_xml(tree)
+ self.on_svg_root(svg_root)
+
self._dispatcher.on_root(tree)
##############################################
+ @property
+ def view_box(self):
+ return self._view_box
+
+ @property
+ def width(self):
+ return self._width
+
+ @property
+ def height(self):
+ return self._height
+
+ ##############################################
+
+ def on_svg_root(self, svg_root):
+ x_inf, y_inf, x_sup, y_sup = svg_root.view_box
+ self._view_box = Interval2D((x_inf, x_sup), (y_inf, y_sup))
+ self._width = svg_root.width
+ self._height = svg_root.height
+
+ ##############################################
+
def on_group(self, group):
self._logger.info('Group: {}\n{}'.format(group.id, group))
diff --git a/Patro/FileFormat/Svg/SvgFormat.py b/Patro/FileFormat/Svg/SvgFormat.py
index 610d45ffcd7985e82deb604e47bafd2153809a18..cdefc1a5a5db419939e2b9cc6f07c0fa22a62444 100644
--- a/Patro/FileFormat/Svg/SvgFormat.py
+++ b/Patro/FileFormat/Svg/SvgFormat.py
@@ -486,7 +486,7 @@ class TransformAttribute(StringAttribute):
elif name == 'scale':
transform = AffineTransformation2D.Scale(*values)
elif name == 'rotate':
- angle, *vector = complete(values, 2)
+ angle, *vector = complete(values, 3)
vector = Vector2D(vector)
transform = AffineTransformation2D.RotationAt(vector, angle)
elif name == 'skewX':
@@ -958,7 +958,7 @@ class PathDataAttribute(StringAttribute):
@classmethod
def from_xml(cls, svg_path):
- # cls._logger.debug('SVG path:\n'+ svg_path)
+ # cls._logger.info('SVG path:\n'+ svg_path)
# Replace comma separator by space
cleaned_svg_path = svg_path.replace(',', ' ')
@@ -990,13 +990,17 @@ class PathDataAttribute(StringAttribute):
i += 1 # move to first arg
# else repeated instruction
next_i = i + number_of_args
- values = parts[i:next_i]
- #! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
- points = values
- commands.append((command, points))
+ args = parts[i:next_i]
+ commands.append((command, args))
i = next_i
+ # for implicit line to
+ if command == 'm':
+ command = 'l'
+ elif command == 'M':
+ command = 'L'
# return commands
+ # Fixme: do later ???
return cls.to_geometry(commands)
##############################################
@@ -1027,7 +1031,7 @@ class PathDataAttribute(StringAttribute):
@classmethod
def to_geometry(cls, commands):
- cls._logger.info('Path:\n' + str(commands).replace('), ', '),\n '))
+ # cls._logger.info('Path:\n' + str(commands).replace('), ', '),\n '))
path = None
for command, args in commands:
command_lower = command.lower()
@@ -1038,15 +1042,18 @@ class PathDataAttribute(StringAttribute):
if path is None:
if command_lower != 'm':
raise NameError('Path must start with m')
- # Fixme: m ???
path = Path2D(args) # Vector2D()
else:
if command_lower == 'l':
path.line_to(args, absolute=absolute)
- elif command_lower == 'h':
- path.horizontal_to(*args, absolute=absolute)
+ elif command == 'h':
+ path.horizontal_to(*args, absolute=False)
+ elif command == 'H':
+ path.absolute_horizontal_to(*args)
elif command_lower == 'v':
path.vertical_to(*args, absolute=absolute)
+ elif command == 'V':
+ path.absolute_vertical_to(*args)
elif command_lower == 'c':
path.cubic_to(*cls.as_vector(args), absolute=absolute)
elif command_lower == 's':
diff --git a/Patro/GeometryEngine/Path.py b/Patro/GeometryEngine/Path.py
index d0863f2ed14a9e53d9dc554d0ef7d87ea3b9c044..6bde532155153118f6b69cc89661e1e28e462265 100644
--- a/Patro/GeometryEngine/Path.py
+++ b/Patro/GeometryEngine/Path.py
@@ -452,7 +452,124 @@ class PathSegment(OnePointMixin, LinearSegment):
####################################################################################################
-class DirectionalSegment(LinearSegment):
+class DirectionalSegmentMixin(LinearSegment):
+
+ ##############################################
+
+ def apply_transformation(self, transformation):
+ # Since a rotation will change the direction
+ # DirectionalSegment must be casted to PathSegment
+ raise NotImplementedError
+
+ ##############################################
+
+ @property
+ def geometry(self):
+ # Fixme: cache ???
+ return Segment2D(self.start_point, self.stop_point)
+
+####################################################################################################
+
+class AbsoluteHVSegment(DirectionalSegmentMixin):
+
+ ##############################################
+
+ def to_path_segment(self):
+
+ # Fixme: duplicted
+ if self._index == 0 and self._radius is not None:
+ radius = None
+ close = True
+ else:
+ radius = self._radius
+ close = False
+
+ path = PathSegment(self._path, self._index, self.stop_point, radius, absolute=True)
+
+ if close:
+ path.close(self._radius)
+
+ return path
+
+####################################################################################################
+
+class AbsoluteHorizontalSegment(AbsoluteHVSegment):
+
+ ##############################################
+
+ def __init__(self, path, index, x, radius=None):
+
+ super().__init__(path, index, radius)
+ self.x = x
+ self._init_absolute(False) # Fixme: mix
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0}(@{1._index}, {1._x})'.format(self.__class__.__name__, self)
+
+ ##############################################
+
+ def clone(self, path):
+ return self.__class__(path, self._index, self._x, self._radius)
+
+ ##############################################
+
+ @property
+ def x(self):
+ return self._x
+
+ @x.setter
+ def x(self, value):
+ self._x = float(value)
+
+ ##############################################
+
+ @property
+ def stop_point(self):
+ return Vector2D(self._x, self.start_point.y)
+
+####################################################################################################
+
+class AbsoluteVerticalSegment(AbsoluteHVSegment):
+
+ ##############################################
+
+ def __init__(self, path, index, y, radius=None):
+
+ super().__init__(path, index, radius)
+ self.y = y
+ self._init_absolute(False) # Fixme: mix
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0}(@{1._index}, {1._y})'.format(self.__class__.__name__, self)
+
+ ##############################################
+
+ def clone(self, path):
+ return self.__class__(path, self._index, self._y, self._radius)
+
+ ##############################################
+
+ @property
+ def y(self):
+ return self._y
+
+ @y.setter
+ def y(self, value):
+ self._y = float(value)
+
+ ##############################################
+
+ @property
+ def stop_point(self):
+ return Vector2D(self.start_point.x, self._y)
+
+####################################################################################################
+
+class DirectionalSegment(DirectionalSegmentMixin):
__angle__ = None
@@ -469,13 +586,6 @@ class DirectionalSegment(LinearSegment):
##############################################
- def apply_transformation(self, transformation):
- # Since a rotation will change the direction
- # DirectionalSegment must be casted to PathSegment
- raise NotImplementedError
-
- ##############################################
-
def clone(self, path):
return self.__class__(path, self._index, self._length, self._radius)
@@ -503,15 +613,21 @@ class DirectionalSegment(LinearSegment):
##############################################
- @property
- def geometry(self):
- # Fixme: cache ???
- return Segment2D(self.start_point, self.stop_point)
+ def to_path_segment(self):
- ##############################################
+ if self._index == 0 and self._radius is not None:
+ radius = None
+ close = True
+ else:
+ radius = self._radius
+ close = False
- def to_path_segment(self):
- return PathSegment(self._path, self._index, self.offset, self._radius, absolute=False)
+ path = PathSegment(self._path, self._index, self.offset, radius, absolute=False)
+
+ if close:
+ path.close(self._radius)
+
+ return path
####################################################################################################
@@ -829,7 +945,7 @@ class Path2D(Primitive2DMixin, Primitive1P):
# print(part)
if isinstance(part, PathSegment):
part._reset_cache()
- if isinstance(part, DirectionalSegment):
+ if isinstance(part, DirectionalSegmentMixin):
# Since a rotation will change the direction
# DirectionalSegment must be casted to PathSegment
part = part.to_path_segment()
@@ -869,47 +985,55 @@ class Path2D(Primitive2DMixin, Primitive1P):
##############################################
- def horizontal_to(self, distance, radius=None, absolute=False):
+ def horizontal_to(self, length, radius=None, absolute=False):
if absolute:
- return self._add_part(PathSegment, self.__vector_cls__(distance, 0), radius,
+ return self._add_part(PathSegment, self.__vector_cls__(length, 0), radius,
absolute=True)
else:
- return self._add_part(HorizontalSegment, distance, radius)
+ return self._add_part(HorizontalSegment, length, radius)
##############################################
- def vertical_to(self, distance, radius=None, absolute=False):
+ def vertical_to(self, length, radius=None, absolute=False):
if absolute:
- return self._add_part(PathSegment, self.__vector_cls__(0, distance), radius,
+ return self._add_part(PathSegment, self.__vector_cls__(0, length), radius,
absolute=True)
else:
- return self._add_part(VerticalSegment, distance, radius)
+ return self._add_part(VerticalSegment, length, radius)
+
+ ##############################################
+
+ def absolute_horizontal_to(self, x, radius=None):
+ return self._add_part(AbsoluteHorizontalSegment, x, radius)
+
+ def absolute_vertical_to(self, y, radius=None):
+ return self._add_part(AbsoluteVerticalSegment, y, radius)
##############################################
- def north_to(self, distance, radius=None):
- return self._add_part(NorthSegment, distance, radius)
+ def north_to(self, length, radius=None):
+ return self._add_part(NorthSegment, length, radius)
- def south_to(self, distance, radius=None):
- return self._add_part(SouthSegment, distance, radius)
+ def south_to(self, length, radius=None):
+ return self._add_part(SouthSegment, length, radius)
- def west_to(self, distance, radius=None):
- return self._add_part(WestSegment, distance, radius)
+ def west_to(self, length, radius=None):
+ return self._add_part(WestSegment, length, radius)
- def east_to(self, distance, radius=None):
- return self._add_part(EastSegment, distance, radius)
+ def east_to(self, length, radius=None):
+ return self._add_part(EastSegment, length, radius)
- def north_east_to(self, distance, radius=None):
- return self._add_part(NorthEastSegment, distance, radius)
+ def north_east_to(self, length, radius=None):
+ return self._add_part(NorthEastSegment, length, radius)
- def south_east_to(self, distance, radius=None):
- return self._add_part(SouthEastSegment, distance, radius)
+ def south_east_to(self, length, radius=None):
+ return self._add_part(SouthEastSegment, length, radius)
- def north_west_to(self, distance, radius=None):
- return self._add_part(NorthWestSegment, distance, radius)
+ def north_west_to(self, length, radius=None):
+ return self._add_part(NorthWestSegment, length, radius)
- def south_west_to(self, distance, radius=None):
- return self._add_part(SouthWestSegment, distance, radius)
+ def south_west_to(self, length, radius=None):
+ return self._add_part(SouthWestSegment, length, radius)
##############################################
diff --git a/examples/file-format/svg/test-svg-import.py b/examples/file-format/svg/test-svg-import.py
index 52df5ebbc6440f9a21aeda208b047f009245a063..56f8baa27e270f1fc72ee300009a618104866eac 100644
--- a/examples/file-format/svg/test-svg-import.py
+++ b/examples/file-format/svg/test-svg-import.py
@@ -32,6 +32,7 @@ if not use_qt:
from Patro.FileFormat.Svg import SvgFormat
from Patro.FileFormat.Svg.SvgFile import SvgFile, SvgFileInternal
+from Patro.GeometryEngine.Transformation import AffineTransformation2D
from Patro.GraphicEngine.GraphicScene.GraphicStyle import GraphicPathStyle, GraphicBezierStyle
from Patro.GraphicEngine.Painter.QtPainter import QtScene
from Patro.GraphicStyle import Colors, StrokeStyle, CapStyle
@@ -82,6 +83,12 @@ class SceneImporter(SvgFileInternal):
##############################################
+ def on_svg_root(self, svg_root):
+ super().on_svg_root(svg_root)
+ self._screen_transformation = AffineTransformation2D.Screen(self._view_box.y.sup)
+
+ ##############################################
+
def on_group(self, group):
# self._logger.info('Group: {}\n{}'.format(group.id, group))
pass
@@ -90,6 +97,17 @@ class SceneImporter(SvgFileInternal):
def on_graphic_item(self, item):
+ # if item.id not in (
+ # 'path12061',
+ # 'path12069',
+ # 'path12077',
+ # 'path12085',
+ # ):
+ # return
+ # from Patro.FileFormat.Svg.SvgFormat import PathDataAttribute
+ # self._logger.info(str(item.id) + '\n' + str(item.path_data))
+ # item.path_data = PathDataAttribute.to_geometry(item.path_data)
+
state = self._dispatcher.state.clone().merge(item)
self._logger.info('Item: {}\n{}'.format(item.id, item))
# self._logger.info('Item State:\n' + str(state))
@@ -107,22 +125,26 @@ class SceneImporter(SvgFileInternal):
# fill_color=fill_color,
)
- transformation = state.transform
+ transformation = self._screen_transformation * state.transform
self._logger.info('Sate Transform\n' + str(transformation))
if isinstance(item, SvgFormat.Path):
# and state.stroke_dasharray is None
path = item.path_data
if path is not None: # Fixme:
+ # self._logger.info(str(item.id) + '\n' + str(path[0].geometry))
path = path.transform(transformation)
for part in path:
self._update_bounding_box(part)
+ # self._logger.info(str(item.id) + '\n' + str(path[0].geometry))
self._scene.add_path(path, path_style)
elif isinstance(item, SvgFormat.Rect):
path = item.geometry
+ path = path.transform(transformation)
self._scene.add_path(path, path_style)
####################################################################################################
+# svg_path = find_data_path('svg', 'basic-demo-2.by-hand.svg')
svg_path = find_data_path('svg', 'demo.svg')
# svg_path = find_data_path('patterns-svg', 'veravenus-little-bias-dress.pattern-a0.svg')
# svg_path = find_data_path('patterns-svg', 'veravenus-little-bias-dress.pattern-a0.no-text-zaggy.svg')
diff --git a/unit-test/FileFormat/test_svg.py b/unit-test/FileFormat/test_svg.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e1cc0a11d76266a8123009dd098ee38e46a89c5
--- /dev/null
+++ b/unit-test/FileFormat/test_svg.py
@@ -0,0 +1,220 @@
+####################################################################################################
+#
+# Patro - A Python library to make patterns for fashion design
+# Copyright (C) 2019 Salvaire Fabrice
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+####################################################################################################
+
+####################################################################################################
+
+import logging
+import unittest
+from pathlib import Path
+
+from Patro.Common.Logging import Logging
+Logging.setup_logging()
+
+from IntervalArithmetic import Interval2D
+
+from Patro.FileFormat.Svg import SvgFormat
+from Patro.FileFormat.Svg.SvgFile import SvgFile, SvgFileInternal
+from Patro.GeometryEngine.Transformation import AffineTransformation2D
+from Patro.GeometryEngine.Vector import Vector2D
+from PatroExample import find_data_path
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+svg_data = """
+
+
+"""
+
+####################################################################################################
+
+class SceneImporter(SvgFileInternal):
+
+ # Fixme: duplicated code
+
+ _logger = _module_logger.getChild('SceneImporter')
+
+ ##############################################
+
+ def __init__(self, svg_path, data=None):
+
+ self._scene = {}
+ self._bounding_box = None
+
+ super().__init__(svg_path, data)
+
+ ##############################################
+
+ def __len__(self):
+ return len(self._scene)
+
+ def __getitem__(self, name):
+ return self._scene[name]
+
+ @property
+ def scene(self):
+ return self._scene
+
+ @property
+ def bounding_box(self):
+ return self._bounding_box
+
+ ##############################################
+
+ def _add_to_scene(self, name, geometry):
+ self._scene[name] = geometry
+
+ ##############################################
+
+ def _update_bounding_box(self, item):
+
+ interval = item.bounding_box
+ if self._bounding_box is None:
+ self._bounding_box = interval
+ else:
+ self._bounding_box |= interval
+
+ ##############################################
+
+ def on_svg_root(self, svg_root):
+ super().on_svg_root(svg_root)
+ self._screen_transformation = AffineTransformation2D.Screen(self._view_box.y.sup)
+
+ ##############################################
+
+ def on_group(self, group):
+ # self._logger.info('Group: {}\n{}'.format(group.id, group))
+ pass
+
+ ##############################################
+
+ def on_graphic_item(self, item):
+
+ state = self._dispatcher.state.clone().merge(item)
+ self._logger.info('Item: {}\n{}'.format(item.id, item))
+ # self._logger.info('Item State:\n' + str(state))
+
+ transformation = state.transform
+ # transformation = self._screen_transformation * state.transform
+ self._logger.info('Sate Transform\n' + str(transformation))
+ if isinstance(item, SvgFormat.Path):
+ path = item.path_data
+ if path is not None: # Fixme:
+ path = path.transform(transformation)
+ self._update_bounding_box(path)
+ self._add_to_scene(item.id, path)
+ elif isinstance(item, SvgFormat.Rect):
+ path = item.geometry
+ self._add_to_scene(item.id, path)
+
+####################################################################################################
+
+def count_svg_tags(svg_data):
+
+ tag_counter = {}
+ for line in svg_data.splitlines():
+ line = line.strip()
+ if line.startswith('<'):
+ position = line.find(' ')
+ tag = line[1:position]
+ if tag[0].isalpha():
+ tag_counter.setdefault(tag, 0)
+ tag_counter[tag] += 1
+
+ return tag_counter
+
+####################################################################################################
+
+class TestLine2D(unittest.TestCase):
+
+ ##############################################
+
+ def test(self):
+
+ svg_path = find_data_path('svg', 'basic-demo-2.by-hand.svg')
+ #data = None
+ data = svg_data
+
+ scene_importer = SceneImporter(svg_path, data=data)
+ scene = scene_importer.scene
+
+ interval = Interval2D((20, 120), (20, 120))
+ self.assertEqual(scene_importer.bounding_box, interval)
+
+ tag_counter = count_svg_tags(data)
+ number_of_items = sum([tag_counter[x] for x in ('path', 'rect')])
+ self.assertEqual(len(scene_importer), number_of_items)
+
+ # for name, item in scene.items():
+ # print(name, item)
+
+ origin = Vector2D(20, 20)
+ for name in ('path-x', 'path-y', 'path-45'):
+ self.assertEqual(scene[name].p0, origin)
+
+ for name, p0 in (
+ ('rect-0', (20, 20)),
+ ('rect-x1', (40, 20)),
+ ('rect-x2', (80, 20)),
+ ('rect-y1', (20, 40)),
+ ('rect-y2', (20, 80)),
+ ('rect-bisect', (50, 50)),
+ #
+ # ('rect-45-2', (50, 50)),
+ # ('rect-45', (70, 70)),
+ # ('rect-30', (80, 80)),
+ # ('rect-60', (90, 90)),
+ ):
+ self.assertEqual(scene[name].p0, Vector2D(p0))
+
+####################################################################################################
+
+if __name__ == '__main__':
+
+ unittest.main()