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
+275
View File
@@ -0,0 +1,275 @@
package pdf
// A Device is the sink the content-stream interpreter draws into.
//
// Everything above this line is PDF knowledge -- resolving resources, tracking
// graphics state, mapping character codes to glyphs. Everything below is
// output. Three devices ship with the library: `device_raster` produces a
// Bitmap, `device_text` produces extracted text, and `device_list` records a
// display list a caller can replay on its own GPU backend. They share one
// interpreter, which is why the text a page extracts to always agrees with the
// text it draws.
//
// Every hook may be nil. The interpreter checks before calling, so a device
// only implements what it cares about -- the text device leaves every path and
// image hook nil.
// Soft_Mask is a /SMask from an ExtGState: a form XObject rendered to a mask
// that modulates everything painted while it is in force (ISO 32000-2, 11.6.5).
//
// It is built in the interpreter's SCRATCH and does not outlive the run, like
// the `Path` and `Text` handed to the hooks below. A device that retains one
// past the call it arrived on must copy it by value -- `device_list` does, and
// says so. Two Soft_Masks with equal contents are the same mask: everything a
// render of one depends on is in here, which is what lets a device that caches
// rendered planes recognise the same mask installed twice.
Soft_Mask :: struct {
form: ^Stream,
luminosity: bool,
// Backdrop colour for a luminosity mask, in the group's colour space.
backdrop: Color,
transfer: ^Function,
// The CTM in force when the ExtGState installed the mask. The mask is
// positioned by this, not by the CTM at the time of the paint operation.
ctm: Matrix,
}
// Text_Item is one positioned glyph.
Text_Item :: struct {
// Glyph index into the font program. 0 is .notdef.
gid: u32,
// The character code taken from the string, and the CID it mapped to.
// For a simple font these are equal.
code: u32,
cid: u32,
// Glyph space to device space, complete: it already folds in the text
// matrix, the font size, the horizontal scale, the rise and the CTM.
// Carrying the matrix rather than a point keeps rotated and skewed text
// correct without every device re-deriving it.
trm: Matrix,
// Horizontal displacement this glyph contributes, in unscaled text space.
advance: f64,
// Unicode for extraction, from /ToUnicode or the encoding. Negative when
// the font offers none.
unicode: rune,
// The code was a single byte 32, which is what word spacing keys off and
// what text extraction treats as a word break.
is_space: bool,
}
// Text is the glyph run produced by one text-showing operator.
Text :: struct {
font: ^Font,
// 0 horizontal, 1 vertical (a Type0 font with a vertical CMap).
wmode: int,
render_mode: Text_Render_Mode,
items: [dynamic]Text_Item,
}
text_make :: proc(allocator := context.allocator) -> Text {
return Text{items = make([dynamic]Text_Item, 0, 32, allocator)}
}
text_destroy :: proc(t: ^Text) {
if t == nil { return }
delete(t.items)
t^ = {}
}
// Device_Hint lets a device tell the interpreter what it can skip. Decoding a
// 4000x4000 JPEG to extract text from the page it sits on is pure waste.
Device_Hint :: enum u8 {
Ignore_Image,
Ignore_Shade,
// Text is wanted as glyph indices and unicode only; do not load outlines.
No_Glyph_Outlines,
// Do not execute tiling patterns; paint their average colour instead.
No_Tiling,
}
Device_Hints :: bit_set[Device_Hint; u8]
Device :: struct {
// Backend state. The interpreter never looks at it.
user: rawptr,
hints: Device_Hints,
// -- paths ------------------------------------------------------------
// `path` is in user space; `ctm` maps it to device space. Devices must not
// retain the path -- it is reused by the interpreter after the call.
fill_path: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint),
stroke_path: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint),
// -- clipping ---------------------------------------------------------
// Every clip_* pushes one entry; `pop_clip` removes one. The interpreter
// guarantees they balance, including on the error paths.
clip_path: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix),
clip_stroke_path: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix),
clip_text: proc(dev: ^Device, text: ^Text, ctm: Matrix),
clip_image_mask: proc(dev: ^Device, img: ^Image, ctm: Matrix),
pop_clip: proc(dev: ^Device),
// -- text -------------------------------------------------------------
fill_text: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint),
stroke_text: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint),
// Render mode 3 (invisible) and 7 (clip-only). Nothing is painted, but the
// text is still there to be extracted -- which is exactly how a scanned
// page with an OCR layer is built.
ignore_text: proc(dev: ^Device, text: ^Text, ctm: Matrix),
// -- images and shadings ----------------------------------------------
fill_image: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint),
// A 1-bit stencil mask painted in the current fill colour.
fill_image_mask: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint),
fill_shade: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint),
// -- transparency -----------------------------------------------------
// begin_mask/end_mask bracket the rendering of a soft mask's form; what is
// drawn between them becomes the mask, not page content.
//
// `end_mask` is told WHICH Soft_Mask it has just drawn. A device that
// caches rendered planes needs that: it is the same pointer that later
// arrives on `Paint.soft_mask`, and without it the owner of a plane can
// only be guessed from whatever paints next.
begin_mask: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint),
end_mask: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function),
// 11.6.6: the outer alpha, blend mode AND soft mask apply to the group's
// RESULT, not to the operations inside it, so all three are handed over
// here -- the interpreter has cleared them for the group's contents and
// this is the only channel left for them.
begin_group: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask),
end_group: proc(dev: ^Device),
// Reports that the device already holds a rendered plane for a mask equal
// to this one, so the interpreter may skip re-running its form. Rendering
// a mask is a whole form XObject rasterised into a plane the size of its
// BBox, and an ExtGState naming an /SMask is commonly applied to run after
// run of content -- once per `gs`, thousands of times on a page. A device
// that caches nothing leaves this nil and every install renders, which is
// what `device_list` does: a recorded list has to carry the mask's drawing
// for whoever replays it.
has_soft_mask: proc(dev: ^Device, mask: ^Soft_Mask) -> bool,
// -- tiling patterns --------------------------------------------------
// Returns the number of times the interpreter should replay the tile's
// content stream: 0 means the device rendered the tile itself and wants
// the content run once, into the tile. A device that cannot tile returns
// a count and lets the interpreter place each copy.
begin_tile: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int,
end_tile: proc(dev: ^Device),
// -- optional content (layers) ----------------------------------------
begin_layer: proc(dev: ^Device, name: string),
end_layer: proc(dev: ^Device),
// Called once when the page is finished, before the device is torn down.
close: proc(dev: ^Device),
}
// ---------------------------------------------------------------------------
// Safe invocation
//
// The interpreter calls these rather than the fields directly, so a device
// that leaves a hook nil costs one predictable branch instead of a crash.
// ---------------------------------------------------------------------------
dev_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) {
if dev != nil && dev.fill_path != nil { dev.fill_path(dev, path, even_odd, ctm, paint) }
}
dev_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
if dev != nil && dev.stroke_path != nil { dev.stroke_path(dev, path, stroke, ctm, paint) }
}
dev_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) {
if dev != nil && dev.clip_path != nil { dev.clip_path(dev, path, even_odd, ctm) }
}
dev_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) {
if dev != nil && dev.clip_stroke_path != nil { dev.clip_stroke_path(dev, path, stroke, ctm) }
}
dev_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) {
if dev != nil && dev.clip_text != nil { dev.clip_text(dev, text, ctm) }
}
dev_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) {
if dev != nil && dev.clip_image_mask != nil { dev.clip_image_mask(dev, img, ctm) }
}
dev_pop_clip :: proc(dev: ^Device) {
if dev != nil && dev.pop_clip != nil { dev.pop_clip(dev) }
}
dev_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) {
if dev != nil && dev.fill_text != nil { dev.fill_text(dev, text, ctm, paint) }
}
dev_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) {
if dev != nil && dev.stroke_text != nil { dev.stroke_text(dev, text, stroke, ctm, paint) }
}
dev_ignore_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) {
if dev != nil && dev.ignore_text != nil { dev.ignore_text(dev, text, ctm) }
}
dev_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
if dev != nil && dev.fill_image != nil { dev.fill_image(dev, img, ctm, paint) }
}
dev_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) {
if dev != nil && dev.fill_image_mask != nil { dev.fill_image_mask(dev, img, ctm, paint) }
}
dev_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) {
if dev != nil && dev.fill_shade != nil { dev.fill_shade(dev, shade, ctm, paint) }
}
dev_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) {
if dev != nil && dev.begin_mask != nil { dev.begin_mask(dev, area, luminosity, backdrop) }
}
dev_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) {
if dev != nil && dev.end_mask != nil { dev.end_mask(dev, mask, transfer) }
}
dev_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) {
if dev != nil && dev.begin_group != nil { dev.begin_group(dev, area, isolated, knockout, blend, alpha, soft_mask) }
}
dev_end_group :: proc(dev: ^Device) {
if dev != nil && dev.end_group != nil { dev.end_group(dev) }
}
dev_has_soft_mask :: proc(dev: ^Device, mask: ^Soft_Mask) -> bool {
if dev != nil && dev.has_soft_mask != nil { return dev.has_soft_mask(dev, mask) }
return false
}
dev_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int {
if dev != nil && dev.begin_tile != nil { return dev.begin_tile(dev, area, view, xstep, ystep, ctm) }
return 0
}
dev_end_tile :: proc(dev: ^Device) {
if dev != nil && dev.end_tile != nil { dev.end_tile(dev) }
}
dev_begin_layer :: proc(dev: ^Device, name: string) {
if dev != nil && dev.begin_layer != nil { dev.begin_layer(dev, name) }
}
dev_end_layer :: proc(dev: ^Device) {
if dev != nil && dev.end_layer != nil { dev.end_layer(dev) }
}
dev_close :: proc(dev: ^Device) {
if dev != nil && dev.close != nil { dev.close(dev) }
}
dev_wants_images :: proc(dev: ^Device) -> bool {
return dev != nil && .Ignore_Image not_in dev.hints
}
dev_wants_shades :: proc(dev: ^Device) -> bool {
return dev != nil && .Ignore_Shade not_in dev.hints
}