276 lines
12 KiB
Odin
276 lines
12 KiB
Odin
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
|
|
}
|