210 lines
8.0 KiB
Markdown
210 lines
8.0 KiB
Markdown
## 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)
|
||
|
|
```
|