initial commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
## 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)
|
||||
```
|
||||
Reference in New Issue
Block a user