## 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. ```odin // 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`) ```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`) ```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`) ```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`) ```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: ```odin // 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: ```odin 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`) ```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) ```