initial commit
This commit is contained in:
+275
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user