1#!/usr/local/bin/python3.8 2# 3# Copyright (C) 2016 su_v, <suv-sf@users.sf.net> 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18# 19""" 20Convert mesh gradient to path 21""" 22 23import inkex 24from inkex.elements import MeshGradient 25 26# globals 27EPSILON = 1e-3 28MG_PROPS = [ 29 'fill', 30 'stroke' 31] 32 33def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): 34 """Test approximate equality. 35 36 ref: 37 PEP 485 -- A Function for testing approximate equality 38 https://www.python.org/dev/peps/pep-0485/#proposed-implementation 39 """ 40 # pylint: disable=invalid-name 41 return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 42 43 44def reverse_path(csp): 45 """Reverse path in CSP notation.""" 46 rcsp = [] 47 for subpath in reversed(csp): 48 rsub = [list(reversed(cp)) for cp in reversed(subpath)] 49 rcsp.append(rsub) 50 return rcsp 51 52 53def join_path(csp1, sp1, csp2, sp2): 54 """Join sub-paths *sp1* and *sp2*.""" 55 pt1 = csp1[sp1][-1][1] 56 pt2 = csp2[sp2][0][1] 57 if (isclose(pt1[0], pt2[0], EPSILON) and 58 isclose(pt1[1], pt2[1], EPSILON)): 59 csp1[sp1][-1][2] = csp2[sp2][0][2] 60 csp1[sp1].extend(csp2[sp2][1:]) 61 else: 62 # inkex.debug('not close') 63 csp1.append(csp2[sp2]) 64 return csp1 65 66 67def is_url(val): 68 """Check whether attribute value is linked resource.""" 69 return val.startswith('url(#') 70 71 72def mesh_corners(meshgradient): 73 """Return list of mesh patch corners, patch paths.""" 74 rows = len(meshgradient) 75 cols = len(meshgradient[0]) 76 # first corner of mesh gradient 77 corner_x = float(meshgradient.get('x', '0.0')) 78 corner_y = float(meshgradient.get('y', '0.0')) 79 # init corner and meshpatch lists 80 corners = [[None for _ in range(cols+1)] for _ in range(rows+1)] 81 corners[0][0] = [corner_x, corner_y] 82 meshpatch_csps = [] 83 for meshrow in range(rows): 84 for meshpatch in range(cols): 85 # get start point for current meshpatch edges 86 if meshrow == 0: 87 first_corner = corners[meshrow][meshpatch] 88 if meshrow > 0: 89 first_corner = corners[meshrow][meshpatch+1] 90 # parse path of meshpatch edges 91 path = 'M {},{}'.format(*first_corner) 92 for edge in meshgradient[meshrow][meshpatch]: 93 path = ' '.join([path, edge.get('path')]) 94 csp = inkex.Path(path).to_superpath() 95 # update corner list with current meshpatch 96 if meshrow == 0: 97 corners[meshrow][meshpatch+1] = csp[0][1][1] 98 corners[meshrow+1][meshpatch+1] = csp[0][2][1] 99 if meshpatch == 0: 100 corners[meshrow+1][meshpatch] = csp[0][3][1] 101 if meshrow > 0: 102 corners[meshrow][meshpatch+1] = csp[0][0][1] 103 corners[meshrow+1][meshpatch+1] = csp[0][1][1] 104 if meshpatch == 0: 105 corners[meshrow+1][meshpatch] = csp[0][2][1] 106 # append to list of meshpatch csp 107 meshpatch_csps.append(csp) 108 return corners, meshpatch_csps 109 110 111def mesh_hvlines(meshgradient): 112 """Return lists of vertical and horizontal patch edges.""" 113 rows = len(meshgradient) 114 cols = len(meshgradient[0]) 115 # init lists for horizontal, vertical lines 116 hlines = [[None for _ in range(cols)] for _ in range(rows+1)] 117 vlines = [[None for _ in range(rows)] for _ in range(cols+1)] 118 for meshrow in range(rows): 119 for meshpatch in range(cols): 120 # horizontal edges 121 if meshrow == 0: 122 edge = meshgradient[meshrow][meshpatch][0] 123 hlines[meshrow][meshpatch] = edge.get('path') 124 edge = meshgradient[meshrow][meshpatch][2] 125 hlines[meshrow+1][meshpatch] = edge.get('path') 126 if meshrow > 0: 127 edge = meshgradient[meshrow][meshpatch][1] 128 hlines[meshrow+1][meshpatch] = edge.get('path') 129 # vertical edges 130 if meshrow == 0: 131 edge = meshgradient[meshrow][meshpatch][1] 132 vlines[meshpatch+1][meshrow] = edge.get('path') 133 if meshpatch == 0: 134 edge = meshgradient[meshrow][meshpatch][3] 135 vlines[meshpatch][meshrow] = edge.get('path') 136 if meshrow > 0: 137 edge = meshgradient[meshrow][meshpatch][0] 138 vlines[meshpatch+1][meshrow] = edge.get('path') 139 if meshpatch == 0: 140 edge = meshgradient[meshrow][meshpatch][2] 141 vlines[meshpatch][meshrow] = edge.get('path') 142 return hlines, vlines 143 144 145def mesh_to_outline(corners, hlines, vlines): 146 """Construct mesh outline as CSP path.""" 147 outline_csps = [] 148 path = 'M {},{}'.format(*corners[0][0]) 149 for edge_path in hlines[0]: 150 path = ' '.join([path, edge_path]) 151 for edge_path in vlines[-1]: 152 path = ' '.join([path, edge_path]) 153 for edge_path in reversed(hlines[-1]): 154 path = ' '.join([path, edge_path]) 155 for edge_path in reversed(vlines[0]): 156 path = ' '.join([path, edge_path]) 157 outline_csps.append(inkex.Path(path).to_superpath()) 158 return outline_csps 159 160 161def mesh_to_grid(corners, hlines, vlines): 162 """Construct mesh grid with CSP paths.""" 163 rows = len(corners) - 1 164 cols = len(corners[0]) - 1 165 gridline_csps = [] 166 # horizontal 167 path = 'M {},{}'.format(*corners[0][0]) 168 for edge_path in hlines[0]: 169 path = ' '.join([path, edge_path]) 170 gridline_csps.append(inkex.Path(path).to_superpath()) 171 for i in range(1, rows+1): 172 path = 'M {},{}'.format(*corners[i][-1]) 173 for edge_path in reversed(hlines[i]): 174 path = ' '.join([path, edge_path]) 175 gridline_csps.append(inkex.Path(path).to_superpath()) 176 # vertical 177 path = 'M {},{}'.format(*corners[-1][0]) 178 for edge_path in reversed(vlines[0]): 179 path = ' '.join([path, edge_path]) 180 gridline_csps.append(inkex.Path(path).to_superpath()) 181 for j in range(1, cols+1): 182 path = 'M {},{}'.format(*corners[0][j]) 183 for edge_path in vlines[j]: 184 path = ' '.join([path, edge_path]) 185 gridline_csps.append(inkex.Path(path).to_superpath()) 186 return gridline_csps 187 188 189def mesh_to_faces(corners, hlines, vlines): 190 """Construct mesh faces with CSP paths.""" 191 rows = len(corners) - 1 192 cols = len(corners[0]) - 1 193 face_csps = [] 194 for row in range(rows): 195 for col in range(cols): 196 # init new face 197 face = [] 198 # init edge paths 199 edge_t = hlines[row][col] 200 edge_b = hlines[row+1][col] 201 edge_l = vlines[col][row] 202 edge_r = vlines[col+1][row] 203 # top edge, first 204 if row == 0: 205 path = 'M {},{}'.format(*corners[row][col]) 206 path = ' '.join([path, edge_t]) 207 face.append(inkex.Path(path).to_superpath()[0]) 208 else: 209 path = 'M {},{}'.format(*corners[row][col+1]) 210 path = ' '.join([path, edge_t]) 211 face.append(reverse_path(inkex.Path(path).to_superpath())[0]) 212 # right edge 213 path = 'M {},{}'.format(*corners[row][col+1]) 214 path = ' '.join([path, edge_r]) 215 join_path(face, -1, inkex.Path(path).to_superpath(), 0) 216 # bottom edge 217 path = 'M {},{}'.format(*corners[row+1][col+1]) 218 path = ' '.join([path, edge_b]) 219 join_path(face, -1, inkex.Path(path).to_superpath(), 0) 220 # left edge 221 if col == 0: 222 path = 'M {},{}'.format(*corners[row+1][col]) 223 path = ' '.join([path, edge_l]) 224 join_path(face, -1, inkex.Path(path).to_superpath(), 0) 225 else: 226 path = 'M {},{}'.format(*corners[row][col]) 227 path = ' '.join([path, edge_l]) 228 join_path(face, -1, reverse_path(inkex.Path(path).to_superpath()), 0) 229 # append face to output list 230 face_csps.append(face) 231 return face_csps 232 233 234class MeshToPath(inkex.EffectExtension): 235 """Effect extension to convert mesh geometry to path data.""" 236 def add_arguments(self, pars): 237 pars.add_argument("--tab", help="The selected UI-tab") 238 pars.add_argument("--mode", default="outline", help="Edge mode") 239 240 def process_props(self, mdict, res_type='meshgradient'): 241 """Process style properties of style dict *mdict*.""" 242 result = [] 243 for key, val in mdict.items(): 244 if key in MG_PROPS: 245 if is_url(val): 246 paint_server = self.svg.getElementById(val) 247 if res_type == 'meshgradient' and isinstance(paint_server, MeshGradient): 248 result.append(paint_server) 249 return result 250 251 def process_style(self, node, res_type='meshgradient'): 252 """Process style of *node*.""" 253 result = [] 254 # Presentation attributes 255 adict = dict(node.attrib) 256 result.extend(self.process_props(adict, res_type)) 257 # Inline CSS style properties 258 result.extend(self.process_props(node.style, res_type)) 259 # TODO: check for child paint servers 260 return result 261 262 def find_meshgradients(self, node): 263 """Parse node style, return list with linked meshgradients.""" 264 return self.process_style(node, res_type='meshgradient') 265 266 # ----- Process meshgradient definitions 267 268 def mesh_to_csp(self, meshgradient): 269 """Parse mesh geometry and build csp-based path data.""" 270 271 # init variables 272 transform = None 273 mode = self.options.mode 274 275 # gradient units 276 mesh_units = meshgradient.get('gradientUnits', 'objectBoundingBox') 277 if mesh_units == 'objectBoundingBox': 278 # TODO: position and scale based on "objectBoundingBox" units 279 return 280 281 # Inkscape SVG 0.92 and SVG 2.0 draft mesh transformations 282 transform = meshgradient.gradientTransform * meshgradient.transform 283 284 # parse meshpatches, calculate absolute corner coords 285 corners, meshpatch_csps = mesh_corners(meshgradient) 286 287 if mode == 'meshpatches': 288 return meshpatch_csps, transform 289 else: 290 hlines, vlines = mesh_hvlines(meshgradient) 291 if mode == 'outline': 292 return mesh_to_outline(corners, hlines, vlines), transform 293 elif mode == 'gridlines': 294 return mesh_to_grid(corners, hlines, vlines), transform 295 elif mode == 'faces': 296 return mesh_to_faces(corners, hlines, vlines), transform 297 298 # ----- Convert meshgradient definitions 299 300 def csp_to_path(self, node, csp_list, transform=None): 301 """Create new paths based on csp data, return group with paths.""" 302 # set up stroke width, group 303 stroke_width = self.svg.unittouu('1px') 304 stroke_color = '#000000' 305 style = { 306 'fill': 'none', 307 'stroke': stroke_color, 308 'stroke-width': str(stroke_width), 309 } 310 311 group = inkex.Group() 312 # apply gradientTransform and node's preserved transform to group 313 group.transform = transform * node.transform 314 315 # convert each csp to path, append to group 316 for csp in csp_list: 317 elem = group.add(inkex.PathElement()) 318 elem.style = style 319 elem.path = inkex.CubicSuperPath(csp) 320 if self.options.mode == 'outline': 321 elem.path.close() 322 elif self.options.mode == 'faces': 323 if len(csp) == 1 and len(csp[0]) == 5: 324 elem.path.close() 325 return group 326 327 def effect(self): 328 """Main routine to convert mesh geometry to path data.""" 329 # loop through selection 330 for node in self.svg.selected.values(): 331 meshgradients = self.find_meshgradients(node) 332 # if style references meshgradient 333 if meshgradients: 334 for meshgradient in meshgradients: 335 csp_list = None 336 result = None 337 # parse mesh geometry 338 if meshgradient is not None: 339 csp_list, mat = self.mesh_to_csp(meshgradient) 340 # generate new paths with path data based on mesh geometry 341 if csp_list is not None: 342 result = self.csp_to_path(node, csp_list, mat) 343 # add result (group) to document 344 if result is not None: 345 index = node.getparent().index(node) 346 node.getparent().insert(index+1, result) 347 348 349if __name__ == '__main__': 350 MeshToPath().run() 351