2022-09-02 18:19:57 +02:00
#!/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
2023-08-03 09:54:01 +02:00
2022-09-02 18:19:57 +02:00
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 ( )
2023-08-03 09:54:01 +02:00
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
2022-09-02 18:19:57 +02:00
2024-10-22 22:11:28 +02:00
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 )
2022-09-02 18:19:57 +02:00
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 ] )
) :
2024-03-31 23:33:36 +02:00
inkex . utils . errormsg ( f " Warning: Shape { node . TAG } ( { node . get ( ' id ' ) } ) is approximate only, try Object to path first for better results " )
2022-09-02 18:19:57 +02:00
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 )
2024-10-22 22:11:28 +02:00
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 ' ) ] :
2022-09-02 18:19:57 +02:00
element . attrib [ ' transform ' ] = str ( transf )
2024-05-31 15:49:30 +02:00
inkex . utils . errormsg ( f " Shape { element . TAG } ( { element . get ( ' id ' ) } ) not yet supported. Not all transforms will be applied. Try Object to path first " )
2022-09-02 18:19:57 +02:00
else :
# e.g. <g style="...">
self . scaleStrokeWidth ( element , transf )
for child in element . getchildren ( ) :
self . recursiveFuseTransform ( child , transf )
if __name__ == ' __main__ ' :
ApplyTransformations ( ) . run ( )