8.0 KiB
Frozen types for the rendering layer
These are shared across many files, so they are fixed here rather than invented by whoever writes the first one. Add fields if you need them; do not rename or restructure without saying so.
Rasteriser (raster.odin)
Rasterising is separated from compositing by a span callback, so the same code fills a bitmap, builds a clip mask, and feeds a display list.
// Coverage for one horizontal run on scanline y, starting at pixel x0.
// Values are 0..255. The slice is only valid for the duration of the call.
Span_Proc :: #type proc(user: rawptr, y: int, x0: int, coverage: []u8)
Raster_Clip :: struct { x0, y0, x1, y1: int } // device pixels, half-open
raster_fill :: proc(fp: ^Flat_Path,
even_odd: bool,
clip: Raster_Clip,
span: Span_Proc,
user: rawptr,
allocator := context.allocator) -> Error
Requirements: exact analytic coverage horizontally; at least 15 sub-scanlines
vertically (or an equivalent cell/area accumulator). Both fill rules. Never
allocate per scanline. Bounded by clip — a path 10^9 units tall must cost the
same as one that fills the clip. Degenerate input (no points, one point, all
points collinear, NaN already filtered by path.odin) must produce no spans
rather than misbehaving.
Stroking (stroke.odin)
// Converts a stroke into a fill. Output is a Flat_Path in DEVICE space,
// already flattened, to be filled with the nonzero rule.
stroke_flatten :: proc(out: ^Flat_Path,
path: ^Path,
state: ^Stroke_State,
ctm: Matrix,
flatness := FLATNESS) -> Error
A zero or sub-pixel line width must still draw a hairline exactly one pixel
wide — that is what 0 w means (ISO 32000-2, 8.4.3.2) and thin table rules are
everywhere. Dashes are applied along the flattened path in user space, then
transformed. Caps apply to the ends of each dash. A degenerate subpath (a single
point) draws a dot under round or square caps and nothing under butt caps.
Colour (colorspace.odin)
Colorspace_Kind :: enum {
Device_Gray, Device_RGB, Device_CMYK,
Cal_Gray, Cal_RGB, Lab, ICC_Based,
Indexed, Separation, Device_N, Pattern,
}
Colorspace :: struct {
kind: Colorspace_Kind,
n: int, // components a colour value in this space has
name: string,
base: ^Colorspace, // Indexed
lookup: []byte,
hival: int,
alt: ^Colorspace, // Separation / DeviceN / ICCBased
tint: ^Function,
colorant_names: []string,
is_none: bool, // /None paints nothing at all
is_all: bool,
white_point: [3]f64, // CalGray / CalRGB / Lab
black_point: [3]f64,
gamma: [3]f64,
mtx: [9]f64,
range: [4]f64,
under: ^Colorspace, // Pattern's underlying space
}
colorspace_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (^Colorspace, Error)
colorspace_to_rgb :: proc(cs: ^Colorspace, c: Color) -> [3]f64
colorspace_default_color :: proc(cs: ^Colorspace) -> Color
colorspace_default_decode :: proc(cs: ^Colorspace, bpc: int, allocator := context.allocator) -> []f64
colorspace_device_gray :: proc(doc: ^Document) -> ^Colorspace // cached singletons
colorspace_device_rgb :: proc(doc: ^Document) -> ^Colorspace
colorspace_device_cmyk :: proc(doc: ^Document) -> ^Colorspace
ICCBased is rendered through its /Alternate, or through the device space
implied by /N when there is none. That is not colour management and is not
pretending to be; it is what every non-colour-managed viewer does. Loaded spaces
are cached on the Document keyed by the object they came from, because a page
that names the same space in 400 XObjects must not build it 400 times.
Functions (function.odin)
// `kind` is an enum whose values ARE the format's type numbers, so a switch on
// it is exhaustive and `FunctionType 1` cannot be smuggled through as an int.
Function_Kind :: enum u8 { Sampled = 0, Exponential = 2, Stitching = 3, PostScript = 4 }
Function :: struct {
kind: Function_Kind,
domain: []f64,
range: []f64, // required for 0 and 4
n_in, n_out: int,
...
}
function_load :: proc(doc: ^Document, obj: Object) -> (^Function, Error)
function_eval :: proc(fn: ^Function, in_: []f64, out: []f64)
Type 4 is a real PostScript calculator: implement the whole operator set with a
bounded operand stack and a bounded execution step count. Inputs are clamped to
/Domain and outputs to /Range before returning — the format requires it and
a shading with an out-of-range tint otherwise paints garbage.
Images (image.odin)
Image :: struct {
width, height: int,
bpc: int,
cs: ^Colorspace,
is_mask: bool, // /ImageMask: a 1-bit stencil, painted in fill colour
interpolate: bool,
decode: []f64,
smask: ^Image, // /SMask, alpha
mask: ^Image, // /Mask as a stencil
color_key: []int, // /Mask as a colour-key range
// Decoded on demand, cached: straight-alpha RGBA8 unless is_mask.
rgba: Bitmap,
decoded: bool,
}
image_load :: proc(doc: ^Document, stm: ^Stream, res: ^Dict) -> (^Image, Error)
image_decode :: proc(doc: ^Document, img: ^Image) -> (^Bitmap, Error) // cached on img
image_load_inline :: proc(doc: ^Document, d: ^Dict, data: []byte, res: ^Dict) -> (^Image, Error)
Decoded images are cached on the Document keyed by their stream, and the cache has a byte budget — a 300-page catalogue must not hold every photograph at once.
Fonts (font.odin and friends)
Each font-program reader is self-contained and knows nothing about PDF:
// font_cff.odin
cff_parse :: proc(data: []byte, allocator := context.allocator) -> (^Cff_Font, Error)
cff_glyph_count :: proc(f: ^Cff_Font) -> int
cff_glyph_path :: proc(f: ^Cff_Font, gid: u16, out: ^Path) -> Error // glyph space
cff_glyph_advance:: proc(f: ^Cff_Font, gid: u16) -> f64
cff_gid_for_name :: proc(f: ^Cff_Font, name: string) -> (u16, bool)
cff_gid_for_cid :: proc(f: ^Cff_Font, cid: u16) -> (u16, bool)
cff_font_matrix :: proc(f: ^Cff_Font) -> Matrix
// font_truetype.odin -- same shape, plus cmap lookup
tt_parse, tt_glyph_count, tt_glyph_path, tt_glyph_advance,
tt_gid_for_unicode, tt_gid_for_mac_code, tt_gid_for_name, tt_units_per_em
// font_type1.odin
t1_parse, t1_glyph_count, t1_glyph_path, t1_glyph_advance, t1_gid_for_name,
t1_builtin_encoding
font.odin ties them to the PDF font dictionary:
Font_Code :: struct {
code: u32, // raw character code from the string
cid: u32,
gid: u32,
width: f64, // glyph space (1/1000 text space for everything but Type3)
unicode: rune, // -1 if unknown
nbytes: int, // bytes of the string this code consumed
is_space: bool, // single-byte code 32 -- word spacing applies
}
font_load :: proc(doc: ^Document, d: ^Dict) -> (^Font, Error) // cached on doc
font_decode :: proc(f: ^Font, s: []byte, out: ^[dynamic]Font_Code)
font_glyph_path :: proc(f: ^Font, gid: u32, out: ^Path) -> Error
font_matrix :: proc(f: ^Font) -> Matrix
A font that fails to load must still produce a Font with widths, so text lays out and extracts correctly even when nothing can be drawn. Missing glyphs draw nothing; they never abort a page.
Public API (page.odin)
Render_Options :: struct {
ctm: Matrix, // applied on top of the page flip and /Rotate
background: [4]u8, // default opaque white
alpha: bool, // default false: an opaque RGB(A) page
no_annotations: bool,
allocator: mem.Allocator,
}
render_page :: proc(page: ^Page, opts := Render_Options{}) -> (Bitmap, Error)
run_page :: proc(page: ^Page, dev: ^Device, ctm: Matrix) -> Error
extract_text :: proc(page: ^Page, allocator := context.allocator) -> (string, Error)