Download src/Mod/Draft/importSVG.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 55.3 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/importSVG.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/Draft/importSVG.py
-
curl -L -o importSVG.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/importSVG.py
55.3 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2009 Yorik van Havre <yorik@uncreated.net> * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * 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 Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| """Provides support for importing and exporting SVG files. | |
| It enables importing/exporting objects directly to/from the 3D document | |
| but doesn't handle the SVG output from the TechDraw module. | |
| Currently it only reads the following entities: | |
| * paths, lines, circular arcs, rects, circles, ellipses, polygons, polylines. | |
| Currently unsupported: | |
| * use, image. | |
| """ | |
| ## @package importSVG | |
| # \ingroup DRAFT | |
| # \brief SVG file importer and exporter | |
| # Check code with | |
| # flake8 --ignore=E226,E266,E401,W503 | |
| __title__ = "FreeCAD Draft Workbench - SVG importer/exporter" | |
| __author__ = "Yorik van Havre, Sebastian Hoogen" | |
| __url__ = "https://www.freecad.org" | |
| # TODO: | |
| # ignoring CDATA | |
| # handle image element (external references and inline base64) | |
| # debug Problem with 'Sans' font from Inkscape | |
| # debug Problem with fill color | |
| # implement inheriting fill style from group | |
| # handle relative units | |
| import math | |
| import os | |
| import re | |
| import xml.sax | |
| import FreeCAD | |
| import Part | |
| import Draft | |
| from DraftVecUtils import equals | |
| from FreeCAD import Vector | |
| from draftutils import params | |
| from draftutils import utils | |
| from draftutils.utils import svg_precision | |
| from draftutils.translate import translate | |
| from draftutils.messages import _err, _msg, _wrn | |
| from draftutils.utils import pyopen | |
| from SVGPath import SvgPathParser | |
| import xml.etree.ElementTree as ET | |
| from copy import deepcopy | |
| if FreeCAD.GuiUp: | |
| from PySide import QtWidgets | |
| import FreeCADGui | |
| gui = True | |
| try: | |
| draftui = FreeCADGui.draftToolBar | |
| except AttributeError: | |
| draftui = None | |
| else: | |
| gui = False | |
| draftui = None | |
| svgcolors = { | |
| "Pink": (255, 192, 203), | |
| "Blue": (0, 0, 255), | |
| "Honeydew": (240, 255, 240), | |
| "Purple": (128, 0, 128), | |
| "Fuchsia": (255, 0, 255), | |
| "LawnGreen": (124, 252, 0), | |
| "Amethyst": (153, 102, 204), | |
| "Crimson": (220, 20, 60), | |
| "White": (255, 255, 255), | |
| "NavajoWhite": (255, 222, 173), | |
| "Cornsilk": (255, 248, 220), | |
| "Bisque": (255, 228, 196), | |
| "PaleGreen": (152, 251, 152), | |
| "Brown": (165, 42, 42), | |
| "DarkTurquoise": (0, 206, 209), | |
| "DarkGreen": (0, 100, 0), | |
| "MediumOrchid": (186, 85, 211), | |
| "Chocolate": (210, 105, 30), | |
| "PapayaWhip": (255, 239, 213), | |
| "Olive": (128, 128, 0), | |
| "Silver": (192, 192, 192), | |
| "PeachPuff": (255, 218, 185), | |
| "Plum": (221, 160, 221), | |
| "DarkGoldenrod": (184, 134, 11), | |
| "SlateGrey": (112, 128, 144), | |
| "MintCream": (245, 255, 250), | |
| "CornflowerBlue": (100, 149, 237), | |
| "Gold": (255, 215, 0), | |
| "HotPink": (255, 105, 180), | |
| "DarkBlue": (0, 0, 139), | |
| "LimeGreen": (50, 205, 50), | |
| "DeepSkyBlue": (0, 191, 255), | |
| "DarkKhaki": (189, 183, 107), | |
| "LightGrey": (211, 211, 211), | |
| "Yellow": (255, 255, 0), | |
| "Gainsboro": (220, 220, 220), | |
| "MistyRose": (255, 228, 225), | |
| "SandyBrown": (244, 164, 96), | |
| "DeepPink": (255, 20, 147), | |
| "Magenta": (255, 0, 255), | |
| "AliceBlue": (240, 248, 255), | |
| "DarkCyan": (0, 139, 139), | |
| "DarkSlateGrey": (47, 79, 79), | |
| "GreenYellow": (173, 255, 47), | |
| "DarkOrchid": (153, 50, 204), | |
| "OliveDrab": (107, 142, 35), | |
| "Chartreuse": (127, 255, 0), | |
| "Peru": (205, 133, 63), | |
| "Orange": (255, 165, 0), | |
| "Red": (255, 0, 0), | |
| "Wheat": (245, 222, 179), | |
| "LightCyan": (224, 255, 255), | |
| "LightSeaGreen": (32, 178, 170), | |
| "BlueViolet": (138, 43, 226), | |
| "LightSlateGrey": (119, 136, 153), | |
| "Cyan": (0, 255, 255), | |
| "MediumPurple": (147, 112, 219), | |
| "MidnightBlue": (25, 25, 112), | |
| "FireBrick": (178, 34, 34), | |
| "PaleTurquoise": (175, 238, 238), | |
| "PaleGoldenrod": (238, 232, 170), | |
| "Gray": (128, 128, 128), | |
| "MediumSeaGreen": (60, 179, 113), | |
| "Moccasin": (255, 228, 181), | |
| "Ivory": (255, 255, 240), | |
| "DarkSlateBlue": (72, 61, 139), | |
| "Beige": (245, 245, 220), | |
| "Green": (0, 128, 0), | |
| "SlateBlue": (106, 90, 205), | |
| "Teal": (0, 128, 128), | |
| "Azure": (240, 255, 255), | |
| "LightSteelBlue": (176, 196, 222), | |
| "DimGrey": (105, 105, 105), | |
| "Tan": (210, 180, 140), | |
| "AntiqueWhite": (250, 235, 215), | |
| "SkyBlue": (135, 206, 235), | |
| "GhostWhite": (248, 248, 255), | |
| "MediumTurquoise": (72, 209, 204), | |
| "FloralWhite": (255, 250, 240), | |
| "LavenderBlush": (255, 240, 245), | |
| "SeaGreen": (46, 139, 87), | |
| "Lavender": (230, 230, 250), | |
| "BlanchedAlmond": (255, 235, 205), | |
| "DarkOliveGreen": (85, 107, 47), | |
| "DarkSeaGreen": (143, 188, 143), | |
| "SpringGreen": (0, 255, 127), | |
| "Navy": (0, 0, 128), | |
| "Orchid": (218, 112, 214), | |
| "SaddleBrown": (139, 69, 19), | |
| "IndianRed": (205, 92, 92), | |
| "Snow": (255, 250, 250), | |
| "SteelBlue": (70, 130, 180), | |
| "MediumSlateBlue": (123, 104, 238), | |
| "Black": (0, 0, 0), | |
| "LightBlue": (173, 216, 230), | |
| "Turquoise": (64, 224, 208), | |
| "MediumVioletRed": (199, 21, 133), | |
| "DarkViolet": (148, 0, 211), | |
| "DarkGray": (169, 169, 169), | |
| "Salmon": (250, 128, 114), | |
| "DarkMagenta": (139, 0, 139), | |
| "Tomato": (255, 99, 71), | |
| "WhiteSmoke": (245, 245, 245), | |
| "Goldenrod": (218, 165, 32), | |
| "MediumSpringGreen": (0, 250, 154), | |
| "DodgerBlue": (30, 144, 255), | |
| "Aqua": (0, 255, 255), | |
| "ForestGreen": (34, 139, 34), | |
| "LemonChiffon": (255, 250, 205), | |
| "LightSlateGray": (119, 136, 153), | |
| "SlateGray": (112, 128, 144), | |
| "LightGray": (211, 211, 211), | |
| "Indigo": (75, 0, 130), | |
| "CadetBlue": (95, 158, 160), | |
| "LightYellow": (255, 255, 224), | |
| "DarkOrange": (255, 140, 0), | |
| "PowderBlue": (176, 224, 230), | |
| "RoyalBlue": (65, 105, 225), | |
| "Sienna": (160, 82, 45), | |
| "Thistle": (216, 191, 216), | |
| "Lime": (0, 255, 0), | |
| "Seashell": (255, 245, 238), | |
| "DarkRed": (139, 0, 0), | |
| "LightSkyBlue": (135, 206, 250), | |
| "YellowGreen": (154, 205, 50), | |
| "Aquamarine": (127, 255, 212), | |
| "LightCoral": (240, 128, 128), | |
| "DarkSlateGray": (47, 79, 79), | |
| "Khaki": (240, 230, 140), | |
| "DarkGrey": (169, 169, 169), | |
| "BurlyWood": (222, 184, 135), | |
| "LightGoldenrodYellow": (250, 250, 210), | |
| "MediumBlue": (0, 0, 205), | |
| "DarkSalmon": (233, 150, 122), | |
| "RosyBrown": (188, 143, 143), | |
| "LightSalmon": (255, 160, 122), | |
| "PaleVioletRed": (219, 112, 147), | |
| "Coral": (255, 127, 80), | |
| "Violet": (238, 130, 238), | |
| "Grey": (128, 128, 128), | |
| "LightGreen": (144, 238, 144), | |
| "Linen": (250, 240, 230), | |
| "OrangeRed": (255, 69, 0), | |
| "DimGray": (105, 105, 105), | |
| "Maroon": (128, 0, 0), | |
| "LightPink": (255, 182, 193), | |
| "MediumAquamarine": (102, 205, 170), | |
| "OldLace": (253, 245, 230), | |
| } | |
| svgcolorslower = dict((key.lower(), value) for (key, value) in list(svgcolors.items())) | |
| def getcolor(color): | |
| """Check if the given string is an RGB value, or if it is a named color. | |
| Parameters | |
| ---------- | |
| color : str | |
| Color in hexadecimal format, long '#12ab9f' or short '#1af' | |
| Returns | |
| ------- | |
| tuple | |
| (r, g, b, a) | |
| RGBA float tuple, where each value is between 0.0 and 1.0. | |
| """ | |
| if color == "none": | |
| FreeCAD.Console.PrintMessage("Color defined as 'none', defaulting to black\n") | |
| return (0.0, 0.0, 0.0, 0.0) | |
| if color[0] == "#": | |
| if len(color) == 7 or len(color) == 9: # Color string '#RRGGBB' or '#RRGGBBAA' | |
| r = float(int(color[1:3], 16) / 255.0) | |
| g = float(int(color[3:5], 16) / 255.0) | |
| b = float(int(color[5:7], 16) / 255.0) | |
| a = 1.0 | |
| if len(color) == 9: | |
| a = float(int(color[7:9], 16) / 255.0) | |
| FreeCAD.Console.PrintMessage(f"Non standard color format #RRGGBBAA : {color}\n") | |
| return (r, g, b, 1 - a) | |
| if len(color) == 4: # Color string '#RGB' | |
| # Expand the hex digits | |
| r = float(int(color[1], 16) * 17 / 255.0) | |
| g = float(int(color[2], 16) * 17 / 255.0) | |
| b = float(int(color[3], 16) * 17 / 255.0) | |
| return (r, g, b, 0.0) | |
| if color.lower().startswith("rgb(") or color.lower().startswith( | |
| "rgba(" | |
| ): # Color string 'rgb[a](0.12,0.23,0.3,0.0)' | |
| cvalues = color.lstrip("rgba(").rstrip(")").replace("%", "").split(",") | |
| if len(cvalues) == 3: | |
| a = 1.0 | |
| if "%" in color: | |
| r, g, b = [int(float(cv)) / 100.0 for cv in cvalues] | |
| else: | |
| r, g, b = [int(float(cv)) / 255.0 for cv in cvalues] | |
| if len(cvalues) == 4: | |
| if "%" in color: | |
| r, g, b, a = [int(float(cv)) / 100.0 for cv in cvalues] | |
| else: | |
| r, g, b, a = [int(float(cv)) / 255.0 for cv in cvalues] | |
| return (r, g, b, 1 - a) | |
| # Trying named color like 'MediumAquamarine' | |
| v = svgcolorslower.get(color.lower()) | |
| if v: | |
| r, g, b = [float(vf) / 255.0 for vf in v] | |
| return (r, g, b, 0.0) | |
| FreeCAD.Console.PrintWarning(f"Unknown color format : {color} : defaulting to black\n") | |
| return (0.0, 0.0, 0.0, 0.0) | |
| def transformCopyShape(shape, m): | |
| """Apply transformation matrix m on given shape. | |
| Since OCCT 6.8.0 transformShape can be used to apply certain | |
| similarity transformations on shapes. This way a conversion | |
| to BSplines in transformGeometry can be avoided. | |
| @sa: Part::TopoShape::transformGeometry(), TopoShapePy::transformGeometry() | |
| @sa: Part::TopoShape::transformShape(), TopoShapePy::transformShape() | |
| Parameters | |
| ---------- | |
| shape : Part::TopoShape | |
| A given shape | |
| m : Base::Matrix4D | |
| A transformation matrix | |
| Returns | |
| ------- | |
| shape : Part::TopoShape | |
| The shape transformed by the matrix | |
| """ | |
| try: | |
| return shape.transformShape(m, True, True) | |
| # Older versions of OCCT will refuse to work on | |
| # non-orthogonal matrices | |
| except Part.OCCError: | |
| pass | |
| return shape.transformGeometry(m) | |
| def getsize(length, mode="discard", base=1): | |
| """Parse the length string containing number and unit. | |
| Parameters | |
| ---------- | |
| length : str | |
| The length is a string, including sign, exponential notation, | |
| and unit: '+56215.14565E+6mm', '-23.156e-2px'. | |
| mode : str, optional | |
| One of 'discard', 'tuple', 'css90.0', 'css96.0', 'mm90.0', 'mm96.0'. | |
| 'discard' (default), it discards the unit suffix, and extracts | |
| a number from the given string. | |
| 'tuple', return number and unit as a tuple | |
| 'css90.0', convert the unit to pixels assuming 90 dpi | |
| 'css96.0', convert the unit to pixels assuming 96 dpi | |
| 'mm90.0', convert the unit to millimeters assuming 90 dpi | |
| 'mm96.0', convert the unit to millimeters assuming 96 dpi | |
| base : float, optional | |
| A base to scale the length. | |
| Returns | |
| ------- | |
| float | |
| The numeric value of the length, as is, or transformed to | |
| millimeters or pixels. | |
| float, string | |
| A tuple with the numeric value, and the unit if `mode='tuple'`. | |
| """ | |
| # Dictionaries to convert units to millimeters or pixels. | |
| # | |
| # The `em` and `ex` units are typographical units used in systems | |
| # like LaTeX. Here the conversion factors are arbitrarily chosen, | |
| # as they should depend on a specific font size used. | |
| # | |
| # The percentage factor is arbitrarily chosen, as it should depend | |
| # on the viewport size or for filling patterns on the bounding box. | |
| if mode == "mm90.0": | |
| tomm = { | |
| "": 25.4 / 90, # default | |
| "px": 25.4 / 90, | |
| "pt": 4.0 / 3 * 25.4 / 90, | |
| "pc": 15 * 25.4 / 90, | |
| "mm": 1.0, | |
| "cm": 10.0, | |
| "in": 25.4, | |
| "em": 15 * 2.54 / 90, | |
| "ex": 10 * 2.54 / 90, | |
| "%": 100, | |
| } | |
| elif mode == "mm96.0": | |
| tomm = { | |
| "": 25.4 / 96, # default | |
| "px": 25.4 / 96, | |
| "pt": 4.0 / 3 * 25.4 / 96, | |
| "pc": 15 * 25.4 / 96, | |
| "mm": 1.0, | |
| "cm": 10.0, | |
| "in": 25.4, | |
| "em": 15 * 2.54 / 96, | |
| "ex": 10 * 2.54 / 96, | |
| "%": 100, | |
| } | |
| elif mode == "css90.0": | |
| topx = { | |
| "": 1.0, # default | |
| "px": 1.0, | |
| "pt": 4.0 / 3, | |
| "pc": 15, | |
| "mm": 90.0 / 25.4, | |
| "cm": 90.0 / 254.0, | |
| "in": 90, | |
| "em": 15, | |
| "ex": 10, | |
| "%": 100, | |
| } | |
| elif mode == "css96.0": | |
| topx = { | |
| "": 1.0, # default | |
| "px": 1.0, | |
| "pt": 4.0 / 3, | |
| "pc": 15, | |
| "mm": 96.0 / 25.4, | |
| "cm": 96.0 / 254.0, | |
| "in": 96, | |
| "em": 15, | |
| "ex": 10, | |
| "%": 100, | |
| } | |
| # Extract a number from a string like '+56215.14565E+6mm' | |
| _num = "([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)" | |
| _unit = "(px|pt|pc|mm|cm|in|em|ex|%)?" | |
| _full_num = _num + _unit | |
| number, exponent, unit = re.findall(_full_num, length)[0] | |
| if mode == "discard": | |
| return float(number) | |
| elif mode == "tuple": | |
| return float(number), unit | |
| elif mode == "isabsolute": | |
| return unit in ("mm", "cm", "in", "px", "pt") | |
| elif mode == "mm96.0" or mode == "mm90.0": | |
| return float(number) * tomm[unit] | |
| elif mode == "css96.0" or mode == "css90.0": | |
| if unit != "%": | |
| return float(number) * topx[unit] | |
| else: | |
| return float(number) * base | |
| def getrgb(color): | |
| """Return an RGB hexadecimal string '#00aaff' from a FreeCAD color. | |
| Parameters | |
| ---------- | |
| color : Base::Color::Color | |
| FreeCAD color. | |
| Returns | |
| ------- | |
| str | |
| The hexadecimal string representation of the color '#00aaff'. | |
| """ | |
| r = str(hex(int(color[0] * 255)))[2:].zfill(2) | |
| g = str(hex(int(color[1] * 255)))[2:].zfill(2) | |
| b = str(hex(int(color[2] * 255)))[2:].zfill(2) | |
| return "#" + r + g + b | |
| class svgHandler(xml.sax.ContentHandler): | |
| """Parse SVG files and create FreeCAD objects.""" | |
| def __init__(self): | |
| super().__init__() | |
| """Retrieve Draft parameters and initialize.""" | |
| self.style = params.get_param("svgstyle") | |
| self.disableUnitScaling = params.get_param("svgDisableUnitScaling") | |
| self.make_cuts = params.get_param("svgMakeCuts") | |
| self.add_wire_for_invalid_face = params.get_param("svgAddWireForInvalidFace") | |
| self.count = 0 | |
| self.transform = None | |
| self.grouptransform = [] | |
| self.groupstyles = [] | |
| self.lastdim = None | |
| self.viewbox = None | |
| self.svgdpi = 1.0 | |
| global Part | |
| if gui and draftui: | |
| r = float(draftui.color.red() / 255.0) | |
| g = float(draftui.color.green() / 255.0) | |
| b = float(draftui.color.blue() / 255.0) | |
| rf = float(draftui.facecolor.red() / 255.0) | |
| gf = float(draftui.facecolor.green() / 255.0) | |
| bf = float(draftui.facecolor.blue() / 255.0) | |
| self.width_default = float(draftui.linewidth) | |
| else: | |
| self.width_default = float(params.get_param_view("DefaultShapeLineWidth")) | |
| r, g, b, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeLineColor")) | |
| rf, gf, bf, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeColor")) | |
| self.fill_default = (rf, gf, bf, 0.0) | |
| self.color_default = (r, g, b, 0.0) | |
| def format(self, obj): | |
| """Apply styles to the object if the graphical interface is up.""" | |
| if FreeCAD.GuiUp: | |
| v = obj.ViewObject | |
| if self.color: | |
| v.LineColor = self.color | |
| if self.width: | |
| v.LineWidth = self.width | |
| if self.fill: | |
| v.ShapeColor = self.fill | |
| def __addFaceToDoc(self, named_face): | |
| """Create a named document object from a name/face tuple | |
| Parameters | |
| ---------- | |
| named_face : name : str, face : Part.Face | |
| The Face/Wire to add, and its name | |
| """ | |
| name, face = named_face | |
| if not face: | |
| return | |
| face = self.applyTrans(face) | |
| obj = self.doc.addObject("Part::Feature", name) | |
| obj.Shape = face | |
| self.format(obj) | |
| def startElement(self, name, attrs): | |
| """Re-organize data into a nice clean dictionary. | |
| Parameters | |
| ---------- | |
| name : str | |
| Name of the element: 'path', 'rect', 'line', 'polyline', | |
| 'polygon', 'ellipse', 'circle', 'text', 'tspan', 'symbol' | |
| attrs : iterable | |
| Dictionary of content of the elements | |
| """ | |
| self.count += 1 | |
| precision = svg_precision() | |
| _msg("processing element {0}: {1}".format(self.count, name)) | |
| _msg("existing group transform: {}".format(self.grouptransform)) | |
| _msg("existing group style: {}".format(self.groupstyles)) | |
| data = {} | |
| for keyword, content in list(attrs.items()): | |
| # print(keyword, content) | |
| if keyword != "style": | |
| content = content.replace(",", " ") | |
| content = content.split() | |
| # print(keyword, content) | |
| data[keyword] = content | |
| # If it's the first element, which is <svg>, | |
| # check if the file is created by Inkscape, and its version, | |
| # in order to consider some attributes of the SVG file. | |
| if self.count == 1 and name == "svg": | |
| if "inkscape:version" in data: | |
| inks_doc_name = attrs.getValue("sodipodi:docname") | |
| inks_full_ver = attrs.getValue("inkscape:version") | |
| inks_ver_pars = re.search("\\d+\\.\\d+", inks_full_ver) | |
| if inks_ver_pars is not None: | |
| inks_ver_f = float(inks_ver_pars.group(0)) | |
| else: | |
| inks_ver_f = 99.99 | |
| # Inkscape before 0.92 used 90 dpi as resolution | |
| # Newer versions use 96 dpi | |
| if inks_ver_f < 0.92: | |
| self.svgdpi = 90.0 | |
| else: | |
| self.svgdpi = 96.0 | |
| if "inkscape:version" not in data: | |
| # exact scaling is calculated later below. Here we just want | |
| # to skip the DPI dialog if a unit is specified in the viewbox | |
| if "width" in data and "mm" in attrs.getValue("width"): | |
| self.svgdpi = 96.0 | |
| elif "width" in data and "in" in attrs.getValue("width"): | |
| self.svgdpi = 96.0 | |
| elif "width" in data and "cm" in attrs.getValue("width"): | |
| self.svgdpi = 96.0 | |
| else: | |
| _inf = ( | |
| "This SVG file does not appear to have been produced " | |
| "by Inkscape. If it does not contain absolute units " | |
| "then a DPI setting will be used." | |
| ) | |
| _qst = ( | |
| "Do you wish to use 96 dpi? Choosing 'No' " | |
| "will use the older standard 90 dpi." | |
| ) | |
| if FreeCAD.GuiUp: | |
| msgBox = QtWidgets.QMessageBox() | |
| msgBox.setText(translate("ImportSVG", _inf)) | |
| msgBox.setInformativeText(translate("ImportSVG", _qst)) | |
| msgBox.setStandardButtons( | |
| QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | |
| ) | |
| msgBox.setDefaultButton(QtWidgets.QMessageBox.No) | |
| ret = msgBox.exec_() | |
| if ret == QtWidgets.QMessageBox.Yes: | |
| self.svgdpi = 96.0 | |
| else: | |
| self.svgdpi = 90.0 | |
| if ret: | |
| _msg(translate("ImportSVG", _inf)) | |
| _msg(translate("ImportSVG", _qst)) | |
| _msg("*** User specified {} " "dpi ***".format(self.svgdpi)) | |
| else: | |
| self.svgdpi = 96.0 | |
| _msg(_inf) | |
| _msg("*** Assuming {} dpi ***".format(self.svgdpi)) | |
| if self.svgdpi == 1.0: | |
| _wrn( | |
| "This SVG file (" + inks_doc_name + ") " | |
| "has an unrecognised format which means " | |
| "the dpi could not be determined; " | |
| "assuming 96 dpi" | |
| ) | |
| self.svgdpi = 96.0 | |
| if "style" in data: | |
| if not data["style"]: | |
| # Empty style attribute stops inheriting from parent | |
| pass | |
| else: | |
| content = data["style"].replace(" ", "") | |
| content = content.split(";") | |
| for i in content: | |
| pair = i.split(":") | |
| if len(pair) > 1: | |
| data[pair[0]] = pair[1] | |
| for k in ["x", "y", "x1", "y1", "x2", "y2", "r", "rx", "ry", "cx", "cy", "width", "height"]: | |
| if k in data: | |
| data[k] = getsize(data[k][0], "css" + str(self.svgdpi)) | |
| for k in ["fill", "stroke", "stroke-width", "font-size"]: | |
| if k in data: | |
| if isinstance(data[k], list): | |
| if data[k][0].lower().startswith("rgb("): | |
| data[k] = ",".join(data[k]) | |
| else: | |
| data[k] = data[k][0] | |
| # Extract style info | |
| self.fill = None | |
| self.color = None | |
| self.width = None | |
| self.text = None | |
| if name == "svg": | |
| m = FreeCAD.Matrix() | |
| if not self.disableUnitScaling: | |
| if "width" in data and "height" in data and "viewBox" in data: | |
| if len(self.grouptransform) == 0: | |
| unitmode = "mm" + str(self.svgdpi) | |
| else: | |
| # nested svg element | |
| unitmode = "css" + str(self.svgdpi) | |
| vbw = round(getsize(data["viewBox"][2], "discard"), precision) | |
| vbh = round(getsize(data["viewBox"][3], "discard"), precision) | |
| abw = round(getsize(attrs.getValue("width"), unitmode), precision) | |
| abh = round(getsize(attrs.getValue("height"), unitmode), precision) | |
| self.viewbox = (vbw, vbh) | |
| sx = abw / vbw | |
| sy = abh / vbh | |
| preserve_ar = " ".join(data.get("preserveAspectRatio", [])).lower() | |
| if preserve_ar.startswith("none"): | |
| m.scale(Vector(sx, sy, 1)) | |
| if sx != sy: | |
| _wrn( | |
| "Non-uniform scaling with probably degenerating " | |
| + "effects on Edges. ({} vs. {}).".format(sx, sy) | |
| ) | |
| else: | |
| # preserve aspect ratio - svg default is 'x/y-mid meet' | |
| if preserve_ar.endswith("slice"): | |
| sxy = max(sx, sy) | |
| else: | |
| sxy = min(sx, sy) | |
| m.scale(Vector(sxy, sxy, 1)) | |
| elif len(self.grouptransform) == 0: | |
| # fallback to current dpi | |
| m.scale(Vector(25.4 / self.svgdpi, 25.4 / self.svgdpi, 1)) | |
| self.grouptransform.append(m) | |
| if "fill" in data: | |
| if data["fill"] != "none": | |
| self.fill = getcolor(data["fill"]) | |
| if "stroke" in data: | |
| if data["stroke"] != "none": | |
| self.color = getcolor(data["stroke"]) | |
| if "stroke-width" in data: | |
| if data["stroke-width"] != "none": | |
| self.width = getsize(data["stroke-width"], "css" + str(self.svgdpi)) | |
| if "transform" in data: | |
| m = self.getMatrix(attrs.getValue("transform")) | |
| else: | |
| m = FreeCAD.Matrix() | |
| if name == "g" or name == "a": | |
| self.grouptransform.append(m) | |
| elif name == "freecad:used": | |
| # use tag acts as g tag but has x,y attribute | |
| x = data.get("x", 0) | |
| y = data.get("y", 0) | |
| if x != 0 or y != 0: | |
| xy = FreeCAD.Matrix() | |
| xy.move(Vector(x, -y, 0)) | |
| m = m.multiply(xy) | |
| self.grouptransform.append(m) | |
| elif "transform" in data: | |
| self.transform = m | |
| if self.style == 0: | |
| if self.fill is not None: | |
| self.fill = self.fill_default | |
| self.color = self.color_default | |
| self.width = self.width_default | |
| # apply group styles | |
| if name == "g" or name == "a" or name == "freecad:used": | |
| self.groupstyles.append([self.fill, self.color, self.width]) | |
| if self.fill is None: | |
| if "fill" not in data: | |
| # do not override fill if this item has specifically set a none fill | |
| for groupstyle in reversed(self.groupstyles): | |
| if groupstyle[0] is not None: | |
| self.fill = groupstyle[0] | |
| break | |
| if self.fill is None: | |
| # svg fill default is Black | |
| self.fill = getcolor("Black") | |
| if self.color is None: | |
| for groupstyle in reversed(self.groupstyles): | |
| if groupstyle[1] is not None: | |
| self.color = groupstyle[1] | |
| break | |
| if self.width is None: | |
| for groupstyle in reversed(self.groupstyles): | |
| if groupstyle[2] is not None: | |
| self.width = groupstyle[2] | |
| break | |
| pathname = None | |
| if "id" in data: | |
| pathname = data["id"][0] | |
| _msg("name: {}".format(pathname)) | |
| # Process paths | |
| if name == "path": | |
| if not pathname: | |
| pathname = "Path" | |
| _msg("data: {}".format(data)) | |
| if "freecad:basepoint1" in data: | |
| p1 = data["freecad:basepoint1"] | |
| p1 = Vector(float(p1[0]), -float(p1[1]), 0) | |
| p2 = data["freecad:basepoint2"] | |
| p2 = Vector(float(p2[0]), -float(p2[1]), 0) | |
| p3 = data["freecad:dimpoint"] | |
| p3 = Vector(float(p3[0]), -float(p3[1]), 0) | |
| obj = Draft.make_dimension(p1, p2, p3) | |
| self.applyTrans(obj) | |
| self.format(obj) | |
| self.lastdim = obj | |
| data["d"] = [] | |
| if "d" in data: | |
| svgPath = SvgPathParser(data, pathname) | |
| svgPath.parse() | |
| svgPath.create_faces(self.fill, self.add_wire_for_invalid_face) | |
| if self.make_cuts: | |
| svgPath.doCuts() | |
| shapes = svgPath.getShapeList() | |
| for named_shape in shapes: | |
| self.__addFaceToDoc(named_shape) | |
| # Process rects | |
| if name == "rect": | |
| if not pathname: | |
| pathname = "Rectangle" | |
| edges = [] | |
| if "x" not in data: | |
| data["x"] = 0 | |
| if "y" not in data: | |
| data["y"] = 0 | |
| # Negative values are invalid | |
| _precision = 10 ** (-precision) | |
| if ("rx" not in data or data["rx"] < _precision) and ( | |
| "ry" not in data or data["ry"] < _precision | |
| ): | |
| # if True: | |
| p1 = Vector(data["x"], -data["y"], 0) | |
| p2 = Vector(data["x"] + data["width"], -data["y"], 0) | |
| p3 = Vector(data["x"] + data["width"], -data["y"] - data["height"], 0) | |
| p4 = Vector(data["x"], -data["y"] - data["height"], 0) | |
| edges.append(Part.LineSegment(p1, p2).toShape()) | |
| edges.append(Part.LineSegment(p2, p3).toShape()) | |
| edges.append(Part.LineSegment(p3, p4).toShape()) | |
| edges.append(Part.LineSegment(p4, p1).toShape()) | |
| else: | |
| # rounded edges | |
| rx = data.get("rx") | |
| ry = data.get("ry") or rx | |
| rx = rx or ry | |
| if rx > 2 * data["width"]: | |
| rx = data["width"] / 2.0 | |
| if ry > 2 * data["height"]: | |
| ry = data["height"] / 2.0 | |
| p1 = Vector(data["x"] + rx, -data["y"] - data["height"] + ry, 0) | |
| p2 = Vector(data["x"] + data["width"] - rx, -data["y"] - data["height"] + ry, 0) | |
| p3 = Vector(data["x"] + data["width"] - rx, -data["y"] - ry, 0) | |
| p4 = Vector(data["x"] + rx, -data["y"] - ry, 0) | |
| if rx < 0 or ry < 0: | |
| _wrn("Warning: 'rx' or 'ry' is negative, " "check the SVG file") | |
| if rx >= ry: | |
| e = Part.Ellipse(Vector(), rx, ry) | |
| e1a = Part.Arc(e, math.radians(180), math.radians(270)) | |
| e2a = Part.Arc(e, math.radians(270), math.radians(360)) | |
| e3a = Part.Arc(e, math.radians(0), math.radians(90)) | |
| e4a = Part.Arc(e, math.radians(90), math.radians(180)) | |
| m = FreeCAD.Matrix() | |
| else: | |
| e = Part.Ellipse(Vector(), ry, rx) | |
| e1a = Part.Arc(e, math.radians(90), math.radians(180)) | |
| e2a = Part.Arc(e, math.radians(180), math.radians(270)) | |
| e3a = Part.Arc(e, math.radians(270), math.radians(360)) | |
| e4a = Part.Arc(e, math.radians(0), math.radians(90)) | |
| # rotate +90 degrees | |
| m = FreeCAD.Matrix(0, -1, 0, 0, 1, 0) | |
| esh = [] | |
| for arc, point in ((e1a, p1), (e2a, p2), (e3a, p3), (e4a, p4)): | |
| m1 = FreeCAD.Matrix(m) | |
| m1.move(point) | |
| arc.transform(m1) | |
| esh.append(arc.toShape()) | |
| for esh1, esh2 in zip(esh[-1:] + esh[:-1], esh): | |
| p1 = esh1.Vertexes[-1].Point | |
| p2 = esh2.Vertexes[0].Point | |
| if not equals(p1, p2, precision): | |
| # straight segments | |
| _sh = Part.LineSegment(p1, p2).toShape() | |
| edges.append(_sh) | |
| # elliptical segments | |
| edges.append(esh2) | |
| sh = Part.Wire(edges) | |
| if self.fill: | |
| sh = Part.Face(sh) | |
| sh = self.applyTrans(sh) | |
| obj = self.doc.addObject("Part::Feature", pathname) | |
| obj.Shape = sh | |
| self.format(obj) | |
| # Process lines | |
| if name == "line": | |
| if not pathname: | |
| pathname = "Line" | |
| p1 = Vector(data["x1"], -data["y1"], 0) | |
| p2 = Vector(data["x2"], -data["y2"], 0) | |
| sh = Part.LineSegment(p1, p2).toShape() | |
| sh = self.applyTrans(sh) | |
| obj = self.doc.addObject("Part::Feature", pathname) | |
| obj.Shape = sh | |
| self.format(obj) | |
| # Process polylines and polygons | |
| if name == "polyline" or name == "polygon": | |
| # A simpler implementation would be | |
| # _p = zip(points[0::2], points[1::2]) | |
| # sh = Part.makePolygon([Vector(svgx, | |
| # -svgy, | |
| # 0) for svgx, svgy in _p]) | |
| # | |
| # but it would be more difficult to search for duplicate | |
| # points beforehand. | |
| if not pathname: | |
| pathname = "Polyline" | |
| points = [float(d) for d in data["points"]] | |
| lenpoints = len(points) | |
| if lenpoints >= 4 and lenpoints % 2 == 0: | |
| lastvec = Vector(points[0], -points[1], 0) | |
| path = [] | |
| if name == "polygon": | |
| points = points + points[:2] # emulate closepath | |
| for svgx, svgy in zip(points[2::2], points[3::2]): | |
| currentvec = Vector(svgx, -svgy, 0) | |
| if not equals(lastvec, currentvec, precision): | |
| seg = Part.LineSegment(lastvec, currentvec).toShape() | |
| # print("polyline seg ", lastvec, currentvec) | |
| lastvec = currentvec | |
| path.append(seg) | |
| if path: | |
| sh = Part.Wire(path) | |
| if self.fill and sh.isClosed(): | |
| sh = Part.Face(sh) | |
| sh = self.applyTrans(sh) | |
| obj = self.doc.addObject("Part::Feature", pathname) | |
| obj.Shape = sh | |
| self.format(obj) | |
| # Process ellipses | |
| if name == "ellipse": | |
| if not pathname: | |
| pathname = "Ellipse" | |
| c = Vector(data.get("cx", 0), -data.get("cy", 0), 0) | |
| rx = data["rx"] | |
| ry = data["ry"] | |
| if rx < 0 or ry < 0: | |
| _wrn("Warning: 'rx' or 'ry' is negative, check the SVG file") | |
| if rx > ry: | |
| sh = Part.Ellipse(c, rx, ry).toShape() | |
| else: | |
| sh = Part.Ellipse(c, ry, rx).toShape() | |
| sh.rotate(c, Vector(0, 0, 1), 90) | |
| if self.fill: | |
| sh = Part.Wire([sh]) | |
| sh = Part.Face(sh) | |
| sh = self.applyTrans(sh) | |
| obj = self.doc.addObject("Part::Feature", pathname) | |
| obj.Shape = sh | |
| self.format(obj) | |
| # Process circles | |
| if name == "circle" and "freecad:skip" not in data: | |
| if not pathname: | |
| pathname = "Circle" | |
| c = Vector(data.get("cx", 0), -data.get("cy", 0), 0) | |
| r = data["r"] | |
| sh = Part.makeCircle(r) | |
| if self.fill: | |
| sh = Part.Wire([sh]) | |
| sh = Part.Face(sh) | |
| sh.translate(c) | |
| sh = self.applyTrans(sh) | |
| obj = self.doc.addObject("Part::Feature", pathname) | |
| obj.Shape = sh | |
| self.format(obj) | |
| # Process texts | |
| if name in ["text", "tspan"]: | |
| if "freecad:skip" not in data: | |
| _msg("processing a text") | |
| if "x" in data: | |
| self.x = data["x"] | |
| else: | |
| self.x = 0 | |
| if "y" in data: | |
| self.y = data["y"] | |
| else: | |
| self.y = 0 | |
| if "font-size" in data: | |
| if data["font-size"] != "none": | |
| self.text = getsize(data["font-size"], "css" + str(self.svgdpi)) | |
| else: | |
| self.text = 1 | |
| else: | |
| if self.lastdim: | |
| _font_size = int(getsize(data["font-size"])) | |
| self.lastdim.ViewObject.FontSize = _font_size | |
| _msg("done processing element {}".format(self.count)) | |
| # startElement() | |
| def characters(self, content): | |
| """Read characters from the given string.""" | |
| if self.text: | |
| _msg("reading characters {}".format(content)) | |
| obj = self.doc.addObject("App::Annotation", "Text") | |
| # use ignore to not break import if char is not found in latin1 | |
| obj.LabelText = content.encode("latin1", "ignore") | |
| vec = Vector(self.x, -self.y, 0) | |
| if self.transform: | |
| vec = self.translateVec(vec, self.transform) | |
| # print("own transform: ", self.transform, vec) | |
| for transform in self.grouptransform[::-1]: | |
| # vec = self.translateVec(vec, transform) | |
| vec = transform.multiply(vec) | |
| # print("applying vector: ", vec) | |
| obj.Position = vec | |
| if FreeCAD.GuiUp: | |
| obj.ViewObject.FontSize = int(self.text) | |
| if self.fill: | |
| obj.ViewObject.TextColor = self.fill | |
| else: | |
| obj.ViewObject.TextColor = (0.0, 0.0, 0.0, 0.0) | |
| def endElement(self, name): | |
| """Finish processing the element indicated by the name. | |
| Parameters | |
| ---------- | |
| name : str | |
| The name of the element | |
| """ | |
| if name not in ["tspan"]: | |
| self.transform = None | |
| self.text = None | |
| if name == "g" or name == "a" or name == "svg" or name == "freecad:used": | |
| _msg("closing group") | |
| self.grouptransform.pop() | |
| if self.groupstyles: | |
| self.groupstyles.pop() | |
| def applyTrans(self, sh): | |
| """Apply transformation to the shape and return the new shape. | |
| Parameters | |
| ---------- | |
| sh : Part.Shape or Draft.Dimension | |
| Object to be transformed | |
| """ | |
| if isinstance(sh, Part.Shape) or isinstance(sh, Part.Wire): | |
| if self.transform: | |
| sh = transformCopyShape(sh, self.transform) | |
| for transform in self.grouptransform[::-1]: | |
| sh = transformCopyShape(sh, transform) | |
| return sh | |
| elif Draft.getType(sh) in ["Dimension", "LinearDimension"]: | |
| pts = [] | |
| for p in [sh.Start, sh.End, sh.Dimline]: | |
| cp = Vector(p) | |
| if self.transform: | |
| cp = self.transform.multiply(cp) | |
| for transform in self.grouptransform[::-1]: | |
| cp = transform.multiply(cp) | |
| pts.append(cp) | |
| sh.Start = pts[0] | |
| sh.End = pts[1] | |
| sh.Dimline = pts[2] | |
| def translateVec(self, vec, mat): | |
| """Translate (move) a point or vector by a matrix. | |
| Parameters | |
| ---------- | |
| vec : Base::Vector3 | |
| The original vector | |
| mat : Base::Matrix4D | |
| The translation matrix, from which only the elements 14, 24, 34 | |
| are used. | |
| """ | |
| v = Vector(mat.A14, mat.A24, mat.A34) | |
| return vec.add(v) | |
| def getMatrix(self, tr): | |
| """Return a FreeCAD matrix from an SVG transform attribute. | |
| Parameters | |
| ---------- | |
| tr : str | |
| The type of transform: 'matrix', 'translate', 'scale', | |
| 'rotate', 'skewX', 'skewY' and its value | |
| Returns | |
| ------- | |
| Base::Matrix4D | |
| The translated matrix. | |
| """ | |
| _op = "(matrix|translate|scale|rotate|skewX|skewY)" | |
| _val = "\\((.*?)\\)" | |
| _transf = _op + "\\s*?" + _val | |
| transformre = re.compile(_transf, re.DOTALL) | |
| m = FreeCAD.Matrix() | |
| for transformation, arguments in reversed(transformre.findall(tr)): | |
| _args_rep = arguments.replace(",", " ").split() | |
| argsplit = [float(arg) for arg in _args_rep] | |
| # m.multiply(FreeCAD.Matrix(1, 0, 0, 0, 0, -1)) | |
| # print('%s:%s %s %d' % (transformation, arguments, | |
| # argsplit, len(argsplit))) | |
| if transformation == "translate": | |
| tx = argsplit[0] | |
| ty = argsplit[1] if len(argsplit) > 1 else 0.0 | |
| m.move(Vector(tx, -ty, 0)) | |
| elif transformation == "scale": | |
| sx = argsplit[0] | |
| sy = argsplit[1] if len(argsplit) > 1 else sx | |
| m.scale(Vector(sx, sy, 1)) | |
| elif transformation == "rotate": | |
| cx = 0 | |
| cy = 0 | |
| angle = argsplit[0] | |
| if len(argsplit) >= 3: | |
| # Rotate around a non-origin centerpoint (note: SVG y axis is opposite FreeCAD y axis) | |
| cx = argsplit[1] | |
| cy = argsplit[2] | |
| m.move(Vector(-cx, cy, 0)) # Reposition for rotation | |
| # Mirroring one axis is equal to changing the direction | |
| # of rotation | |
| m.rotateZ(math.radians(-angle)) | |
| if len(argsplit) >= 3: | |
| m.move(Vector(cx, -cy, 0)) # Reverse repositioning | |
| elif transformation == "skewX": | |
| _m = FreeCAD.Matrix(1, -math.tan(math.radians(argsplit[0]))) | |
| m = m.multiply(_m) | |
| elif transformation == "skewY": | |
| _m = FreeCAD.Matrix(1, 0, 0, 0, -math.tan(math.radians(argsplit[0]))) | |
| m = m.multiply(_m) | |
| elif transformation == "matrix": | |
| # transformation matrix: | |
| # FreeCAD SVG | |
| # (+A -C +0 +E) (A C 0 E) | |
| # (-B +D -0 -F) = (-Y) * (B D 0 F) * (-Y) | |
| # (+0 -0 +1 +0) (0 0 1 0) | |
| # (+0 -0 +0 +1) (0 0 0 1) | |
| # | |
| # Put the first two rows of the matrix | |
| # fmt: off | |
| _m = FreeCAD.Matrix(argsplit[0], -argsplit[2], | |
| 0, argsplit[4], | |
| -argsplit[1], argsplit[3], | |
| 0, -argsplit[5]) | |
| # fmt: on | |
| m = m.multiply(_m) | |
| # else: | |
| # print('SKIPPED %s' % transformation) | |
| # print("m = ", m) | |
| # print("generating transformation: ", m) | |
| return m | |
| # getMatrix | |
| # class svgHandler | |
| def getContents(filename, tag, stringmode=False): | |
| """Get the contents of all occurrences of the given tag in the file. | |
| Parameters | |
| ---------- | |
| filename : str | |
| A filename to scan for tags. | |
| tag : str | |
| An SVG tag to find inside a file, for example, `some` | |
| in <some id="12">information</some> | |
| stringmode : bool, optional | |
| The default is False. | |
| If False, `filename` is a path to a file. | |
| If True, `filename` is already a pointer to an open file. | |
| Returns | |
| ------- | |
| dict | |
| A dictionary with tagids and the information associated with that id | |
| results[tagid] = information | |
| """ | |
| result = {} | |
| if stringmode: | |
| contents = filename | |
| else: | |
| # Use the native Python open which was saved as `pyopen` | |
| f = pyopen(filename) | |
| contents = f.read() | |
| f.close() | |
| # Replace the newline character with a string | |
| # so that it's easiert to parse; later on the newline character | |
| # will be restored | |
| contents = contents.replace("\n", "_linebreak") | |
| searchpat = "<" + tag + ".*?</" + tag + ">" | |
| tags = re.findall(searchpat, contents) | |
| for t in tags: | |
| tagid = re.findall(r'id="(.*?)"', t) | |
| if tagid: | |
| tagid = tagid[0] | |
| else: | |
| tagid = "none" | |
| res = t.replace("_linebreak", "\n") | |
| result[tagid] = res | |
| return result | |
| def open(filename): | |
| """Open filename and parse using the svgHandler(). | |
| Parameters | |
| ---------- | |
| filename : str | |
| The path to the filename to be opened. | |
| Returns | |
| ------- | |
| App::Document | |
| The new FreeCAD document object created, with the parsed information. | |
| """ | |
| docname = os.path.split(filename)[1] | |
| doc = FreeCAD.newDocument(docname) | |
| doc.Label = docname[:-4] | |
| # Set up the parser | |
| parser = xml.sax.make_parser() | |
| parser.setFeature(xml.sax.handler.feature_external_ges, False) | |
| handler = svgHandler() | |
| parser.setContentHandler(handler) | |
| parser._cont_handler.doc = doc | |
| # preprocess file to replace use tag to it's referenced object | |
| new_svg_content = replace_use_with_reference(filename) | |
| xml.sax.parseString(new_svg_content, handler) | |
| doc.recompute() | |
| return doc | |
| def insert(filename, docname): | |
| """Get an active document and parse using the svgHandler(). | |
| If no document exist, it is created. | |
| Parameters | |
| ---------- | |
| filename : str | |
| The path to the filename to be opened. | |
| docname : str | |
| The name of the active App::Document if one exists, or | |
| of the new one created. | |
| Returns | |
| ------- | |
| App::Document | |
| The active FreeCAD document, or the document created if none exists, | |
| with the parsed information. | |
| """ | |
| try: | |
| doc = FreeCAD.getDocument(docname) | |
| except NameError: | |
| doc = FreeCAD.newDocument(docname) | |
| FreeCAD.ActiveDocument = doc | |
| # Set up the parser | |
| parser = xml.sax.make_parser() | |
| parser.setFeature(xml.sax.handler.feature_external_ges, False) | |
| handler = svgHandler() | |
| parser.setContentHandler(handler) | |
| parser._cont_handler.doc = doc | |
| # preprocess file to replace use tag to it's referenced object | |
| new_svg_content = replace_use_with_reference(filename) | |
| xml.sax.parseString(new_svg_content, handler) | |
| doc.recompute() | |
| def export(exportList, filename): | |
| """Export the SVG file with a given list of objects. | |
| The objects must be derived from Part::Feature, in order to be processed | |
| and exported. | |
| Parameters | |
| ---------- | |
| exportList : list | |
| List of document objects to export. | |
| filename : str | |
| Path to the new file. | |
| Returns | |
| ------- | |
| None | |
| If `exportList` doesn't have shapes to export. | |
| """ | |
| svg_export_style = params.get_param("svg_export_style") | |
| if svg_export_style != 0 and svg_export_style != 1: | |
| _msg(translate("ImportSVG", "Unknown SVG export style, switching to Translated")) | |
| svg_export_style = 0 | |
| tmp = [] | |
| hidden_doc = None | |
| base_sketch_pla = None # Placement of the 1st sketch. | |
| for obj in exportList: | |
| if obj.isDerivedFrom("Sketcher::SketchObject"): | |
| if hidden_doc is None: | |
| hidden_doc = FreeCAD.newDocument(name="hidden", hidden=True, temp=True) | |
| base_sketch_pla = obj.Placement | |
| sh = Part.Compound() | |
| sh.Placement = base_sketch_pla | |
| sh.add(obj.Shape.copy()) | |
| sh.transformShape(base_sketch_pla.inverse().Matrix) | |
| new = hidden_doc.addObject("Part::Part2DObjectPython") | |
| new.Shape = sh | |
| if FreeCAD.GuiUp: | |
| for attr in ("DrawStyle", "LineColor", "LineWidth"): | |
| setattr(new.ViewObject, attr, getattr(obj.ViewObject, attr)) | |
| tmp.append(new) | |
| else: | |
| tmp.append(obj) | |
| exportList = tmp | |
| # Determine the size of the page by adding the bounding boxes | |
| # of all shapes | |
| bb = FreeCAD.BoundBox() | |
| for obj in exportList: | |
| if hasattr(obj, "Shape") and obj.Shape and obj.Shape.BoundBox.isValid(): | |
| bb.add(obj.Shape.BoundBox) | |
| else: | |
| # if Draft.get_type(obj) in ("Text", "LinearDimension", ...) | |
| _wrn("'{}': no Shape, " "calculate manual bounding box".format(obj.Label)) | |
| bb.add(Draft.get_bbox(obj)) | |
| if not bb.isValid(): | |
| _err( | |
| translate( | |
| "ImportSVG", "The export list contains no object " "with a valid bounding box" | |
| ) | |
| ) | |
| return | |
| minx = bb.XMin | |
| maxx = bb.XMax | |
| miny = bb.YMin | |
| maxy = bb.YMax | |
| if svg_export_style == 0: | |
| # translated-style exports get a bit of a margin | |
| margin = (maxx - minx) * 0.01 | |
| else: | |
| # raw-style exports get no margin | |
| margin = 0 | |
| minx -= margin | |
| maxx += margin | |
| miny -= margin | |
| maxy += margin | |
| sizex = maxx - minx | |
| sizey = maxy - miny | |
| miny += margin | |
| # Use the native Python open which was saved as `pyopen` | |
| svg = pyopen(filename, "w") | |
| # Write header. | |
| # We specify the SVG width and height in FreeCAD's physical units (mm), | |
| # and specify the viewBox so that user units maps one-to-one to mm. | |
| svg.write('<?xml version="1.0"?>\n') | |
| svg.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"') | |
| svg.write(' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n') | |
| svg.write("<svg") | |
| svg.write(' width="' + str(sizex) + 'mm" height="' + str(sizey) + 'mm"') | |
| if svg_export_style == 0: | |
| # translated-style exports have the viewbox starting at X=0, Y=0 | |
| svg.write(' viewBox="0 0 ' + str(sizex) + " " + str(sizey) + '"') | |
| else: | |
| # Raw-style exports have the viewbox starting at X=xmin, Y=-ymax | |
| # We need the negative Y here because SVG is upside down, and we | |
| # flip the sketch right-way up with a scale later | |
| svg.write(' viewBox="%f %f %f %f"' % (minx, -maxy, sizex, sizey)) | |
| svg.write(' xmlns="http://www.w3.org/2000/svg" version="1.1"') | |
| svg.write(">\n") | |
| # Write paths | |
| for ob in exportList: | |
| if svg_export_style == 0: | |
| # translated-style exports have the entire sketch translated | |
| # to fit in the X>0, Y>0 quadrant | |
| # svg.write('<g transform="translate(' | |
| # + str(-minx) + ',' + str(-miny + 2*margin) | |
| # + ') scale(1,-1)">\n') | |
| svg.write( | |
| '<g id="%s" transform="translate(%f,%f) ' 'scale(1,-1)">\n' % (ob.Name, -minx, maxy) | |
| ) | |
| else: | |
| # raw-style exports do not translate the sketch | |
| svg.write('<g id="%s" transform="scale(1,-1)">\n' % ob.Name) | |
| svg.write(Draft.get_svg(ob, override=False)) | |
| _label_enc = str(ob.Label.encode("utf8")) | |
| _label = _label_enc.replace("<", "<").replace(">", ">") | |
| # replace('"', """) | |
| svg.write("<title>%s</title>\n" % _label) | |
| svg.write("</g>\n") | |
| # Close the file | |
| svg.write("</svg>") | |
| svg.close() | |
| if hidden_doc is not None: | |
| try: | |
| App.closeDocument(hidden_doc.Name) | |
| except: | |
| pass | |
| # function to replace use tag to it's referenced object | |
| def replace_use_with_reference(file_path): | |
| # function that replace use tag to freecad:used | |
| def register_svg_namespaces(svg_content): | |
| # register namespaces | |
| xmlns_attrs = re.findall(r'\s+xmlns(?::([a-zA-Z0-9_]+))?="([^"]+)"', svg_content) | |
| for prefix, uri in xmlns_attrs: | |
| ns_prefix = "" if prefix is None or prefix == "svg" else prefix | |
| ET.register_namespace(ns_prefix, uri) | |
| def replace_use(element, tree): | |
| while True: | |
| uses = element.findall(".//{http://www.w3.org/2000/svg}use") | |
| if uses == []: | |
| break | |
| # create parent map | |
| parent_map = {child: parent for parent in tree.iter() for child in parent} | |
| for use in uses: | |
| parent = parent_map[use] | |
| href = use.attrib.get("href", "") | |
| # if href is empty, try to get xlink:href. | |
| if not href: | |
| href = use.attrib.get("{http://www.w3.org/1999/xlink}href", "") | |
| if href.startswith("#"): | |
| ref_id = href[1:] | |
| ref_element = id_map.get(ref_id) | |
| if ref_element is not None: | |
| # defs tag could not be referenced by use tag. | |
| if ref_element.tag.endswith("defs"): | |
| continue | |
| # make new element named freecad:used because use tag may have it own transform. | |
| new_element = ET.Element("freecad:used") | |
| for attr in use.attrib: | |
| # copy attribute to new one except href attribute | |
| if ( | |
| attr not in {"href", "{http://www.w3.org/1999/xlink}href"} | |
| and attr not in new_element.attrib | |
| ): | |
| new_element.set(attr, use.attrib[attr]) | |
| ref_element = deepcopy(ref_element) | |
| # change referenced symbol tag to g tag, because symbol tag will be ignored when importing. | |
| if ref_element.tag.endswith("symbol"): | |
| ref_element.tag = "g" | |
| # remove id from referenced element.(without this multiple same id problem) | |
| if "id" in ref_element.attrib: | |
| del ref_element.attrib["id"] | |
| for child in list(ref_element): | |
| # remove id from child of referenced element.(without this multiple same id problem) | |
| if "id" in child.attrib: | |
| del child.attrib["id"] | |
| new_element.append(ref_element) | |
| # replace use tag by freecad:used tag. | |
| parent.append(new_element) | |
| # remove use when referenced element is not found. | |
| parent.remove(use) | |
| # now all use tag processd | |
| # remove symbol and defs tag from tree. | |
| parent_map = {child: parent for parent in tree.iter() for child in parent} | |
| symbols = element.findall(".//{http://www.w3.org/2000/svg}symbol") | |
| for symbol in symbols: | |
| parent = parent_map[symbol] | |
| parent.remove(symbol) | |
| deftags = element.findall(".//{http://www.w3.org/2000/svg}defs") | |
| for deftag in deftags: | |
| parent = parent_map[deftag] | |
| parent.remove(deftag) | |
| # open file and read | |
| svg_content = pyopen(file_path).read() | |
| # register namespace before parsing | |
| register_svg_namespaces(svg_content) | |
| # parse as xml. | |
| tree = ET.ElementTree(ET.fromstring(svg_content)) | |
| root = tree.getroot() | |
| # create id dictionary. | |
| id_map = {} | |
| for elem in root.findall(".//*[@id]"): | |
| id_map[elem.attrib["id"]] = elem | |
| replace_use(root, tree) | |
| # return tree as xml string with namespace declaration. | |
| return ET.tostring(root, encoding="unicode", xml_declaration=True) | |