#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2005 Aaron Spike, aaron@ekips.org # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Perspective approach & math by Dmitry Platonov, shadowjack@mail.ru, 2006 """ import inkex from inkex.localization import inkex_gettext as _ X, Y = range(2) try: import numpy as np import numpy.linalg as lin FLOAT = np.float64 except ImportError: np = None class Perspective(inkex.EffectExtension): """Apply a perspective to a path/group of paths""" def effect(self): if np is None: raise inkex.AbortExtension( _( "Failed to import the numpy or numpy.linalg modules. " "These modules are required by this extension. Please install them." " On a Debian-like system this can be done with the command, " "sudo apt-get install python-numpy." ) ) if len(self.svg.selection) != 2: raise inkex.AbortExtension( _("This extension requires two selected objects.") ) obj, envelope = self.svg.selection if isinstance(obj, (inkex.PathElement, inkex.Group)): if isinstance(envelope, inkex.PathElement): path = envelope.path.transform( envelope.composed_transform() ).to_superpath() if len(path) < 1 or len(path[0]) < 4: raise inkex.AbortExtension( _( "This extension requires that the second path be four " "nodes long." ) ) dip = np.zeros((4, 2), dtype=FLOAT) for i in range(4): dip[i][0] = path[0][i][1][0] dip[i][1] = path[0][i][1][1] # Get bounding box plus any extra composed transform of parents. bbox = obj.bounding_box(obj.getparent().composed_transform()) sip = np.array( [ [bbox.left, bbox.bottom], [bbox.left, bbox.top], [bbox.right, bbox.top], [bbox.right, bbox.bottom], ], dtype=FLOAT, ) else: if isinstance(envelope, inkex.Group): raise inkex.AbortExtension( _( "The second selected object is a group, not a" " path.\nTry using Object->Ungroup." ) ) raise inkex.AbortExtension( _( "The second selected object is not a path.\nTry using" " the procedure Path->Object to Path." ) ) else: raise inkex.AbortExtension( _( "The first selected object is neither a path nor a group.\nTry " "using the procedure Path->Object to Path." ) ) solmatrix = np.zeros((8, 8), dtype=FLOAT) free_term = np.zeros(8, dtype=FLOAT) for i in (0, 1, 2, 3): solmatrix[i][0] = sip[i][0] solmatrix[i][1] = sip[i][1] solmatrix[i][2] = 1 solmatrix[i][6] = -dip[i][0] * sip[i][0] solmatrix[i][7] = -dip[i][0] * sip[i][1] solmatrix[i + 4][3] = sip[i][0] solmatrix[i + 4][4] = sip[i][1] solmatrix[i + 4][5] = 1 solmatrix[i + 4][6] = -dip[i][1] * sip[i][0] solmatrix[i + 4][7] = -dip[i][1] * sip[i][1] free_term[i] = dip[i][0] free_term[i + 4] = dip[i][1] res = lin.solve(solmatrix, free_term) projmatrix = np.array( [[res[0], res[1], res[2]], [res[3], res[4], res[5]], [res[6], res[7], 1.0]], dtype=FLOAT, ) self.process_object(obj, projmatrix) def process_object(self, obj, matrix): if isinstance(obj, inkex.PathElement): self.process_path(obj, matrix) elif isinstance(obj, inkex.Group): self.process_group(obj, matrix) def process_group(self, group, matrix): """Go through all groups to process all paths inside them""" for node in group: self.process_object(node, matrix) def process_path(self, element, matrix): """Apply the transformation to the selected path""" point = ( element.path.to_absolute() .transform(element.composed_transform()) .to_superpath() ) for subs in point: for csp in subs: csp[0] = self.project_point(csp[0], matrix) csp[1] = self.project_point(csp[1], matrix) csp[2] = self.project_point(csp[2], matrix) element.path = inkex.Path(point).transform(-element.composed_transform()) @staticmethod def project_point(point, matrix): """Apply the matrix to the given point""" return [ (point[X] * matrix[0][0] + point[Y] * matrix[0][1] + matrix[0][2]) / (point[X] * matrix[2][0] + point[Y] * matrix[2][1] + matrix[2][2]), (point[X] * matrix[1][0] + point[Y] * matrix[1][1] + matrix[1][2]) / (point[X] * matrix[2][0] + point[Y] * matrix[2][1] + matrix[2][2]), ] if __name__ == "__main__": Perspective().run()