///|
/// A two-dimensional point in PDF user-space coordinates.
pub(all) struct Point2 {
x : Double
y : Double
} derive(Debug, Eq, ToJson)
///|
/// A high-level affine transform operation.
///
/// Operation lists preserve CamlPDF's transform model; they can be rendered for
/// diagnostics or lowered to `TransformMatrix` for application to points.
pub(all) enum TransformOp {
Scale(Point2, Double, Double)
Rotate(Point2, Double)
Translate(Double, Double)
ShearX(Point2, Double)
ShearY(Point2, Double)
} derive(Debug, Eq, ToJson)
///|
/// A PDF affine transform matrix.
///
/// The fields correspond to PDF's six-operand matrix `[a b c d e f]`, where a
/// point is transformed as `x' = a*x + c*y + e` and
/// `y' = b*x + d*y + f`.
pub(all) struct TransformMatrix {
a : Double
b : Double
c : Double
d : Double
e : Double
f : Double
} derive(Debug, Eq, ToJson)
///|
/// The decomposed components of an affine transform matrix.
///
/// Values are expressed as scale, aspect ratio, rotation, shear, and
/// translation components suitable for recomposition.
pub(all) struct TransformDecomposition {
scale : Double
aspect : Double
rotation : Double
shear : Double
translate_x : Double
translate_y : Double
} derive(Debug, Eq, ToJson)
///|
fn transform_point_debug_string(point : Point2) -> String {
"(" + point.x.to_string() + ", " + point.y.to_string() + ")"
}
///|
/// Formats one transform operation in CamlPDF-compatible debug text.
pub fn TransformOp::debug_string(self : TransformOp) -> String {
match self {
Scale(origin, sx, sy) =>
"Scale about " +
transform_point_debug_string(origin) +
" by " +
sx.to_string() +
" in x and " +
sy.to_string() +
" in y\n"
Rotate(origin, angle) =>
"Rotate by " +
angle.to_string() +
" about " +
transform_point_debug_string(origin) +
"\n"
Translate(dx, dy) =>
"Translate by " + dx.to_string() + ", " + dy.to_string() + "\n"
ShearX(origin, factor) =>
"Shear in X about " +
transform_point_debug_string(origin) +
", proportionality constant " +
factor.to_string() +
"\n"
ShearY(origin, factor) =>
"Shear in Y about " +
transform_point_debug_string(origin) +
", proportionality constant " +
factor.to_string() +
"\n"
}
}
///|
/// Formats one transform operation in CamlPDF-compatible debug text.
pub fn transform_string_of_op(op : TransformOp) -> String {
op.debug_string()
}
///|
/// Formats a transform operation list in display order.
///
/// CamlPDF prints the most recently composed operation first, so this walks the
/// input list in reverse.
pub fn transform_string_of_ops(ops : ArrayView[TransformOp]) -> String {
let mut text = ""
for index in 0.. String {
transform_string_of_ops(ops)
}
///|
/// Formats a matrix as its six comma-separated operands.
pub fn TransformMatrix::debug_string(self : TransformMatrix) -> String {
self.a.to_string() +
", " +
self.b.to_string() +
", " +
self.c.to_string() +
", " +
self.d.to_string() +
", " +
self.e.to_string() +
", " +
self.f.to_string()
}
///|
/// Formats a matrix as its six comma-separated operands.
pub fn transform_string_of_matrix(matrix : TransformMatrix) -> String {
matrix.debug_string()
}
///|
/// Creates a two-dimensional point.
pub fn point2(x : Double, y : Double) -> Point2 {
{ x, y }
}
///|
/// The identity affine transform matrix.
pub let transform_identity_matrix : TransformMatrix = {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
}
///|
/// Returns an empty transform operation list, representing identity.
pub fn transform_identity() -> Array[TransformOp] {
[]
}
///|
/// Creates a translation matrix.
pub fn transform_translate(tx : Double, ty : Double) -> TransformMatrix {
{ ..transform_identity_matrix, e: tx, f: ty }
}
///|
/// Composes two matrices so `before` is applied first and `after` second.
pub fn transform_matrix_compose(
after : TransformMatrix,
before : TransformMatrix,
) -> TransformMatrix {
{
a: after.a * before.a + after.c * before.b,
c: after.a * before.c + after.c * before.d,
e: after.a * before.e + after.c * before.f + after.e,
b: after.b * before.a + after.d * before.b,
d: after.b * before.c + after.d * before.d,
f: after.b * before.e + after.d * before.f + after.f,
}
}
///|
/// Creates a scale matrix around `origin`.
pub fn transform_scale(
origin : Point2,
sx : Double,
sy : Double,
) -> TransformMatrix {
let translate = transform_translate(-origin.x, -origin.y)
let translate_back = transform_translate(origin.x, origin.y)
transform_matrix_compose(
translate_back,
transform_matrix_compose(
{ ..transform_identity_matrix, a: sx, d: sy },
translate,
),
)
}
///|
/// Creates a rotation matrix around `origin`.
///
/// `angle` is measured in radians.
pub fn transform_rotate(origin : Point2, angle : Double) -> TransformMatrix {
let translate = transform_translate(-origin.x, -origin.y)
let translate_back = transform_translate(origin.x, origin.y)
transform_matrix_compose(
translate_back,
transform_matrix_compose(
{
..transform_identity_matrix,
a: @math.cos(angle),
c: -@math.sin(angle),
b: @math.sin(angle),
d: @math.cos(angle),
},
translate,
),
)
}
///|
/// Creates an X-shear matrix around `origin`.
pub fn transform_shear_x(origin : Point2, factor : Double) -> TransformMatrix {
let translate = transform_translate(-origin.x, -origin.y)
let translate_back = transform_translate(origin.x, origin.y)
transform_matrix_compose(
translate_back,
transform_matrix_compose(
{ ..transform_identity_matrix, c: factor },
translate,
),
)
}
///|
/// Creates a Y-shear matrix around `origin`.
pub fn transform_shear_y(origin : Point2, factor : Double) -> TransformMatrix {
let translate = transform_translate(-origin.x, -origin.y)
let translate_back = transform_translate(origin.x, origin.y)
transform_matrix_compose(
translate_back,
transform_matrix_compose(
{ ..transform_identity_matrix, b: factor },
translate,
),
)
}
///|
fn transform_ops_with_head(
head : TransformOp,
tail : ArrayView[TransformOp],
) -> Array[TransformOp] {
let output = [head]
for item in tail {
output.push(item)
}
output
}
///|
/// Adds one operation to the front of an operation list, merging with the first
/// operation when the two adjacent operations are compatible.
///
/// Compatible adjacent translations, scales about the same origin, rotations
/// about the same origin, and shears about the same origin are combined to keep
/// operation lists compact.
pub fn transform_compose_op(
op : TransformOp,
ops : ArrayView[TransformOp],
) -> Array[TransformOp] {
if ops.length() == 0 {
[op]
} else {
let head = ops[0]
let rest = ops[1:]
match (head, op) {
(Translate(dx, dy), Translate(dx2, dy2)) =>
transform_ops_with_head(Translate(dx + dx2, dy + dy2), rest)
(Scale(origin, sx, sy), Scale(origin2, sx2, sy2)) if origin == origin2 =>
transform_ops_with_head(Scale(origin, sx * sx2, sy * sy2), rest)
(Rotate(origin, angle), Rotate(origin2, angle2)) if origin == origin2 =>
transform_ops_with_head(Rotate(origin, angle + angle2), rest)
(ShearX(origin, factor), ShearX(origin2, factor2)) if origin == origin2 =>
transform_ops_with_head(ShearX(origin, factor + factor2), rest)
(ShearY(origin, factor), ShearY(origin2, factor2)) if origin == origin2 =>
transform_ops_with_head(ShearY(origin, factor + factor2), rest)
_ => transform_ops_with_head(op, ops)
}
}
}
///|
/// Concatenates two transform operation lists.
///
/// The `after` operations are placed before the `before` operations to preserve
/// the same ordering used by CamlPDF transform lists.
pub fn transform_append(
after : ArrayView[TransformOp],
before : ArrayView[TransformOp],
) -> Array[TransformOp] {
let output : Array[TransformOp] = []
for item in after {
output.push(item)
}
for item in before {
output.push(item)
}
output
}
///|
/// Applies this matrix to a point.
pub fn TransformMatrix::apply(self : TransformMatrix, point : Point2) -> Point2 {
{
x: point.x * self.a + point.y * self.c + self.e,
y: point.x * self.b + point.y * self.d + self.f,
}
}
///|
/// Applies a matrix to a point.
pub fn transform_apply_matrix(
matrix : TransformMatrix,
point : Point2,
) -> Point2 {
matrix.apply(point)
}
///|
/// Lowers one high-level transform operation to a matrix.
pub fn transform_matrix_of_op(op : TransformOp) -> TransformMatrix {
match op {
Scale(origin, sx, sy) => transform_scale(origin, sx, sy)
Rotate(origin, angle) => transform_rotate(origin, angle)
Translate(dx, dy) => transform_translate(dx, dy)
ShearX(origin, factor) => transform_shear_x(origin, factor)
ShearY(origin, factor) => transform_shear_y(origin, factor)
}
}
///|
/// Lowers an operation list to one composed matrix.
pub fn transform_matrix_of_ops(ops : ArrayView[TransformOp]) -> TransformMatrix {
let mut matrix = transform_identity_matrix
for op in ops {
matrix = transform_matrix_compose(matrix, transform_matrix_of_op(op))
}
matrix
}
///|
/// Compatibility alias for lowering a transform operation list to a matrix.
pub fn transform_matrix_of_transform(
ops : ArrayView[TransformOp],
) -> TransformMatrix {
transform_matrix_of_ops(ops)
}
///|
/// Applies a transform operation list to a point.
pub fn transform_apply_ops(
ops : ArrayView[TransformOp],
point : Point2,
) -> Point2 {
transform_matrix_of_ops(ops).apply(point)
}
///|
/// Compatibility alias for applying a transform operation list to a point.
pub fn transform_apply(ops : ArrayView[TransformOp], point : Point2) -> Point2 {
transform_apply_ops(ops, point)
}
///|
/// Returns the inverse of this matrix.
///
/// Raises `PdfError::MatrixNotInvertable` when the matrix determinant is zero
/// or too small to produce a finite inverse.
pub fn TransformMatrix::invert(
self : TransformMatrix,
) -> TransformMatrix raise PdfError {
let divisor = self.a * self.d - self.b * self.c
if divisor == 0.0 {
raise PdfError::MatrixNotInvertable
}
let scale = 1.0 / divisor
if scale == 0.0 {
raise PdfError::MatrixNotInvertable
}
{
a: scale * self.d,
b: scale * -self.b,
c: scale * -self.c,
d: scale * self.a,
e: scale * (self.c * self.f - self.d * self.e),
f: scale * (self.b * self.e - self.a * self.f),
}
}
///|
/// Returns the inverse of a matrix.
///
/// Raises `PdfError::MatrixNotInvertable` when the matrix cannot be inverted.
pub fn transform_matrix_invert(
matrix : TransformMatrix,
) -> TransformMatrix raise PdfError {
matrix.invert()
}
///|
fn transform_safe_float(value : Double) -> Double {
if value.is_nan() ||
value.is_inf() ||
value == 0.0 ||
value.abs() < 2.2250738585072014e-308 {
0.0
} else {
value
}
}
///|
/// Decomposes this matrix into scale, aspect, rotation, shear, and translation
/// components.
///
/// Non-finite and underflowed intermediate values are normalized to zero so the
/// result is stable for malformed or degenerate matrices.
pub fn TransformMatrix::decompose(
self : TransformMatrix,
) -> TransformDecomposition {
let determinant = self.a * self.d - self.c * self.b
let moda = (self.a * self.a + self.b * self.b).sqrt()
let modb = (self.c * self.c + self.d * self.d).sqrt()
let dot = self.a * self.c + self.b * self.d
let scale = determinant / moda
let aspect = if scale.abs() == 0.0 { 1.0 } else { moda / scale.abs() }
let rotation = @math.atan2(self.b, self.a)
let shear = if moda * modb == 0.0 {
0.0
} else {
@math.PI / 2.0 - @math.acos(dot / (moda * modb))
}
{
scale: transform_safe_float(scale),
aspect: transform_safe_float(aspect),
rotation: transform_safe_float(rotation),
shear: transform_safe_float(shear),
translate_x: transform_safe_float(self.e),
translate_y: transform_safe_float(self.f),
}
}
///|
/// Decomposes a matrix into scale, aspect, rotation, shear, and translation.
pub fn transform_decompose(matrix : TransformMatrix) -> TransformDecomposition {
matrix.decompose()
}
///|
/// Recomposes a matrix from decomposed transform components.
pub fn transform_recompose(parts : TransformDecomposition) -> TransformMatrix {
let scale_aspect_shear = {
..transform_identity_matrix,
a: parts.scale.abs() * parts.aspect,
c: parts.scale * @math.tan(parts.shear),
d: parts.scale,
}
transform_matrix_compose(
transform_translate(parts.translate_x, parts.translate_y),
transform_matrix_compose(
transform_rotate(point2(0.0, 0.0), parts.rotation),
scale_aspect_shear,
),
)
}
///|
/// Recomposes a matrix from individual decomposition fields.
pub fn transform_recompose_components(
scale : Double,
aspect : Double,
rotation : Double,
shear : Double,
translate_x : Double,
translate_y : Double,
) -> TransformMatrix {
transform_recompose({
scale,
aspect,
rotation,
shear,
translate_x,
translate_y,
})
}