206 lines
8.7 KiB
Python
206 lines
8.7 KiB
Python
|
#!/usr/bin/env python3
|
||
|
#
|
||
|
# License: GPL2
|
||
|
# Copyright Mark "Klowner" Riedesel
|
||
|
# https://github.com/Klowner/inkscape-applytransforms
|
||
|
#
|
||
|
import copy
|
||
|
import math
|
||
|
from lxml import etree
|
||
|
import re
|
||
|
import inkex
|
||
|
from inkex.paths import CubicSuperPath, Path
|
||
|
from inkex.transforms import Transform
|
||
|
from inkex.styles import Style
|
||
|
|
||
|
NULL_TRANSFORM = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
||
|
|
||
|
|
||
|
class ApplyTransformations(inkex.EffectExtension):
|
||
|
|
||
|
def effect(self):
|
||
|
if self.svg.selected:
|
||
|
for id, shape in self.svg.selected.items():
|
||
|
self.recursiveFuseTransform(shape)
|
||
|
else:
|
||
|
self.recursiveFuseTransform(self.document.getroot())
|
||
|
|
||
|
@staticmethod
|
||
|
def objectToPath(element):
|
||
|
if element.tag == inkex.addNS('g', 'svg'):
|
||
|
return element
|
||
|
|
||
|
if element.tag == inkex.addNS('path', 'svg') or element.tag == 'path':
|
||
|
for attName in element.attrib.keys():
|
||
|
if ("sodipodi" in attName) or ("inkscape" in attName):
|
||
|
del element.attrib[attName]
|
||
|
return element
|
||
|
|
||
|
return element
|
||
|
|
||
|
def scaleStrokeWidth(self, element, transf):
|
||
|
if 'style' in element.attrib:
|
||
|
style = element.attrib.get('style')
|
||
|
style = dict(Style.parse_str(style))
|
||
|
update = False
|
||
|
if 'stroke-width' in style:
|
||
|
try:
|
||
|
stroke_width = self.svg.unittouu(style.get('stroke-width')) / self.svg.unittouu("1px")
|
||
|
stroke_width *= math.sqrt(abs(transf.a * transf.d - transf.b * transf.c))
|
||
|
style['stroke-width'] = str(stroke_width)
|
||
|
update = True
|
||
|
except AttributeError as e:
|
||
|
pass
|
||
|
|
||
|
if update:
|
||
|
element.attrib['style'] = Style(style).to_str()
|
||
|
|
||
|
def recursiveFuseTransform(self, element, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
|
||
|
|
||
|
transf = Transform(transf) @ Transform(element.get("transform", None)) #a, b, c, d = linear transformations / e, f = translations
|
||
|
|
||
|
if 'transform' in element.attrib:
|
||
|
del element.attrib['transform']
|
||
|
|
||
|
element = ApplyTransformations.objectToPath(element)
|
||
|
|
||
|
if transf == NULL_TRANSFORM:
|
||
|
# Don't do anything if there is effectively no transform applied
|
||
|
# reduces alerts for unsupported elements
|
||
|
pass
|
||
|
elif 'd' in element.attrib:
|
||
|
d = element.get('d')
|
||
|
p = CubicSuperPath(d)
|
||
|
p = Path(p).to_absolute().transform(transf, True)
|
||
|
element.set('d', str(Path(CubicSuperPath(p).to_path())))
|
||
|
|
||
|
self.scaleStrokeWidth(element, transf)
|
||
|
|
||
|
elif element.tag in [inkex.addNS('polygon', 'svg'),
|
||
|
inkex.addNS('polyline', 'svg')]:
|
||
|
points = element.get('points')
|
||
|
points = points.strip().split(' ')
|
||
|
for k, p in enumerate(points):
|
||
|
if ',' in p:
|
||
|
p = p.split(',')
|
||
|
p = [float(p[0]), float(p[1])]
|
||
|
p = transf.apply_to_point(p)
|
||
|
p = [str(p[0]), str(p[1])]
|
||
|
p = ','.join(p)
|
||
|
points[k] = p
|
||
|
points = ' '.join(points)
|
||
|
element.set('points', points)
|
||
|
|
||
|
self.scaleStrokeWidth(element, transf)
|
||
|
|
||
|
elif element.tag in [inkex.addNS("ellipse", "svg"), inkex.addNS("circle", "svg")]:
|
||
|
|
||
|
def isequal(a, b):
|
||
|
return abs(a - b) <= transf.absolute_tolerance
|
||
|
|
||
|
if element.TAG == "ellipse":
|
||
|
rx = float(element.get("rx"))
|
||
|
ry = float(element.get("ry"))
|
||
|
else:
|
||
|
rx = float(element.get("r"))
|
||
|
ry = rx
|
||
|
|
||
|
cx = float(element.get("cx"))
|
||
|
cy = float(element.get("cy"))
|
||
|
sqxy1 = (cx - rx, cy - ry)
|
||
|
sqxy2 = (cx + rx, cy - ry)
|
||
|
sqxy3 = (cx + rx, cy + ry)
|
||
|
newxy1 = transf.apply_to_point(sqxy1)
|
||
|
newxy2 = transf.apply_to_point(sqxy2)
|
||
|
newxy3 = transf.apply_to_point(sqxy3)
|
||
|
|
||
|
element.set("cx", (newxy1[0] + newxy3[0]) / 2)
|
||
|
element.set("cy", (newxy1[1] + newxy3[1]) / 2)
|
||
|
edgex = math.sqrt(
|
||
|
abs(newxy1[0] - newxy2[0]) ** 2 + abs(newxy1[1] - newxy2[1]) ** 2
|
||
|
)
|
||
|
edgey = math.sqrt(
|
||
|
abs(newxy2[0] - newxy3[0]) ** 2 + abs(newxy2[1] - newxy3[1]) ** 2
|
||
|
)
|
||
|
|
||
|
if not isequal(edgex, edgey) and (
|
||
|
element.TAG == "circle"
|
||
|
or not isequal(newxy2[0], newxy3[0])
|
||
|
or not isequal(newxy1[1], newxy2[1])
|
||
|
):
|
||
|
inkex.utils.errormsg(
|
||
|
"Warning: Shape %s (%s) is approximate only, try Object to path first for better results"
|
||
|
% (element.TAG, element.get("id"))
|
||
|
)
|
||
|
|
||
|
if element.TAG == "ellipse":
|
||
|
element.set("rx", edgex / 2)
|
||
|
element.set("ry", edgey / 2)
|
||
|
else:
|
||
|
element.set("r", edgex / 2)
|
||
|
|
||
|
# this is unstable at the moment
|
||
|
elif element.tag == inkex.addNS("use", "svg"):
|
||
|
href = None
|
||
|
old_href_key = '{http://www.w3.org/1999/xlink}href'
|
||
|
new_href_key = 'href'
|
||
|
if element.attrib.has_key(old_href_key) is True: # {http://www.w3.org/1999/xlink}href (which gets displayed as 'xlink:href') attribute is deprecated. the newer attribute is just 'href'
|
||
|
href = element.attrib.get(old_href_key)
|
||
|
#element.attrib.pop(old_href_key)
|
||
|
if element.attrib.has_key(new_href_key) is True:
|
||
|
href = element.attrib.get(new_href_key) #we might overwrite the previous deprecated xlink:href but it's okay
|
||
|
#element.attrib.pop(new_href_key)
|
||
|
|
||
|
#get the linked object from href attribute
|
||
|
linkedObject = self.document.getroot().xpath("//*[@id = '%s']" % href.lstrip('#')) #we must remove hashtag symbol
|
||
|
linkedObjectCopy = copy.copy(linkedObject[0])
|
||
|
objectType = linkedObject[0].tag
|
||
|
|
||
|
if objectType == inkex.addNS("image", "svg"):
|
||
|
mask = None #image might have an alpha channel
|
||
|
new_mask_id = self.svg.get_unique_id("mask")
|
||
|
newMask = None
|
||
|
if element.attrib.has_key('mask') is True:
|
||
|
mask = element.attrib.get('mask')
|
||
|
#element.attrib.pop('mask')
|
||
|
|
||
|
#get the linked mask from mask attribute. We remove the old and create a new
|
||
|
if mask is not None:
|
||
|
linkedMask = self.document.getroot().xpath("//*[@id = '%s']" % mask.lstrip('url(#').rstrip(')')) #we must remove hashtag symbol
|
||
|
linkedMask[0].delete()
|
||
|
maskAttributes = {'id': new_mask_id}
|
||
|
newMask = etree.SubElement(self.document.getroot(), inkex.addNS('mask', 'svg'), maskAttributes)
|
||
|
|
||
|
width = float(linkedObjectCopy.get('width')) * transf.a
|
||
|
height = float(linkedObjectCopy.get('height')) * transf.d
|
||
|
linkedObjectCopy.set('width', '{:1.6f}'.format(width))
|
||
|
linkedObjectCopy.set('height', '{:1.6f}'.format(height))
|
||
|
linkedObjectCopy.set('x', '{:1.6f}'.format(transf.e))
|
||
|
linkedObjectCopy.set('y', '{:1.6f}'.format(transf.f))
|
||
|
if newMask is not None:
|
||
|
linkedObjectCopy.set('mask', 'url(#' + new_mask_id + ')')
|
||
|
maskRectAttributes = {'x': '{:1.6f}'.format(transf.e), 'y': '{:1.6f}'.format(transf.f), 'width': '{:1.6f}'.format(width), 'height': '{:1.6f}'.format(height), 'style':'fill:#ffffff;'}
|
||
|
maskRect = etree.SubElement(newMask, inkex.addNS('rect', 'svg'), maskRectAttributes)
|
||
|
self.document.getroot().append(linkedObjectCopy) #for each svg:use we append a copy to the document root
|
||
|
element.delete() #then we remove the use object
|
||
|
else:
|
||
|
#self.recursiveFuseTransform(linkedObjectCopy, transf)
|
||
|
self.recursiveFuseTransform(element.unlink(), transf)
|
||
|
|
||
|
elif element.tag in [inkex.addNS('rect', 'svg'),
|
||
|
inkex.addNS('text', 'svg'),
|
||
|
inkex.addNS('image', 'svg')]:
|
||
|
element.attrib['transform'] = str(transf)
|
||
|
inkex.utils.errormsg(
|
||
|
"Shape %s (%s) not yet supported. Not all transforms will be applied. Try Object to path first"
|
||
|
% (element.TAG, element.get("id"))
|
||
|
)
|
||
|
else:
|
||
|
# e.g. <g style="...">
|
||
|
self.scaleStrokeWidth(element, transf)
|
||
|
|
||
|
for child in element.getchildren():
|
||
|
self.recursiveFuseTransform(child, transf)
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
ApplyTransformations().run()
|