matrix.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. # SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com>
  2. # SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
  3. __all__ = ("PdfMatrix", )
  4. import math
  5. import ctypes
  6. import pypdfium2.raw as pdfium_c
  7. # Note, the code below was written by a non-mathematician - might contain mistakes!
  8. # In the future, we may want to consider adding a PdfRectangle support model to calculate size and corner points.
  9. class PdfMatrix:
  10. """
  11. PDF transformation matrix helper class.
  12. See the PDF 1.7 specification, Section 8.3.3 ("Common Transformations").
  13. Note:
  14. * The PDF format uses row vectors.
  15. * Transformations operate from the origin of the coordinate system
  16. (PDF coordinates: commonly bottom left, but can be any corner in principle. Device coordinates: top left).
  17. * Matrix calculations are implemented independently in Python.
  18. * Matrix objects are immutable, so transforming methods return a new matrix.
  19. * Matrix objects implement ctypes auto-conversion to ``FS_MATRIX`` for easy use as C function parameter.
  20. Attributes:
  21. a (float): Matrix value [0][0].
  22. b (float): Matrix value [0][1].
  23. c (float): Matrix value [1][0].
  24. d (float): Matrix value [1][1].
  25. e (float): Matrix value [2][0] (X translation).
  26. f (float): Matrix value [2][1] (Y translation).
  27. """
  28. # See also pdfium/core/fxcrt/fx_coordinates.{h,cpp} (unfortunately, pdfium's matrix implementation is non-public)
  29. def __init__(self, a=1, b=0, c=0, d=1, e=0, f=0):
  30. self.a, self.b, self.c, self.d, self.e, self.f = a, b, c, d, e, f
  31. def __repr__(self):
  32. return f"PdfMatrix{self.get()}"
  33. def __eq__(self, other):
  34. if type(self) is not type(other):
  35. return False
  36. return self.get() == other.get()
  37. @property
  38. def _as_parameter_(self):
  39. return ctypes.byref( self.to_raw() )
  40. def get(self):
  41. """
  42. Get the matrix as tuple of the form (a, b, c, d, e, f).
  43. """
  44. return (self.a, self.b, self.c, self.d, self.e, self.f)
  45. @classmethod
  46. def from_raw(cls, raw):
  47. """
  48. Load a :class:`.PdfMatrix` from a raw :class:`FS_MATRIX` object.
  49. """
  50. return cls(raw.a, raw.b, raw.c, raw.d, raw.e, raw.f)
  51. def to_raw(self):
  52. """
  53. Convert the matrix to a raw :class:`FS_MATRIX` object.
  54. """
  55. return pdfium_c.FS_MATRIX(*self.get())
  56. def multiply(self, other):
  57. """
  58. Multiply this matrix by another :class:`.PdfMatrix`, to concatenate transformations.
  59. """
  60. # M1 x M2 (self x other)
  61. # (a1, b1, 0) (a2, b2, 0) (a1a2+b1c2, a1b2+b1d2, 0)
  62. # (c1, d1, 0) x (c2, d2, 0) = (c1a2+d1c2, c1b2+d1d2, 0)
  63. # (e1, f1, 1) (e2, f2, 1) (e1a2+f1c2+e2, e1b2+f1d2+f2, 1)
  64. return PdfMatrix(
  65. a = self.a*other.a + self.b*other.c,
  66. b = self.a*other.b + self.b*other.d,
  67. c = self.c*other.a + self.d*other.c,
  68. d = self.c*other.b + self.d*other.d,
  69. # corresponds to: e, f = other.on_point(self.e, self.f) - transforms X/Y translation
  70. e = self.e*other.a + self.f*other.c + other.e,
  71. f = self.e*other.b + self.f*other.d + other.f,
  72. )
  73. def translate(self, x, y):
  74. """
  75. Parameters:
  76. x (float): Horizontal shift (<0: left, >0: right).
  77. y (float): Vertical shift.
  78. """
  79. # same as return PdfMatrix(self.a, self.b, self.c, self.d, self.e+x, self.f+y)
  80. return self.multiply( PdfMatrix(1, 0, 0, 1, x, y) )
  81. def scale(self, x, y):
  82. """
  83. Parameters:
  84. x (float): A factor to scale the X axis (<1: compress, >1: stretch).
  85. y (float): A factor to scale the Y axis.
  86. """
  87. # same as return PdfMatrix(self.a*x, self.b*y, self.c*x, self.d*y, self.e*x, self.f*y)
  88. return self.multiply( PdfMatrix(x, 0, 0, y) )
  89. def rotate(self, angle, ccw=False, rad=False):
  90. """
  91. Parameters:
  92. angle (float): Angle by which to rotate the matrix.
  93. ccw (bool): If True, rotate counter-clockwise.
  94. rad (bool): If True, interpret the angle as radians.
  95. """
  96. if not rad:
  97. angle = math.radians(angle)
  98. c, s = math.cos(angle), math.sin(angle)
  99. return self.multiply( PdfMatrix(c, s, -s, c) if ccw else PdfMatrix(c, -s, s, c) )
  100. def mirror(self, invert_x, invert_y):
  101. """
  102. Parameters:
  103. invert_x (bool): If True, invert X coordinates (horizontal transform). Corresponds to flipping around the Y axis.
  104. invert_y (bool): If True, invert Y coordinates (vertical transform). Corresponds to flipping around the X axis.
  105. Note:
  106. Flipping around a vertical axis leads to a horizontal transform, and vice versa.
  107. """
  108. return self.scale(x=(-1 if invert_x else 1), y=(-1 if invert_y else 1))
  109. def skew(self, x_angle, y_angle, rad=False):
  110. """
  111. Parameters:
  112. x_angle (float): Inner angle to skew the X axis.
  113. y_angle (float): Inner angle to skew the Y axis.
  114. rad (bool): If True, interpret the angles as radians.
  115. """
  116. if not rad:
  117. x_angle = math.radians(x_angle)
  118. y_angle = math.radians(y_angle)
  119. return self.multiply( PdfMatrix(1, math.tan(x_angle), math.tan(y_angle), 1) )
  120. def on_point(self, x, y):
  121. """
  122. Returns:
  123. (float, float): Transformed point.
  124. """
  125. # (x, y) -> (ax+cy+e, bx+dy+f)
  126. return ( # new point
  127. self.a*x + self.c*y + self.e, # x
  128. self.b*x + self.d*y + self.f, # y
  129. )
  130. def on_rect(self, left, bottom, right, top):
  131. """
  132. Returns:
  133. (float, float, float, float): Transformed rectangle.
  134. """
  135. points = (
  136. self.on_point(left, top),
  137. self.on_point(left, bottom),
  138. self.on_point(right, top),
  139. self.on_point(right, bottom),
  140. )
  141. return ( # new rect
  142. min(p[0] for p in points), # left
  143. min(p[1] for p in points), # bottom
  144. max(p[0] for p in points), # right
  145. max(p[1] for p in points), # top
  146. )