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 }