Files

210 lines
8.0 KiB
Markdown
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
## 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)
```