mightyscape-1.2/extensions/fablabchemnitz/apply_transformations/apply_transformations.py

259 lines
11 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()
if 'stroke-width' in element.attrib:
style = element.attrib.get('style')
style = dict(Style.parse_str(style))
update = False
try:
stroke_width = self.svg.unittouu(element.attrib.get('stroke-width')) / self.svg.unittouu("1px")
stroke_width *= math.sqrt(abs(transf.a * transf.d - transf.b * transf.c))
element.attrib['stroke-width'] = str(stroke_width)
update = True
except AttributeError as e:
pass
def transformRectangle(self, node, transf: Transform):
x = float(node.get('x', '0'))
y = float(node.get('y', '0'))
width = float(node.get('width', '0'))
height = float(node.get('height', '0'))
rx = float(node.get('rx', '0'))
ry = float(node.get('ry', '0'))
# Extract translation, scaling and rotation
a, b, c, d = transf.a, transf.b, transf.c, transf.d
tx, ty = transf.e, transf.f
sx = math.sqrt(a**2 + c**2)
sy = math.sqrt(b**2 + d**2)
angle = math.degrees(math.atan2(b, a))
# Calculate the center of the rectangle
cx = x + width / 2
cy = y + height / 2
# Apply the transformation to the center point
new_cx, new_cy = transf.apply_to_point((cx, cy))
new_x = new_cx - (width * sx) / 2
new_y = new_cy - (height * sy) / 2
# Update rectangle attributes
node.set('x', str(new_x))
node.set('y', str(new_y))
node.set('width', str(width * sx))
node.set('height', str(height * sy))
# Apply scale to rx and ry if they exist
if rx > 0:
node.set('rx', str(rx * sx))
if ry > 0:
node.set('ry', str(ry * sy))
# Add rotation if it exists
if abs(angle) > 1e-6:
tr = Transform(f"rotate({angle:.6f},{new_cx:.6f},{new_cy:.6f})")
node.set('transform',tr)
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(f"Warning: Shape {node.TAG} ({node.get('id')}) is approximate only, try Object to path first for better results")
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 == inkex.addNS('rect', 'svg'):
self.transformRectangle(element, transf)
self.scaleStrokeWidth(element, transf)
elif element.tag in [inkex.addNS('text', 'svg'),
inkex.addNS('image', 'svg'),
inkex.addNS('use', 'svg')]:
element.attrib['transform'] = str(transf)
inkex.utils.errormsg(f"Shape {element.TAG} ({element.get('id')}) not yet supported. Not all transforms will be applied. Try Object to path first")
else:
# e.g. <g style="...">
self.scaleStrokeWidth(element, transf)
for child in element.getchildren():
self.recursiveFuseTransform(child, transf)
if __name__ == '__main__':
ApplyTransformations().run()