Page MenuHomec4science

matrix.py
No OneTemporary

File Metadata

Created
Wed, May 1, 12:00

matrix.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" This class can be used to transform a svg/text[@transform] matrix-string into a matrix representation.
"""
# Copyright (C) University of Basel 2019 {{{1
#
# 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 3 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, see <https://www.gnu.org/licenses/> 1}}}
__author__ = "Christian Steiner"
__maintainer__ = __author__
__copyright__ = 'University of Basel'
__email__ = "christian.steiner@unibas.ch"
__status__ = "Development"
__license__ = "GPL v3"
__version__ = "0.0.1"
import re
import math
class Matrix:
"""
This class transforms a svg @transform matrix-string into a matrix representation.
Args:
transform_matrix_string (str) string of the form 'matrix(1.0 0.0 0.0 1.0 0.0 0.0)' or 'rotate(10)'
"""
A = 0
B = 1
C = 2
D = 3
E = 4
F = 5
XINDEX = 4
YINDEX = 5
MATRIX_LENGTH = 6
DOWN = 1
STRAIGHT = 0
UP = -1
def __init__(self, transform_matrix_string=None, transkription_field=None, matrix_list=[]):
self.matrix = [ 0.0 for i in range(Matrix.MATRIX_LENGTH) ] if len(matrix_list) < 6 else matrix_list
if transform_matrix_string is not None:
m = re.search('(?<=rotate\()[-]*[0-9]+', transform_matrix_string)
if m is not None: # transform='rotate(a)' to transform='matrix(cos(a), sin(a), -sin(a), cos(a), 0, 0)'
angle = float(m.group(0))
self.matrix[Matrix.A] = round(math.cos(math.radians(angle)), 3)
self.matrix[Matrix.B] = round(math.sin(math.radians(angle)), 3)
self.matrix[Matrix.C] = round(math.sin(math.radians(angle))*-1, 3)
self.matrix[Matrix.D] = round(math.cos(math.radians(angle)), 3)
self.matrix[Matrix.E] = 0
self.matrix[Matrix.F] = 0
elif re.search(r'matrix\(\s*([-]*\d+(\.\d+(e-\d+)*)*[,\s][\s]*){5}[-]*\d+(\.\d+)*.*\s*\)', transform_matrix_string):
#elif re.search(r'matrix\(\s*([-]*[0-9].*\s){5}[-]*[0-9].*\s*\)', transform_matrix_string):
# old-> does not include comma separated matrix string
self.matrix = [ float(i) for i in transform_matrix_string.replace('matrix(','').\
replace(', ', ',').replace(',', ' ').replace(')','').split(' ') ]
else:
raise Exception('Error: string "{}" is not a valid transform matrix string!'.format(transform_matrix_string))
if transkription_field is not None:
self.matrix[Matrix.XINDEX] -= transkription_field.xmin
self.matrix[Matrix.YINDEX] -= transkription_field.ymin
if(len(self.matrix) < Matrix.MATRIX_LENGTH):
raise Exception('Error: string "{}" is not a valid matrix string!'.format(transform_matrix_string))
def add2X(self, add_to_x=0):
"""Return x-value of matrix (float) + add_to_x.
"""
return self.matrix[Matrix.XINDEX] + float(add_to_x)
def add2Y(self, add_to_y=0):
"""Return y-value of matrix (float) + add_to_y.
"""
return self.matrix[Matrix.YINDEX] + float(add_to_y)
def getX(self):
"""Return x-value of matrix (float).
"""
return self.matrix[Matrix.XINDEX]
def getY(self):
"""Return y-value of matrix (float).
"""
return self.matrix[Matrix.YINDEX]
def is_matrix_horizontal(self):
"""Returns whether matrix is horizontal.
[:return:] True/False
"""
return self.matrix[Matrix.A] == 1 and self.matrix[Matrix.B] == 0 and self.matrix[Matrix.C] == 0 and self.matrix[Matrix.D] == 1
def get_new_x(self, x=0.0, y=0.0):
"""Returns new position of x.
:return: (float) x
"""
top_left_x = x - self.matrix[self.E] if x != 0.0 else 0.0
top_left_y = y - self.matrix[self.F] if y != 0.0 else 0.0
return self.matrix[Matrix.A] * top_left_x + self.matrix[Matrix.C] * top_left_y + self.matrix[self.E]
def get_new_y(self, x=0.0, y=0.0):
"""Returns new position of y.
:return: (float) y
"""
top_left_x = x - self.matrix[self.E] if x != 0.0 else 0.0
top_left_y = y - self.matrix[self.F] if y != 0.0 else 0.0
return self.matrix[Matrix.B] * top_left_x + self.matrix[Matrix.D] * top_left_y + self.matrix[self.F]
def get_old_x(self, x=0.0, y=0.0):
"""Returns old position of x.
:return: (float) x
"""
old_x = (self.matrix[self.D]*x - self.matrix[Matrix.D]*self.matrix[Matrix.E] - self.matrix[Matrix.C]*y + self.matrix[Matrix.C]*self.matrix[Matrix.F])\
/(self.matrix[Matrix.A]*self.matrix[Matrix.D] - self.matrix[Matrix.B]*self.matrix[Matrix.C])
return self.add2X(old_x)
def get_transformed_positions(self, x=0.0, y=0.0, width=0.0, height=0.0):
"""Returns transformed x, y, width and height.
"""
top_left_x = x
top_left_y = y
top_right_x = x + width
top_right_y = y
bottom_left_x = x
bottom_left_y = y + height
bottom_right_x = x + width
bottom_right_y = y + height
new_x = self.matrix[Matrix.A] * top_left_x + self.matrix[Matrix.C] * top_left_y + self.matrix[self.E]
new_y = self.matrix[Matrix.B] * top_left_x + self.matrix[Matrix.D] * top_left_y + self.matrix[self.F]
new_top_right_x = self.matrix[Matrix.A] * top_right_x + self.matrix[Matrix.C] * top_right_y + self.matrix[self.E]
new_top_right_y = self.matrix[Matrix.B] * top_right_x + self.matrix[Matrix.D] * top_right_y + self.matrix[self.F]
new_bottom_left_x = self.matrix[Matrix.A] * bottom_left_x + self.matrix[Matrix.C] * bottom_left_y + self.matrix[self.E]
new_bottom_left_y = self.matrix[Matrix.B] * bottom_left_x + self.matrix[Matrix.D] * bottom_left_y + self.matrix[self.F]
new_bottom_right_x = self.matrix[Matrix.A] * bottom_right_x + self.matrix[Matrix.C] * bottom_right_y + self.matrix[self.E]
new_bottom_right_y = self.matrix[Matrix.B] * bottom_right_x + self.matrix[Matrix.D] * bottom_right_y + self.matrix[self.F]
new_width = abs(new_top_right_x - new_x)\
if abs(new_top_right_x - new_x) >= abs(new_bottom_right_x - new_bottom_left_x)\
else abs(new_bottom_right_x - new_bottom_left_x)
new_height = abs(new_bottom_left_y - new_y)\
if abs(new_bottom_left_y - new_y) >= abs(new_top_right_y - new_bottom_right_y)\
else abs(new_top_right_y - new_bottom_right_y)
return new_x, new_y, new_width, new_height
def clone_transformation_matrix(self):
"""Returns a matrix that contains only the transformation part.
[:return:] (Matrix) a clone of this matrix
"""
return Matrix(matrix_list=self.matrix[0:4]+[0,0])
def isRotationMatrix(self):
"""Return whether matrix is a rotation matrix.
"""
return self.matrix[Matrix.A] < 1 or self.matrix[Matrix.B] != 0
def toCSSTransformString(self):
"""Returns the CSS3 transform string: 'rotate(Xdeg)' where X is the angle.
"""
angle = 0
if self.isRotationMatrix():
angle = int(round(math.degrees(math.asin(self.matrix[Matrix.B])), 0))
if angle == 0:
angle = int(round(math.degrees(math.acos(self.matrix[Matrix.A])), 0))
return 'rotate({}deg)'.format(angle)
def toString(self):
"""Returns a transform_matrix_string representation of the matrix.
[:returns:] (str) 'matrix(X X X X X X)'
"""
return 'matrix(' + ' '.join([ str(round(x, 5)) for x in self.matrix ]) + ')'
def get_rotation_direction(self):
"""Get rotation direction of rotation matrix.
[:return:] (int) direction code Matrix.UP, Matrix.STRAIGHT, Matrix.DOWN
"""
if not self.isRotationMatrix():
return self.STRAIGHT
else:
angle = int(round(math.degrees(math.asin(self.matrix[Matrix.B])), 0))
return self.UP if angle < 0 else self.DOWN
@staticmethod
def IS_IN_FOOTNOTE_AREA(transform_matrix_string, transkription_field, x=0.0):
"""Returns true if matrix specifies a position that is part of the footnote area.
text_node (lxml.etree.Element)
transkription_field (datatypes.transkription_field.TranskriptionField)
"""
matrix = Matrix(transform_matrix_string=transform_matrix_string)
if matrix.getY() < transkription_field.ymax:
return False
is_part = matrix.getX() + x > transkription_field.xmin\
if transkription_field.is_page_verso()\
else matrix.getX() + x > transkription_field.documentWidth/4
return is_part
@staticmethod
def NODE_HAS_CONTENT_IN_FOOTNOTE_AREA(node, transkription_field):
"""Returns true if matrix specifies a position that is part of the footnote area.
text_node (lxml.etree.Element)
transkription_field (datatypes.transkription_field.TranskriptionField)
"""
matrix = Matrix(transform_matrix_string=node.get('transform'))
if matrix.getY() < transkription_field.ymax:
return False
x = sorted([ float(x.get('x')) for x in node.getchildren()])[-1]\
if len(node.getchildren()) > 0 else 0.0
is_part = matrix.getX() + x > transkription_field.xmin\
if transkription_field.is_page_verso()\
else matrix.getX() + x > transkription_field.documentWidth/4
return is_part
@staticmethod
def IS_IN_MARGIN_FIELD(transform_matrix_string, transkription_field):
"""Returns true if matrix specifies a position that is part of the margin field.
text_node (lxml.etree.Element)
transkription_field (datatypes.transkription_field.TranskriptionField)
"""
line_number_area_width = 15\
if transkription_field.line_number_area_width == 0.0\
else transkription_field.line_number_area_width
matrix = Matrix(transform_matrix_string=transform_matrix_string)
if matrix.getY() < transkription_field.ymin or matrix.getY() > transkription_field.ymax:
return False
is_part = matrix.getX() < transkription_field.xmin - line_number_area_width\
if transkription_field.is_page_verso()\
else matrix.getX() > transkription_field.xmax + line_number_area_width
return is_part
@staticmethod
def IS_IN_PLACE_OF_PRINTING_AREA(transform_matrix_string, transkription_field):
"""Returns true if matrix specifies a position that is part of the area where the places of printing ('Druckorte') are printed.
text_node (lxml.etree.Element)
transkription_field (datatypes.transkription_field.TranskriptionField)
"""
matrix = Matrix(transform_matrix_string=transform_matrix_string)
if matrix.getY() < transkription_field.ymax:
return False
is_part = matrix.getX() < transkription_field.xmin\
if transkription_field.is_page_verso()\
else matrix.getX() < transkription_field.documentWidth/4
return is_part
@staticmethod
def IS_PART_OF_TRANSKRIPTION_FIELD(transkription_field, text_node=None, matrix=None):
"""Returns true if matrix specifies a position that is part of transkription field.
text_node (lxml.etree.Element)
transkription_field (datatypes.transkription_field.TranskriptionField)
"""
if matrix is None and not bool(text_node.get('transform')):
return False
if matrix is None:
matrix = Matrix(transform_matrix_string=text_node.get('transform'))
is_part = matrix.getX() > transkription_field.xmin and matrix.getX() < transkription_field.xmax\
and matrix.getY() > transkription_field.ymin and matrix.getY() < transkription_field.ymax
if not is_part and matrix.isRotationMatrix() and len([child.text for child in text_node.getchildren() if not re.match(r'^\s*$', child.text)]) > 0:
first_tspan_node = [ child for child in text_node.getchildren() if not re.match(r'^\s*$', child.text)][0]
x = matrix.add2X(float(first_tspan_node.get('x')))
y = matrix.add2Y(float(first_tspan_node.get('y')))
new_x = matrix.get_new_x(x=x, y=y)
new_y = matrix.get_new_y(x=x, y=y)
return new_x > transkription_field.xmin and new_x < transkription_field.xmax\
and new_y > transkription_field.ymin and new_y < transkription_field.ymax
return is_part
@staticmethod
def IS_NEARX_TRANSKRIPTION_FIELD(transform_matrix_string, transkription_field, diffx=20.0):
"""Returns true if matrix specifies a position that is on its x axis near the transkription_field.
transform_matrix_string (str): string from which to init Matrix.
transkription_field (svgscripts.TranskriptionField)
diffx (float): defines threshold for positions that count as near.
"""
matrix = Matrix(transform_matrix_string=transform_matrix_string)
MINLEFT = transkription_field.xmin - diffx
MAXRIGHT = transkription_field.xmax + diffx
return matrix.getY() > transkription_field.ymin and matrix.getY() < transkription_field.ymax\
and ((matrix.getX() > MINLEFT and matrix.getX() < transkription_field.xmin)\
or (matrix.getX() > transkription_field.xmax and matrix.getX() < MAXRIGHT))
@staticmethod
def DO_CONVERSION_FACTORS_DIFFER(matrix_a, matrix_b, diff_threshold=0.001):
"""Returns whether the conversion factors (a-d) differ more than diff_threshold.
"""
if matrix_a is None or matrix_b is None:
return not (matrix_a is None and matrix_b is None)
return abs(matrix_a.matrix[Matrix.A] - matrix_b.matrix[Matrix.A]) > diff_threshold\
or abs(matrix_a.matrix[Matrix.B] - matrix_b.matrix[Matrix.B]) > diff_threshold\
or abs(matrix_a.matrix[Matrix.C] - matrix_b.matrix[Matrix.C]) > diff_threshold\
or abs(matrix_a.matrix[Matrix.D] - matrix_b.matrix[Matrix.D]) > diff_threshold
def __eq__(self, other):
"""Return self.matrix == other.matrix.
"""
if other is None:
return False
return self.matrix == other.matrix
def __hash__(self):
"""Return hash value.
"""
return hash((self.matrix[Matrix.E], self.matrix[Matrix.F]))

Event Timeline