Source code for pypdfium2._helpers.matrix

# SPDX-FileCopyrightText: 2024 geisserml <geisserml@gmail.com>
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause

__all__ = ("PdfMatrix", )

import math
import ctypes
import pypdfium2.raw as pdfium_c


# TODO consider adding PdfRectangle support model to calculate size and corner points
# NOTE the code below was written by a non-mathematician - might contain mistakes!


[docs] class PdfMatrix: """ PDF transformation matrix helper class. See the PDF 1.7 specification, Section 8.3.3 ("Common Transformations"). Note: * The PDF format uses row vectors. * Transformations operate from the origin of the coordinate system (PDF coordinates: bottom left corner, Device coordinates: top left corner). * Matrix calculations are implemented independently in Python. * Matrix objects are immutable, so transforming methods return a new matrix. * Matrix objects implement ctypes auto-conversion to ``FS_MATRIX`` for easy use as C function parameter. Attributes: a (float): Matrix value [0][0]. b (float): Matrix value [0][1]. c (float): Matrix value [1][0]. d (float): Matrix value [1][1]. e (float): Matrix value [2][0] (X translation). f (float): Matrix value [2][1] (Y translation). """ # See also pdfium/core/fxcrt/fx_coordinates.{h,cpp} (unfortunately, pdfium's matrix implementation is non-public) def __init__(self, a=1, b=0, c=0, d=1, e=0, f=0): self.a, self.b, self.c, self.d, self.e, self.f = a, b, c, d, e, f def __repr__(self): return f"PdfMatrix{self.get()}" def __eq__(self, other): if type(self) is not type(other): return False return (self.get() == other.get()) @property def _as_parameter_(self): return ctypes.byref( self.to_raw() )
[docs] def get(self): """ Get the matrix as tuple of the form (a, b, c, d, e, f). """ return (self.a, self.b, self.c, self.d, self.e, self.f)
[docs] @classmethod def from_raw(cls, raw): """ Load a :class:`.PdfMatrix` from a raw :class:`FS_MATRIX` object. """ return cls(raw.a, raw.b, raw.c, raw.d, raw.e, raw.f)
[docs] def to_raw(self): """ Convert the matrix to a raw :class:`FS_MATRIX` object. """ return pdfium_c.FS_MATRIX(*self.get())
[docs] def multiply(self, other): """ Multiply this matrix by another :class:`.PdfMatrix`, to concatenate transformations. """ # M1 x M2 (self x other) # (a1, b1, 0) (a2, b2, 0) (a1a2+b1c2, a1b2+b1d2, 0) # (c1, d1, 0) x (c2, d2, 0) = (c1a2+d1c2, c1b2+d1d2, 0) # (e1, f1, 1) (e2, f2, 1) (e1a2+f1c2+e2, e1b2+f1d2+f2, 1) return PdfMatrix( a = self.a*other.a + self.b*other.c, b = self.a*other.b + self.b*other.d, c = self.c*other.a + self.d*other.c, d = self.c*other.b + self.d*other.d, e = self.e*other.a + self.f*other.c + other.e, f = self.e*other.b + self.f*other.d + other.f, )
[docs] def translate(self, x, y): """ Parameters: x (float): Horizontal shift (<0: left, >0: right). y (float): Vertical shift. """ # same as return PdfMatrix(self.a, self.b, self.c, self.d, self.e+x, self.f+y) return self.multiply( PdfMatrix(1, 0, 0, 1, x, y) )
[docs] def scale(self, x, y): """ Parameters: x (float): A factor to scale the X axis (<1: compress, >1: stretch). y (float): A factor to scale the Y axis. """ # same as return PdfMatrix(self.a*x, self.b*y, self.c*x, self.d*y, self.e*x, self.f*y) return self.multiply( PdfMatrix(x, 0, 0, y) )
[docs] def rotate(self, angle, ccw=False, rad=False): """ Parameters: angle (float): Angle by which to rotate the matrix. ccw (bool): If True, rotate counter-clockwise. rad (bool): If True, interpret the angle as radians. """ if not rad: angle = math.radians(angle) c, s = math.cos(angle), math.sin(angle) return self.multiply( PdfMatrix(c, s, -s, c) if ccw else PdfMatrix(c, -s, s, c) )
[docs] def mirror(self, v, h): """ Parameters: v (bool): Whether to mirror vertically (at the Y axis). h (bool): Whether to mirror horizontall (at the X axis). """ return self.scale(x=(-1 if v else 1), y=(-1 if h else 1))
[docs] def skew(self, x_angle, y_angle, rad=False): """ Parameters: x_angle (float): Inner angle to skew the X axis. y_angle (float): Inner angle to skew the Y axis. rad (bool): If True, interpret the angles as radians. """ if not rad: x_angle = math.radians(x_angle) y_angle = math.radians(y_angle) return self.multiply( PdfMatrix(1, math.tan(x_angle), math.tan(y_angle), 1) )
[docs] def on_point(self, x, y): """ Returns: (float, float): Transformed point. """ # (x, y) -> (ax+cy+e, bx+dy+f) return ( # new point self.a*x + self.c*y + self.e, # x self.b*x + self.d*y + self.f, # y )
[docs] def on_rect(self, left, bottom, right, top): """ Returns: (float, float, float, float): Transformed rectangle. """ points = ( self.on_point(left, top), self.on_point(left, bottom), self.on_point(right, top), self.on_point(right, bottom), ) # NOTE maybe a single loop with min/max x/y vars and </> comparisons would be more efficient... return ( # new rect min(p[0] for p in points), # left min(p[1] for p in points), # bottom max(p[0] for p in points), # right max(p[1] for p in points), # top )