# SPDX-FileCopyrightText: 2022 James R. Barlow # SPDX-License-Identifier: MPL-2.0 """PDF content matrix support.""" from __future__ import annotations from math import cos, pi, sin class PdfMatrix: """ Support class for PDF content stream matrices. PDF content stream matrices are 3x3 matrices summarized by a shorthand ``(a, b, c, d, e, f)`` which correspond to the first two column vectors. The final column vector is always ``(0, 0, 1)`` since this is using `homogenous coordinates `_. PDF uses row vectors. That is, ``vr @ A'`` gives the effect of transforming a row vector ``vr=(x, y, 1)`` by the matrix ``A'``. Most textbook treatments use ``A @ vc`` where the column vector ``vc=(x, y, 1)'``. (``@`` is the Python matrix multiplication operator.) Addition and other operations are not implemented because they're not that meaningful in a PDF context (they can be defined and are mathematically meaningful in general). PdfMatrix objects are immutable. All transformations on them produce a new matrix. """ def __init__(self, *args): # fmt: off if not args: self.values = ((1, 0, 0), (0, 1, 0), (0, 0, 1)) elif len(args) == 6: a, b, c, d, e, f = map(float, args) self.values = ((a, b, 0), (c, d, 0), (e, f, 1)) elif isinstance(args[0], PdfMatrix): self.values = args[0].values elif len(args[0]) == 6: a, b, c, d, e, f = map(float, args[0]) self.values = ((a, b, 0), (c, d, 0), (e, f, 1)) elif len(args[0]) == 3 and len(args[0][0]) == 3: self.values = (tuple(args[0][0]), tuple(args[0][1]), tuple(args[0][2])) else: raise ValueError('invalid arguments: ' + repr(args)) # fmt: on @staticmethod def identity(): """Constructs and returns an identity matrix.""" return PdfMatrix() def __matmul__(self, other): """Multiply this matrix by another matrix. Can be used to concatenate transformations. """ a = self.values b = other.values return PdfMatrix( [ [sum(float(i) * float(j) for i, j in zip(row, col)) for col in zip(*b)] for row in a ] ) def scaled(self, x, y): """Concatenates a scaling matrix on this matrix.""" return self @ PdfMatrix((x, 0, 0, y, 0, 0)) def rotated(self, angle_degrees_ccw): """Concatenates a rotation matrix on this matrix.""" angle = angle_degrees_ccw / 180.0 * pi c, s = cos(angle), sin(angle) return self @ PdfMatrix((c, s, -s, c, 0, 0)) def translated(self, x, y): """Translates this matrix.""" return self @ PdfMatrix((1, 0, 0, 1, x, y)) @property def shorthand(self): """Return the 6-tuple (a,b,c,d,e,f) that describes this matrix.""" return (self.a, self.b, self.c, self.d, self.e, self.f) @property def a(self): """Return matrix this value.""" return self.values[0][0] @property def b(self): """Return matrix this value.""" return self.values[0][1] @property def c(self): """Return matrix this value.""" return self.values[1][0] @property def d(self): """Return matrix this value.""" return self.values[1][1] @property def e(self): """Return matrix this value. Typically corresponds to translation on the x-axis. """ return self.values[2][0] @property def f(self): """Return matrix this value. Typically corresponds to translation on the y-axis. """ return self.values[2][1] def __eq__(self, other): if isinstance(other, PdfMatrix): return self.shorthand == other.shorthand return False def encode(self): """Encode this matrix in binary suitable for including in a PDF.""" return '{:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f}'.format( self.a, self.b, self.c, self.d, self.e, self.f ).encode() def __repr__(self): return f"pikepdf.PdfMatrix({repr(self.values)})"