initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+506
View File
@@ -0,0 +1,506 @@
package pdf
import "core:math"
// Patterns (ISO 32000-2, 8.7.3 and 8.7.4).
//
// PATTERN SPACE IS ANCHORED TO THE PAGE, NOT TO THE FILL
//
// A pattern's /Matrix maps pattern space to the DEFAULT coordinate space of
// the page -- or of the form XObject, Type 3 glyph or annotation appearance
// stream -- in which the pattern is a resource (8.7.3.1). It does NOT compose
// with the CTM that happens to be in force when the fill runs. So the mapping
// every procedure in this file builds is
//
// pattern space --(/Matrix)--> default space --(base CTM)--> device
//
// and `ctm` throughout this file means that base CTM: the one the interpreter
// started the content stream with, saved before the first `cm`, never
// `gs.ctm`.
//
// Two fills with the same pattern under different CTMs must therefore land on
// exactly the same lattice. A patterned rectangle drawn twice -- once plainly,
// once inside `q 2 0 0 2 30 40 cm ... Q` -- shows the same hatch, at the same
// phase, in the same place, and only the shape being filled moves. Composing
// with the live CTM instead is the classic pattern bug: the texture slides
// with whatever transform preceded the fill. It looks plausible in isolation
// and is obvious the moment two shapes with different CTMs share an edge, or
// the moment the page is rendered at a second zoom level.
//
// One consequence is worth stating because the interpreter has to act on it: a
// form XObject re-anchors pattern space. A pattern named from a form's
// /Resources is anchored to the form's default space -- the CTM at the moment
// the form was invoked, including the form's own /Matrix -- so entering a form
// means saving a new base CTM, and leaving it means restoring the old one.
// The same goes for a Type 3 glyph and an annotation appearance stream.
//
// WHAT THE INTERPRETER STILL HAS TO DO
//
// `pattern_tiles` says where to replay a tiling cell, not how. For each matrix
// it returns, the interpreter sets the CTM to that matrix, clips to the
// pattern's /BBox (a rotated pattern makes that a path, not a scissor
// rectangle), and runs `content` against `resources`. For an uncoloured
// pattern -- `pattern_needs_color` -- every colour operator inside the cell is
// ignored and the whole cell paints in the colour supplied with the pattern
// name. Recursion into a pattern that fills with a pattern is the
// interpreter's to bound.
Pattern_Kind :: enum u8 {
Tiling = 1,
Shading = 2,
}
Paint_Type :: enum u8 {
Coloured = 1, // the cell supplies its own colour
Uncoloured = 2, // the cell is a stencil painted in the caller's colour
}
Tiling_Type :: enum u8 {
Constant_Spacing = 1,
No_Distortion = 2,
Faster_Constant_Spacing = 3,
}
Pattern :: struct {
kind: Pattern_Kind,
// Pattern space to the default space of the page or form the pattern is
// used in -- NOT to the CTM in force when it is painted (8.7.3.1). Getting
// this wrong makes patterns slide around under a transformed fill.
mtx: Matrix,
// Tiling.
paint_type: Paint_Type,
tiling_type: Tiling_Type,
bbox: Rect,
xstep: f64,
ystep: f64,
resources: ^Dict,
content: ^Stream,
// Shading.
shading: ^Shading,
ext_gstate: ^Dict,
// The Document this was read from. A lattice can only be found to be
// pathological once the area to cover is known, which is inside
// `pattern_tiles`, and the repair that records it has to be attached to
// something.
doc: ^Document,
// Set once the tile cap has been reported for this pattern. A page that
// fills with the same hostile pattern five hundred times should cost one
// repair, not five hundred crowding every other one out of doc.repairs.
cap_reported: bool,
}
// ---------------------------------------------------------------------------
// Limits
//
// Each of these bounds a number that came out of a file, on a path any file
// can reach: `/Pattern cs /P1 scn ... f` is three operators.
// ---------------------------------------------------------------------------
// Cells one call may ask the interpreter to replay. A full page at 300 dpi is
// 2550 x 3300 device pixels, so even a 16-pixel cell covering all of it is
// about 33 000 -- this is twice that, and everything past it is a file whose
// /XStep is small enough to make one fill cost a million content streams.
@(private)
MAX_PATTERN_TILES :: 1 << 16
// How many lattice steps a cell may span before the step is treated as
// degenerate rather than as overlapping cells. Cells that overlap are a real
// design -- a brick bond with a bleed, a hatch drawn with a fat pen -- so a
// generous factor is allowed before the step is snapped back to the cell size.
@(private)
MAX_TILE_OVERLAP :: 64.0
// Bound on a lattice index before it is narrowed to an int. The quotients that
// produce the index range are computed from file-derived numbers and can be
// 1e300; converting that to an int is undefined, so the range is clamped in
// f64 first. 2^24 is exact in f64 and is already 16 million cells along one
// axis, far past what the tile cap will let through.
@(private)
MAX_TILE_INDEX :: 1 << 24
// ---------------------------------------------------------------------------
// Loading
// ---------------------------------------------------------------------------
// pattern_load builds the pattern `obj` describes: a stream for a tiling
// pattern (/PatternType 1), whose dictionary carries the pattern entries and
// whose bytes are the cell's content, or a dictionary for a shading pattern
// (/PatternType 2).
//
// `res` is the /Resources dictionary the pattern was named from. It resolves a
// colour space named by a bare name inside a shading pattern's /Shading, and
// it stands in for a tiling pattern that omits its own /Resources.
//
// The result is owned by the Document and must not be freed. Nothing is
// cached: like a shading, a pattern is built once per `scn` that names it, and
// the content stream it points at is decoded by `stream_data`, which caches.
pattern_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (^Pattern, Error) {
if doc == nil {
return nil, .Invalid_Argument
}
o := resolve(doc, obj)
d, has_dict := to_dict(o)
if !has_dict {
return nil, .Bad_Pattern
}
stm, is_stream := to_stream(o)
kind := dict_int(doc, d, "PatternType", 0)
if kind != 1 && kind != 2 {
// /PatternType is required, but a file that leaves it out still says
// plainly which kind it meant: only a tiling pattern is a stream, and
// only a shading pattern has a /Shading.
switch {
case dict_has(d, "Shading"): kind = 2
case is_stream: kind = 1
case: return nil, .Bad_Pattern
}
add_repair(doc, .Bad_Resource, 0)
}
p := Pattern{doc = doc}
// `object_to_matrix` is the finiteness boundary for a matrix out of a
// file, so a /Matrix that is short, non-numeric or infinite arrives here
// as the identity -- which is the format's own default and the only
// sensible guess.
p.mtx = dict_matrix(doc, d, "Matrix")
if kind == 2 {
p.kind = .Shading
sh, sh_err := shading_load(doc, dict_get(doc, d, "Shading"), res)
// A shading pattern is nothing but its shading; with no shading to
// paint there is no degraded version of it to hand back.
if sh_err != .None || sh == nil {
return nil, .Bad_Pattern
}
p.shading = sh
// /ExtGState is applied by the caller: it belongs to the graphics
// state the shading paints under, not to the shading itself, and it
// can carry a soft mask that only the interpreter can build.
p.ext_gstate = dict_dict(doc, d, "ExtGState")
return pattern_intern(doc, p), .None
}
p.kind = .Tiling
if !is_stream {
// The cell's content is the stream's own bytes. A tiling pattern
// written as a bare dictionary has no content at all, and an empty
// cell tiled across a fill is indistinguishable from not filling.
return nil, .Bad_Pattern
}
p.content = stm
bbox, bbox_ok := object_to_rect(doc, dict_get(doc, d, "BBox"))
// Both corners are finite by the time `object_to_rect` returns, but their
// difference need not be: a /BBox of [-1e308 -1e308 1e308 1e308] has an
// infinite width, and a step of infinity multiplied by a lattice index of
// zero is a NaN in the middle of a cell matrix. Nothing below the Device
// line may see one, and a cell the size of the number line is not a cell.
bw := rect_width(bbox)
bh := rect_height(bbox)
if !bbox_ok || rect_is_empty(bbox) || !is_finite(bw) || !is_finite(bh) {
// 8.7.3.1: the cell is clipped to /BBox, so a missing or degenerate
// one clips every cell away to nothing.
return nil, .Bad_Pattern
}
p.bbox = bbox
// After this the steps are finite and strictly positive, which is the
// invariant `pattern_bounds` and `pattern_tiles` are written against.
p.xstep = pattern_step(doc, dict_num(doc, d, "XStep"), bw)
p.ystep = pattern_step(doc, dict_num(doc, d, "YStep"), bh)
// Both are required entries, and an out-of-range value is a broken file
// rather than a new feature; the defaults are what a viewer shows.
p.paint_type = dict_int(doc, d, "PaintType", 1) == 2 ? .Uncoloured : .Coloured
switch dict_int(doc, d, "TilingType", 1) {
case 2: p.tiling_type = .No_Distortion
case 3: p.tiling_type = .Faster_Constant_Spacing
case: p.tiling_type = .Constant_Spacing
}
p.resources = dict_dict(doc, d, "Resources")
if p.resources == nil {
// /Resources is required (8.7.3.3), but a cell that only draws
// rectangles needs nothing out of it, so its absence is not on its own
// a defect worth recording. Falling back to the resources of the
// stream that named the pattern is what every viewer does, and it is
// the only other dictionary in scope; a name that still does not
// resolve is reported by the interpreter that looks it up.
p.resources = res
}
return pattern_intern(doc, p), .None
}
// pattern_intern copies a finished pattern into the Document arena. Patterns
// are built on the stack first so that a file that fails half way through
// validation costs no arena at all -- an arena is never reclaimed before
// `close`, and a page can name a broken pattern in every one of its fills.
@(private)
pattern_intern :: proc(doc: ^Document, p: Pattern) -> ^Pattern {
out := new(Pattern, doc.allocator)
out^ = p
return out
}
// pattern_step normalises /XStep or /YStep against the size of the cell.
//
// Only the magnitude spaces the lattice, so a negative step is read as its
// absolute value rather than rejected. Zero is forbidden by the format and
// occurs anyway; it would stack every cell of an infinite row at one place, so
// it falls back to the cell size, which is what the step means when cells
// abut.
//
// A step far smaller than the cell is the same denial of service written
// differently: at a thousandth of the cell width every device pixel is covered
// by a thousand cells, and one fill costs a thousand times what it looks like.
// Overlapping cells are a legitimate design, so an overlap of up to
// MAX_TILE_OVERLAP-fold is honoured before the step is snapped to the cell.
@(private)
pattern_step :: proc(doc: ^Document, step, cell: f64) -> f64 {
s := abs(step)
if !is_finite(s) || s <= 0 || s * MAX_TILE_OVERLAP < cell {
add_repair(doc, .Bad_Resource, 0)
return cell
}
return s
}
// ---------------------------------------------------------------------------
// The one composition every caller needs
// ---------------------------------------------------------------------------
// pattern_matrix maps the pattern's own space to device space.
//
// `ctm` is the DEFAULT-space CTM of the page or form the pattern is a resource
// of, not the CTM of the fill being painted -- see the note at the top of this
// file. This is the only place the two are composed, so an interpreter that
// routes everything through here cannot get 8.7.3.1 wrong in one operator and
// right in another.
@(private)
pattern_matrix :: proc(p: ^Pattern, ctm: Matrix) -> Matrix {
if p == nil {
return ctm
}
return matrix_mul(p.mtx, ctm)
}
// pattern_needs_color reports whether the caller must supply the colour: an
// uncoloured tiling pattern (/PaintType 2) is a stencil, and every colour
// operator inside its cell is ignored in favour of the components given
// alongside the pattern name in `scn` (8.7.3.1). Those components live in the
// underlying colour space of the /Pattern space, which is what
// `Colorspace.under` holds.
@(private)
pattern_needs_color :: proc(p: ^Pattern) -> bool {
return p != nil && p.kind == .Tiling && p.paint_type == .Uncoloured
}
// ---------------------------------------------------------------------------
// Bounds
// ---------------------------------------------------------------------------
// pattern_bounds is the device-space region the pattern can paint under the
// default-space `ctm`.
//
// A tiling pattern repeats over the whole plane, so the honest answer is
// RECT_INFINITE, the identity for `rect_intersect`: a caller that intersects
// it with the clip gets the clip. A shading pattern is bounded exactly when
// its shading is -- an un-extended radial one is, an axial one is not.
//
// A transform that collapses the plane to a line or a point paints nothing at
// all, and that is RECT_EMPTY rather than RECT_INFINITE, because the two are
// opposite identities and a caller intersecting with the wrong one would fill
// the whole clip with a pattern that has no area.
pattern_bounds :: proc(p: ^Pattern, ctm: Matrix) -> Rect {
if p == nil || !matrix_is_finite(ctm) {
return RECT_EMPTY
}
m := pattern_matrix(p, ctm)
if !matrix_is_finite(m) {
return RECT_EMPTY
}
if _, ok := matrix_invert(m); !ok {
return RECT_EMPTY
}
switch p.kind {
case .Shading:
// `shading_bounds` answers RECT_EMPTY for a nil shading, which is the
// right answer and is not worth a second guard here.
return shading_bounds(p.shading, m)
case .Tiling:
// The lattice is infinite in both directions: /XStep and /YStep are
// finite and non-zero after loading, so there is always another cell.
return RECT_INFINITE
}
return RECT_EMPTY
}
// ---------------------------------------------------------------------------
// Tiling
// ---------------------------------------------------------------------------
// pattern_tiles returns the matrices at which the interpreter must replay a
// tiling pattern's content stream to cover `area`.
//
// Each matrix maps the cell's own space -- the space /BBox is written in -- to
// device space, and already carries the pattern matrix and `ctm`, so the
// interpreter sets the CTM to it, clips to /BBox, and runs the content. Cell
// (i, j) of the lattice is the cell translated by (i * /XStep, j * /YStep) in
// pattern space, so the returned matrices differ only in that translation and
// share the pattern's rotation and scale exactly; the cells cannot drift apart
// at the far end of a long fill.
//
// area the device-space region to cover, normally the fill's bounding box
// intersected with the clip. It must be finite: RECT_INFINITE, an
// empty rectangle and a NaN one all mean nothing to draw, because
// "cover an unbounded area with a repeating cell" has no answer.
// ctm the DEFAULT-space CTM of the page or form -- see the top of this
// file. It is emphatically not the CTM of the fill.
//
// An empty slice means nothing to draw, which is also what a shading pattern
// always gets: it paints in one pass rather than as cells. The result is
// allocated with `allocator` and belongs to the caller.
//
// A cell whose /BBox merely touches the edge of `area` is included. It is
// clipped away to a hairline at worst, and being inclusive is what keeps the
// common case -- a lattice exactly aligned with the area, where the quotients
// land on integers -- from dropping a whole row to a rounding error.
pattern_tiles :: proc(p: ^Pattern, area: Rect, ctm: Matrix,
allocator := context.allocator) -> []Matrix {
if p == nil || p.kind != .Tiling || p.content == nil {
return nil
}
// RECT_INFINITE has to be caught by identity, not by is_finite: its
// corners are 1e30, which is a perfectly finite number, and
// `transform_rect` passes it through unchanged.
if area == RECT_INFINITE || !rect_is_finite(area) || rect_is_empty(area) {
return nil
}
if !matrix_is_finite(ctm) || p.xstep <= 0 || p.ystep <= 0 {
return nil
}
m := pattern_matrix(p, ctm)
if !matrix_is_finite(m) {
return nil
}
inv, invertible := matrix_invert(m)
if !invertible {
// A singular pattern matrix collapses every cell to zero area.
return nil
}
// The lattice is regular in pattern space and only there, so the area is
// taken back into pattern space and the indices are read off directly. For
// a rotated or skewed pattern the pulled-back rectangle is the bounding
// box of a rotated one, so a few cells in the corners fall outside the
// area and are clipped away; that costs a little work and never misses a
// cell, which is the right way round.
pa := transform_rect(inv, area)
if !rect_is_finite(pa) {
return nil
}
// Cell (i, j) spans [bbox.x0 + i*xs, bbox.x1 + i*xs] across, so it meets
// the area when bbox.x0 + i*xs <= pa.x1 and bbox.x1 + i*xs >= pa.x0.
i0, i1, ok_x := tile_range(pa.x0, pa.x1, p.bbox.x0, p.bbox.x1, p.xstep)
j0, j1, ok_y := tile_range(pa.y0, pa.y1, p.bbox.y0, p.bbox.y1, p.ystep)
if !ok_x || !ok_y {
return nil
}
nx := i1 - i0 + 1
ny := j1 - j0 + 1
if nx <= 0 || ny <= 0 {
return nil
}
// In f64 because the product of two clamped index counts overflows no f64
// but would need care in an int.
total := f64(nx) * f64(ny)
n := nx * ny
if total > f64(MAX_PATTERN_TILES) {
// Painting fewer cells than were asked for leaves part of the fill
// bare, which is the least bad of the three options: the whole fill
// dropped, a lattice respaced to fit (which would show gaps at the
// wrong pitch and look like a rendering bug rather than a damaged
// file), or an unbounded amount of work. Row-major truncation keeps
// the painted part contiguous.
n = MAX_PATTERN_TILES
if !p.cap_reported {
p.cap_reported = true
add_repair(p.doc, .Bad_Resource, 0)
}
}
out, alloc_err := make([]Matrix, n, allocator)
if alloc_err != nil {
return nil
}
k := 0
rows: for j := j0; j <= j1; j += 1 {
ty := f64(j) * p.ystep
for i := i0; i <= i1; i += 1 {
if k >= n {
break rows
}
out[k] = matrix_mul(translate_matrix(f64(i) * p.xstep, ty), m)
k += 1
}
}
return out
}
// tile_range is the inclusive range of lattice indices whose cell meets
// [lo, hi], for a cell spanning [c0, c1] and a step of `step`.
//
// The quotients are formed from numbers that came out of a file and can be
// enormous, so they are clamped while they are still f64: `int(1e300)` is
// undefined, and the clamp is what makes the tile cap the only thing standing
// between a hostile file and an allocation.
@(private)
tile_range :: proc(lo, hi, c0, c1, step: f64) -> (from, to: int, ok: bool) {
if step <= 0 || !is_finite(step) {
return 0, 0, false
}
a := math.ceil((lo - c1) / step)
b := math.floor((hi - c0) / step)
if !is_finite(a) || !is_finite(b) {
return 0, 0, false
}
a = clamp(a, -MAX_TILE_INDEX, MAX_TILE_INDEX)
b = clamp(b, -MAX_TILE_INDEX, MAX_TILE_INDEX)
if b < a {
return 0, 0, false
}
return int(a), int(b), true
}
// ---------------------------------------------------------------------------
// Painting a shading pattern
// ---------------------------------------------------------------------------
// pattern_paint_shading composites a shading pattern's shading onto `bmp`
// through `clip`, which the caller has already narrowed to the shape being
// filled.
//
// This exists so that the pattern matrix is composed in exactly one place:
// everything else about the call is `shading_paint`'s, including that `bmp` is
// premultiplied and that `mask` is an optional soft mask aligned with it in
// device pixels.
//
// `ctm` is the default-space CTM, not the fill's. The pattern's /ExtGState is
// the caller's to apply -- `alpha`, `mode` and `mask` are where its effect
// arrives here.
@(private)
pattern_paint_shading :: proc(doc: ^Document, p: ^Pattern, bmp: ^Bitmap, ctm: Matrix,
clip: Raster_Clip, alpha: f64, mode: Blend_Mode,
mask: ^Bitmap) -> Error {
if p == nil || p.kind != .Shading || p.shading == nil {
return .Invalid_Argument
}
return shading_paint(doc, p.shading, bmp, pattern_matrix(p, ctm), clip, alpha, mode, mask)
}