PeachDrawing.Core

PeachDrawing.Core

ColorMatrix Struct

The same shape as SVG’s feColorMatrix type="matrix" (SVG Filter Effects §15.17) and CSS Filter Effects’ color-altering filter functions (grayscale(), sepia(), saturate(), hue-rotate(), invert(), brightness(), contrast()): an affine map [R' G' B' A'] = Linear · [R G B A] + Offset over premultiplied-free RGBA components in [0, 1].

public readonly struct ColorMatrix

Remarks

Why this is a math-only type with no PDF-writing method on it. The obvious next step - “apply this to whatever a Form XObject tile already painted” - does not have a general, spec-conformant PDF answer, and that finding shapes how a PDF Type 4 function and Canvas.DrawImageWithColorMatrix are actually built. Two real PDF mechanisms exist that are shaped like “run every color through a function”, and neither does what a first read of “just make a Type 4 function and hang it off the graphics state” suggests:

Net effect: DrawImageWithColorMatrix is spec-correct and real for the channel-independent subset (via /TR), and throws rather than silently mis-rendering for the cross-channel subset, instead of emitting a plausible-looking but non-conformant PDF construct.

Constructors  
ColorMatrix(Matrix4x4, Vector4) Creates a color matrix from its linear part and constant offset.
Fields  
Identity The identity color matrix: output equals input, unchanged.
Properties  
IsChannelIndependent Whether every output channel among R, G, B depends on only its own matching input channel (i.e. Linear has no off-diagonal coupling among the R/G/B/A inputs feeding the R/G/B outputs) - the exact condition under which this matrix is representable as a PDF ExtGState /TR transfer function (see the type-level remarks). Input alpha’s contribution to each of the R/G/B outputs is included in the check: a filter whose R/G/B output depends on input alpha could not be expressed as a transfer function either, since /TR only ever sees one color component’s own value, never alpha. The matrix’s own alpha OUTPUT (column 4 - System.Numerics.Matrix4x4.M14/System.Numerics.Matrix4x4.M24/ System.Numerics.Matrix4x4.M34/System.Numerics.Matrix4x4.M44) is not checked - /TR is a color-space transfer function and is never applied to alpha, so nothing about how this matrix would compute alpha bears on whether it fits. See Linear’s remarks for why the row is the input component and the column is the output component here.
Linear The linear (4x4) part of the affine map. Apply(Vector4) and Compose(ColorMatrix) both go through System.Numerics.Vector4.Transform(System.Numerics.Vector4,System.Numerics.Matrix4x4), which treats a color as a row vector (result = color * Linear): per its documented implementation, output component c is sum over r of color[r] * Linear[r, c]. That makes each field’s ROW (the first index in Mrowcol) the INPUT component and its COLUMN the OUTPUT component - e.g. Linear.M21 (row 2 = input G, col 1 = output R) is how much input G contributes to output R. This is the transpose of feColorMatrix’s own row-major table (SVG Filter Effects §15.17, where row = output); a caller building a ColorMatrix from such a table must transpose it first.
Offset The constant term added after Linear is applied, one component per output channel (R, G, B, A).
Methods  
Apply(Vector4) Applies this matrix to a single RGBA color: Linear * color + Offset. Used by tests and by any future caller that needs the transformed value of one already-known, static color (e.g. a solid text/fill color) - not by DrawImageWithColorMatrix, which composites against arbitrary already-painted tile content rather than a single known input color.
Compose(ColorMatrix) Composes this matrix with appliedAfterThis, producing the single matrix equivalent to applying this first and then appliedAfterThis to the result - i.e. result.Apply(c) == appliedAfterThis.Apply(this.Apply(c)) for every color c. Affine composition: the linear parts multiply (order matters - the second transform’s matrix goes on the left), and the first transform’s offset is carried through the second transform’s linear part before the second transform’s own offset is added.