initial commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Layer 3 plan (rendering primitives + fonts) -> workflow 2
|
||||
|
||||
Stage A (parallel, independent - depend only on the frozen spine):
|
||||
raster raster.odin span-callback scanline AA rasteriser, both fill rules
|
||||
stroke stroke.odin stroke -> Flat_Path, caps/joins/miter/dashes, hairlines
|
||||
blend blend.odin Blend_Mode math, premultiplied compositing, group/mask compose
|
||||
colorfn colorspace.odin + function.odin (coupled: Separation needs a tint transform)
|
||||
cff font_cff.odin CFF/Type2 charstrings, CIDFont CFF, seac, hint-skip
|
||||
truetype font_truetype.odin glyf/loca/cmap/hmtx/post/composite glyphs
|
||||
type1 font_type1.odin PFB/PFA, eexec, Type1 charstrings, seac, flex/hint replacement
|
||||
stdfonts font_standard.odin standard-14 metrics + Standard/WinAnsi/MacRoman/MacExpert/Symbol/ZapfD
|
||||
cmaps cmap.odin embedded CMaps, /ToUnicode, predefined CMap identity + common ones
|
||||
|
||||
Stage B (needs A):
|
||||
image image.odin image XObjects -> RGBA8, /Decode, /ImageMask, /SMask, /Mask, colour key
|
||||
shading shading.odin types 1-7 (function, axial, radial, free/lattice triangle mesh, coons, tensor)
|
||||
font font.odin the PDF font dictionary -> Font, widths, encodings, CID mapping, Type3
|
||||
|
||||
Stage C: integrate + adversarial review + fix
|
||||
|
||||
# Layer 4 plan (interpreter + devices + public API) -> workflow 3
|
||||
pattern.odin, device_raster.odin, device_text.odin, device_list.odin,
|
||||
content.odin, page.odin, annot.odin, outline.odin,
|
||||
examples/pdfinfo, examples/pdfrender, examples/pdftext, README.md
|
||||
@@ -0,0 +1,46 @@
|
||||
# What real PDFs actually use
|
||||
|
||||
Feature counts across the 51 PDFs on this machine (39 in ~/Downloads, 12 ISO spec
|
||||
documents) — contracts, invoices, standards, scans, exports from Word, InDesign
|
||||
and web tooling. Percentages are of files that contain the marker at all.
|
||||
|
||||
| Feature | Files | Note |
|
||||
|---|---:|---|
|
||||
| FlateDecode | 51 (100%) | the only filter that must be fast |
|
||||
| DCTDecode | 19 (37%) | own JPEG decoder is not optional |
|
||||
| ASCII85Decode | 3 (6%) | |
|
||||
| CCITTFaxDecode | 1 (2%) | long tail |
|
||||
| LZWDecode | 1 (2%) | long tail |
|
||||
| JPXDecode / JBIG2Decode | 0 | never seen; pass through |
|
||||
| xref streams + object streams | 26 (51%) | half the corpus is PDF 1.5+ structure |
|
||||
| Linearized | 13 (25%) | |
|
||||
| Encrypted | 3 (6%) | |
|
||||
| AcroForm | 13 (25%) | |
|
||||
| Annots | 27 (53%) | appearance streams matter for rendering |
|
||||
| ExtGState | 44 (86%) | |
|
||||
| SMask | 30 (59%) | **transparency is mainstream, not exotic** |
|
||||
| Group (transparency) | 25 (49%) | |
|
||||
| BM (blend mode) | 23 (45%) | |
|
||||
| Type0 (composite) fonts | 27 (53%) | |
|
||||
| CIDFontType2 | 29 (57%) | |
|
||||
| FontFile2 (TrueType) | 31 (61%) | |
|
||||
| FontFile3 (CFF) | 11 (22%) | |
|
||||
| ToUnicode | 32 (63%) | text extraction has a real chance |
|
||||
| Type3 fonts | 1 (2%) | long tail |
|
||||
| ICCBased | 12 (24%) | render via /Alternate |
|
||||
| Indexed | 8 (16%) | |
|
||||
| Separation | 3 (6%) | |
|
||||
| Lab | 3 (6%) | |
|
||||
| Pattern | 7 (14%) | |
|
||||
| Shading | 3 (6%) | |
|
||||
| DeviceN, CalRGB | 0 | implement, but do not tune for |
|
||||
|
||||
Reproduce with the loop in the project history; it is a plain grep for each
|
||||
marker across every PDF, which works because stream dictionaries are never
|
||||
themselves inside an object stream.
|
||||
|
||||
**What this ordered:** Flate and DCT decode quality, TrueType and CFF glyph
|
||||
outlines, Type0/CID mapping, `/ToUnicode`, and the transparency model (soft
|
||||
masks, groups, blend modes) are the load-bearing work. CCITT, LZW, JPX, JBIG2,
|
||||
Type3, DeviceN and the exotic shading types are correctness obligations, not
|
||||
performance ones.
|
||||
@@ -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