lib/pdf/reader/transformation_matrix.rb



# coding: utf-8
# typed: strict
# frozen_string_literal: true

class PDF::Reader
  # co-ordinate systems in PDF files are specified using a 3x3 matrix that looks
  # something like this:
  #
  #   [ a b 0 ]
  #   [ c d 0 ]
  #   [ e f 1 ]
  #
  # Because the final column never changes, we can represent each matrix using
  # only 6 numbers. This is important to save CPU time, memory and GC pressure
  # caused by allocating too many unnecessary objects.
  class TransformationMatrix
    attr_reader :a, :b, :c, :d, :e, :f

    def initialize(a, b, c, d, e, f)
      @a, @b, @c, @d, @e, @f = a, b, c, d, e, f
    end

    def inspect
      "#{a}, #{b}, 0,\n#{c}, #{d}, #{0},\n#{e}, #{f}, 1"
    end

    def to_a
      [@a,@b,0,
       @c,@d,0,
       @e,@f,1]
    end

    # multiply this matrix with another.
    #
    # the second matrix is represented by the 6 scalar values that are changeable
    # in a PDF transformation matrix.
    #
    # WARNING: This mutates the current matrix to avoid allocating memory when
    #          we don't need too. Matrices are multiplied ALL THE FREAKING TIME
    #          so this is a worthwhile optimisation
    #
    # NOTE: When multiplying matrices, ordering matters. Double check
    #       the PDF spec to ensure you're multiplying things correctly.
    #
    # NOTE: see Section 8.3.3, PDF 32000-1:2008, pp 119
    #
    # NOTE: The if statements in this method are ordered to prefer optimisations
    #       that allocate fewer objects
    #
    # TODO: it might be worth adding an optimised path for vertical
    #       displacement to speed up processing documents that use vertical
    #       writing systems
    #
    def multiply!(a,b,c, d,e,f)
      if a == 1 && b == 0 && c == 0 && d == 1 && e == 0 && f == 0
        # the identity matrix, no effect
        self
      elsif @a == 1 && @b == 0 && @c == 0 && @d == 1 && @e == 0 && @f == 0
        # I'm the identity matrix, so just copy values across
        @a = a
        @b = b
        @c = c
        @d = d
        @e = e
        @f = f
      elsif a == 1 && b == 0 && c == 0 && d == 1 && f == 0
        # the other matrix is a horizontal displacement
        horizontal_displacement_multiply!(e)
      elsif @a == 1 && @b == 0 && @c == 0 && @d == 1 && @f == 0
        # I'm a horizontal displacement
        horizontal_displacement_multiply_reversed!(a,b,c,d,e,f)
      elsif @a != 1 && @b == 0 && @c == 0 && @d != 1 && @e == 0 && @f == 0
        # I'm a xy scale
        xy_scaling_multiply_reversed!(a,b,c,d,e,f)
      elsif a != 1 && b == 0 && c == 0 && d != 1 && e == 0 && f == 0
        # the other matrix is an xy scale
        xy_scaling_multiply!(a,b,c,d,e,f)
      else
        faster_multiply!(a,b,c, d,e,f)
      end
      self
    end

    # Optimised method for when the second matrix in the calculation is
    # a simple horizontal displacement.
    #
    # Like this:
    #
    #   [ 1 2 0 ]   [ 1  0 0 ]
    #   [ 3 4 0 ] x [ 0  1 0 ]
    #   [ 5 6 1 ]   [ e2 0 1 ]
    #
    def horizontal_displacement_multiply!(e2)
      @e = @e + e2
    end

    private

    # Optimised method for when the first matrix in the calculation is
    # a simple horizontal displacement.
    #
    # Like this:
    #
    #   [ 1 0 0 ]   [ 1 2 0 ]
    #   [ 0 1 0 ] x [ 3 4 0 ]
    #   [ 5 0 1 ]   [ 5 6 1 ]
    #
    def horizontal_displacement_multiply_reversed!(a2,b2,c2,d2,e2,f2)
      newa = a2
      newb = b2
      newc = c2
      newd = d2
      newe = (@e * a2) + e2
      newf = (@e * b2) + f2
      @a, @b, @c, @d, @e, @f = newa, newb, newc, newd, newe, newf
    end

    # Optimised method for when the second matrix in the calculation is
    # an X and Y scale
    #
    # Like this:
    #
    #   [ 1 2 0 ]   [ 5 0 0 ]
    #   [ 3 4 0 ] x [ 0 5 0 ]
    #   [ 5 6 1 ]   [ 0 0 1 ]
    #
    def xy_scaling_multiply!(a2,b2,c2,d2,e2,f2)
      newa = @a * a2
      newb = @b * d2
      newc = @c * a2
      newd = @d * d2
      newe = @e * a2
      newf = @f * d2
      @a, @b, @c, @d, @e, @f = newa, newb, newc, newd, newe, newf
    end

    # Optimised method for when the first matrix in the calculation is
    # an X and Y scale
    #
    # Like this:
    #
    #   [ 5 0 0 ]   [ 1 2 0 ]
    #   [ 0 5 0 ] x [ 3 4 0 ]
    #   [ 0 0 1 ]   [ 5 6 1 ]
    #
    def xy_scaling_multiply_reversed!(a2,b2,c2,d2,e2,f2)
      newa = @a * a2
      newb = @a * b2
      newc = @d * c2
      newd = @d * d2
      newe = e2
      newf = f2
      @a, @b, @c, @d, @e, @f = newa, newb, newc, newd, newe, newf
    end

    # A general solution to multiplying two 3x3 matrixes. This is correct in all cases,
    # but slower due to excessive object allocations. It's not actually used in any
    # active code paths, but is here for reference. Use faster_multiply instead.
    #
    # Like this:
    #
    #   [ a b 0 ]   [ a b 0 ]
    #   [ c d 0 ] x [ c d 0 ]
    #   [ e f 1 ]   [ e f 1 ]
    #
    def regular_multiply!(a2,b2,c2,d2,e2,f2)
      newa = (@a * a2) + (@b * c2) + (e2 * 0)
      newb = (@a * b2) + (@b * d2) + (f2 * 0)
      newc = (@c * a2) + (@d * c2) + (e2 * 0)
      newd = (@c * b2) + (@d * d2) + (f2 * 0)
      newe = (@e * a2) + (@f * c2) + (e2 * 1)
      newf = (@e * b2) + (@f * d2) + (f2 * 1)
      @a, @b, @c, @d, @e, @f = newa, newb, newc, newd, newe, newf
    end

    # A general solution for multiplying two matrices when we know all values
    # in the final column are fixed. This is the fallback method for when none
    # of the optimised methods are applicable.
    #
    # Like this:
    #
    #   [ a b 0 ]   [ a b 0 ]
    #   [ c d 0 ] x [ c d 0 ]
    #   [ e f 1 ]   [ e f 1 ]
    #
    def faster_multiply!(a2,b2,c2, d2,e2,f2)
      newa = (@a * a2) + (@b * c2)
      newb = (@a * b2) + (@b * d2)
      newc = (@c * a2) + (@d * c2)
      newd = (@c * b2) + (@d * d2)
      newe = (@e * a2) + (@f * c2) + e2
      newf = (@e * b2) + (@f * d2) + f2
      @a, @b, @c, @d, @e, @f = newa, newb, newc, newd, newe, newf
    end
  end
end