defmodule Pdf.Reader.GraphicsState do @moduledoc """ Struct and operations for the PDF graphics state during content stream interpretation. CTM (current transformation matrix) and text matrices are stored as 6-float tuples `{a, b, c, d, e, f}` — the affine subset used by PDF (§8.3.3 of the PDF spec). Full 4×4 matrices are NOT used; PDF only needs the 2D affine form. The `:stack` holds snapshots pushed by the `q` operator and restored by `Q`. ## Text state additions (§ 9.4.4) The `:widths_fn` field holds the per-glyph width closure for the active font. It is set by the `Tf` operator handler and used by `advance_tm/2` to compute exact horizontal text advance per the full § 9.4.4 formula. When `nil`, advance defaults to 0 per glyph (only `Tc`, `Tw`, and `Tfs` terms contribute). ## Matrix convention PDF uses the row-vector convention (§ 8.3.3): | a b 0 | | c d 0 | | e f 1 | Multiplication: M3 = M1 × M2 with formulas: a3 = a1*a2 + b1*c2 b3 = a1*b2 + b1*d2 c3 = c1*a2 + d1*c2 d3 = c1*b2 + d1*d2 e3 = e1*a2 + f1*c2 + e2 f3 = e1*b2 + f1*d2 + f2 """ @type matrix :: {float(), float(), float(), float(), float(), float()} @identity {1.0, 0.0, 0.0, 1.0, 0.0, 0.0} @type t :: %__MODULE__{ ctm: matrix(), tm: matrix(), tlm: matrix(), font: nil | binary(), font_size: float(), leading: float(), char_spacing: float(), word_spacing: float(), horizontal_scaling: float(), # percentage (100.0 = normal); Th = value / 100.0 rise: float(), widths_fn: nil | (binary() -> [non_neg_integer()]), stack: [t()] } defstruct ctm: @identity, tm: @identity, tlm: @identity, font: nil, font_size: 0.0, leading: 0.0, char_spacing: 0.0, word_spacing: 0.0, # horizontal_scaling is stored as a percentage (e.g. 100.0 = normal). # Th = horizontal_scaling / 100.0. Default per PDF spec § 9.4.4 is 100%. horizontal_scaling: 100.0, rise: 0.0, widths_fn: nil, stack: [] # --------------------------------------------------------------------------- # Constructor # --------------------------------------------------------------------------- @doc "Returns a fresh GraphicsState with identity matrices and zeroed text state." @spec new() :: t() def new, do: %__MODULE__{} # --------------------------------------------------------------------------- # Matrix operations # --------------------------------------------------------------------------- @doc """ Multiplies two affine matrices under the PDF row-vector convention. `multiply(m1, m2)` returns `M3 = M1 × M2`. Both arguments and the result are 6-element `{a, b, c, d, e, f}` tuples. Spec reference: PDF 1.7 § 8.3.4. """ @spec multiply(matrix(), matrix()) :: matrix() def multiply( {a1, b1, c1, d1, e1, f1}, {a2, b2, c2, d2, e2, f2} ) do { a1 * a2 + b1 * c2, a1 * b2 + b1 * d2, c1 * a2 + d1 * c2, c1 * b2 + d1 * d2, e1 * a2 + f1 * c2 + e2, e1 * b2 + f1 * d2 + f2 } end # --------------------------------------------------------------------------- # Stack operations — `q` and `Q` # --------------------------------------------------------------------------- @doc """ Pushes the current graphics state onto `:stack` (implements PDF `q` operator). The full state struct is saved. No fields are excluded — this keeps semantics consistent with the writer's `q`/`Q` discipline. """ @spec push(t()) :: t() def push(%__MODULE__{stack: stack} = state) do %{state | stack: [state | stack]} end @doc """ Pops the most-recently-pushed graphics state from `:stack` (implements PDF `Q` operator). If the stack is empty (malformed stream), this is a silent no-op — the interpreter remains in the current state rather than crashing. Spec allows senders to have unbalanced `q`/`Q` in practice (e.g. content streams generated without strict nesting). """ @spec pop(t()) :: t() def pop(%__MODULE__{stack: []} = state), do: state def pop(%__MODULE__{stack: [saved | rest]}) do %{saved | stack: rest} end end