commit fff8b677f83a7f163df1f0937bd5ac611a7b1721 Author: Hisham Aburaqibah Date: Fri Sep 25 23:32:29 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e55b03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +docs/workflows/* +**/*.png +**/*.ppm diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..6afd9f1 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,379 @@ +# pdf-odin — architecture and contributor contract + +A self-contained PDF reader and renderer in pure Odin. No C dependencies: +everything outside of `core:` is written here. + +## Pipeline + +``` +bytes + │ lexer.odin, parser.odin COS syntax → Object + │ xref.odin, objstm.odin file structure, object streams + │ crypt.odin decryption (transparent, at parse time) + ▼ +Document ── document.odin, pagetree.odin ─────────────────────────────────� + │ filters.odin + filter_*.odin stream decoding │ + ▼ │ +Page ── page.odin │ + │ content.odin content-stream operators │ + │ gstate.odin graphics state stack │ + │ colorspace.odin, function.odin, shading.odin, pattern.odin │ + │ font*.odin, cmap.odin glyph outlines and widths │ + ▼ │ +Device (vtable) ── device.odin ───────────────────────────────────────────┘ + ├── device_raster.odin → Bitmap (RGBA8) path.odin, raster.odin, + ├── device_text.odin → extracted text stroke.odin, blend.odin + └── device_list.odin → display list image.odin +``` + +Everything above `Device` is format knowledge; everything below is pixels. The +three devices share one interpreter, which is why text extraction agrees with +what is drawn instead of being a second, subtly different parse. + +## File ownership + +One concern per file. Do not add procedures to a file you do not own. + +| File | Contents | +|---|---| +| `pdf.odin` | package doc, version constants | +| `error.odin` | `Error`, `Repair`, `error_string` | +| `object.odin` | `Object`, `Dict`, `Array`, `Stream`, accessors | +| `geom.odin` | `Point`, `Matrix`, `Rect` | +| `lexer.odin` | COS tokeniser | +| `parser.odin` | token stream → `Object` | +| `xref.odin` | xref tables, xref streams, trailer chain, rebuild-by-scan | +| `objstm.odin` | `/Type /ObjStm` compressed object streams | +| `document.odin` | `Document`, `open_*`, `close`, `resolve`, object cache | +| `pagetree.odin` | page tree walk, inherited attributes, `Page` | +| `crypt.odin` | standard security handler, RC4, AES-CBC, key derivation | +| `filters.odin` | filter chain dispatch, `stream_data`, predictors | +| `filter_flate.odin` | tolerant inflate | +| `filter_lzw.odin` | LZW | +| `filter_ascii.odin` | ASCIIHex, ASCII85 | +| `filter_runlength.odin` | RunLength | +| `filter_ccitt.odin` | CCITT G3/G4 fax | +| `filter_dct.odin` | JPEG: baseline + progressive, grey/YCbCr/CMYK/YCCK | +| `encoding.odin` | text strings, PDFDocEncoding, dates, name/number trees | +| `function.odin` | PDF functions type 0/2/3/4 | +| `colorspace.odin` | all colour spaces → RGB | +| `path.odin` | `Path`, construction, transform, flatten | +| `raster.odin` | anti-aliased scanline fill | +| `stroke.odin` | stroke → fill path (joins, caps, dashes) | +| `bitmap.odin` | `Bitmap`: allocation, rows, premultiplication, RGBA conversion | +| `blend.odin` | blend modes, compositing, soft masks | +| `image.odin` | image XObjects → samples, image masks, `/SMask` | +| `shading.odin` | shading types 1–7 | +| `pattern.odin` | tiling and shading patterns | +| `cmap.odin` | CMap parsing, `/ToUnicode`, predefined CMaps | +| `font.odin` | font loading and dispatch, widths, encodings | +| `font_cff.odin` | CFF / Type2 charstrings | +| `font_truetype.odin` | `glyf`/`loca`/`cmap`/`hmtx`/`post` | +| `font_type1.odin` | Type1, eexec, Type1 charstrings | +| `font_standard.odin` | standard-14 metrics, built-in encodings | +| `content.odin` | content-stream lexer and operator dispatch, Type3 glyph execution | +| `gstate.odin` | graphics state and its stack | +| `device.odin` | the `Device` vtable | +| `device_raster.odin` | rasterising device | +| `device_text.odin` | text-extraction device | +| `device_list.odin` | display-list device | +| `page.odin` | `render_page`, `extract_text`, public page API | +| `annot.odin` | annotation appearance streams | +| `outline.odin` | document outline, destinations | + +## Conventions + +- **4 spaces**, no tabs. Types `Pascal_Snake` (`Xref_Entry`, `Graphics_State`) + except the handful of short public nouns already fixed (`Object`, `Dict`, + `Page`, `Matrix`). Procedures and variables `snake_case`. Enum members + `Pascal_Snake`. +- Procedures are prefixed by the noun they act on: `path_transform`, + `font_glyph`, `dict_get`. This keeps a flat 40-file package navigable. +- `@(private)` anything that is not part of the public API or used across + files. The public API is exactly what the package doc comment at the top of + `pdf.odin` names, plus the types those procedures take and return: `open_file`, + `open_bytes`, `Open_Options`, `close`, `page_count`, `page_get`, `Page`, the + object accessors (`resolve`, `get_object`, `get_dict`, `get_stream`, `dict_*`, + `array_*`, `to_*`, `stream_data`), `Error`, `Repair`, `error_string`, the + encryption accessors, and -- once the device layer exists -- `render_page`, + `extract_text` and `Bitmap`. Everything else is `@(private)`, tests included: + a test lives in the same package and reaches private names fine, so being + called by one is not a reason to export. Widening the public API means editing + that doc comment in the same commit, which is what keeps the two in step. +- Comments explain *why*, especially why a tolerance exists: "a `/Length` + that disagrees with the bytes is common enough that …". Do not narrate what + the next line does. +- Cite the spec by clause number when the behaviour is non-obvious: + `// ISO 32000-2, 7.5.8.2`. + +## Memory + +- A `Document` owns a growing arena (`core:mem/virtual`). Every object, + dictionary, stream and decoded buffer produced while reading lives there and + is reclaimed in one call by `close`. Allocate with `doc.allocator`. +- Anything handed to a caller that is *not* owned by the Document takes an + explicit `allocator := context.allocator` parameter and says so in its doc + comment. That is `render_page`'s `Bitmap`, `extract_text`'s string, and + nothing else. +- Per-page rendering scratch goes in a temporary arena that is reset between + pages, never in the Document arena — a 900-page render must not grow without + bound. +- Never call `free` or `delete` on Document-arena memory. + +## Robustness — the rule that matters most + +Malformed PDFs are the normal case, not the exception. A reader that rejects a +file every other viewer opens is a broken reader. + +1. **Never panic.** No unchecked slicing, no `assert` on file-derived data, no + unwrapped `or_else` on parsed values. Bounds-check every index that came + from the file. +2. **Never loop forever.** Every loop driven by file data needs a bound: a + recursion depth limit (`MAX_DEPTH`), an iteration fuel counter, or a + visited-set. Cyclic `/Parent` chains, self-referencing object streams and + xref `/Prev` loops all occur in real files. +3. **Degrade, don't fail.** A broken font renders as blanks; a broken image is + skipped; an unparsable content fragment is stepped over to the next + operator. Record a `Repair` on the Document and carry on. Only return an + `Error` when there is genuinely nothing to hand back. +4. **Clamp allocations.** A `/Length`, `/W`, `/Height` or `/Size` from a file is + attacker-controlled. Check it against the remaining file size before + allocating, and against `MAX_*` limits. +5. **No NaN or infinity below the Device line.** Numbers coming out of a file + are checked with `is_finite` at the boundary. + +## Tests — write them with the code, not after + +Every file `foo.odin` gets a `foo_test.odin` in the same package, using +`core:testing`. A layer is not finished until its tests pass. + +- **Unit tests** are self-contained: byte literals in, expected values out. + Codec tests use vectors from the format's own specification where one exists. +- **Golden PDFs** live in `testdata/` and are tiny, hand-written, and + committed. They cover the structural cases: classic xref, xref stream, object + stream, incremental update, broken xref needing a rebuild, wrong `/Length`, + each encryption revision. +- **Corpus tests** run over whatever is in the directory named by the + `PDF_ODIN_CORPUS` environment variable and skip silently when it is unset, so + they never depend on files that are not in the repository. They assert only + invariants: opens without error, page count > 0, every page renders without + crashing, no leaks. +- Fuzz-shaped tests: truncate and bit-flip the golden files at many offsets and + assert the library returns an error rather than crashing or hanging. + +Run `scripts/check.sh` (type-check + vet) and `scripts/test.sh` before +declaring anything done. + +## Limits + +Defined in `document.odin`, referenced everywhere: + +``` +MAX_DEPTH 32 // nested arrays/dicts, form XObject recursion +MAX_OBJECT_NUMBER 8388607 +MAX_XREF_CHAIN 64 // /Prev hops +MAX_CONTENT_OPERANDS 32 +MAX_BITMAP_PIXELS (1 << 28) +``` + + +## Status: what is written and what remains + +**The library renders.** Every file in the table above is written, tested and +driven over a real corpus. `open_file -> page_get -> render_page` produces a +correct picture of a real page, `extract_text` produces its text off the same +interpreter, and `page_display_list` records it for a caller with a backend of +their own. There is no skeleton left. + +Complete: `pdf error object geom document lexer parser xref objstm pagetree +crypt encoding filters filter_flate filter_lzw filter_ascii filter_runlength +filter_ccitt filter_dct path bitmap gstate device raster stroke blend +colorspace function cmap font_standard font_cff font_truetype font_type1 font +image shading pattern content device_raster device_text device_list page annot +outline`. + +Type3 was planned as `font_type3.odin` and does not need a file: a Type3 glyph +is a content stream, so loading it belongs to `font.odin` with the other font +dictionaries (`font_load_type3`, `font_type3_proc`) and *running* it belongs to +the interpreter that runs every other content stream (`content_run_type3`, +`content_type3_d1`, and the `Type3_Glyph` flag that makes `d0`/`d1` legal and +stops a glyph that shows itself from recursing). Splitting it out would have +meant a file that could only call back into `content.odin`. It is finished and +proven end to end: a Type3 font whose glyph is `0 0 750 750 re f` at 40 pt +rasterises to exactly the three 30-point squares its `/FontMatrix` and +`/Widths` call for. + +### What remains + +Nothing structural. What is left is one real gap, two deliberate +approximations, and the review debt from the earlier batches. + +**Non-embedded fonts draw nothing.** This is the one visible defect in the +whole layer and the render sweep below found it. A font dictionary with no +`/FontFile*` still loads, still lays out and still extracts text correctly -- +that is what the render-layer contract asks for and it works -- but +`font_glyph_path` has no outline to return, so the text is invisible. The +library ships no font programs of its own and reads none from the system, so +there is nothing to substitute *with*: fixing it means either bundling a +metric-compatible face for the standard 14 or adding a system-font lookup, and +both are new subsystems rather than a repair. Fifty-nine of the corpus's 894 +fonts are unembedded, and they are why fifteen corpus pages come back blank or +nearly blank. + +**Two transparency approximations**, both stated at the top of +`device_raster.odin` where they are made: a knockout group is rendered as a +non-knockout one, and a non-isolated group whose own alpha is not 1 or whose +own blend mode is not Normal is rendered isolated. Both are the approximations +every renderer without a backdrop-removal pass makes, and `docs/corpus-notes.md` +ranks the cases they differ on as rare. + +**Still owed from the earlier batches**, unchanged: the two adversarial review +lenses -- visibly-wrong pixels, and the data tables audited line by line +against the AFMs and the AGL. Every table reached by binary search +(`STD_TEXT_GLYPHS`, `STD_SYMBOL_GLYPHS`, `STD_ZAPF_GLYPHS`, `AGL_TABLE`, +`AGL_BY_RUNE`) has been verified sorted and in range, and spot values across +all five width tables and both AGL directions match their source documents, but +that is a smoke test, not the audit. + +### What the integration checks have covered + +Three batches were built in parallel and each has since been integrated. The +first was the six rendering primitives — `raster stroke blend colorspace +function cmap font_standard`; the second was the seven files above them — +`font font_cff font_truetype font_type1 image shading pattern`; the third was +the render layer itself — `content device_raster device_text device_list page +annot outline`. + +From the first batch: + +- The layer composes. `path_flatten -> raster_fill -> blend_span` and + `stroke_flatten -> raster_fill -> blend_span` were driven end to end into a + Bitmap and checked pixel by pixel against the geometry: aligned and + half-covered rectangles, both fill rules, butt and round joins and caps, a + dashed line, a hollow stroked square, a hairline under a 4x CTM, and a clip + not anchored at the origin. All correct, with no fringing and no seams. +- The three conventions the primitives meet on hold everywhere: coverage is + 0..255 with 255 meaning fully covered; every `f64` colour is straight and + 0..1; every `Bitmap` below the Device line is premultiplied. `blend.odin`'s + header states this rule and every entry point in it obeys it. +- One composition bug, in `path.odin` rather than in the six: `path_flatten` + dropped the connecting segment of a `l` that followed an `h`, because its + `.Line` arm did not seed the new subpath with the current point the way its + `.Curve` arm always has. Fixed, with a regression test that holds the two arms + to each other. +- Two parallel-authorship seams, both fixed: `object_identity` (the cache key + both `colorspace_load` and `function_load` use) had been defined in + `function.odin` and reached into from `colorspace.odin`, and now lives in + `object.odin` with the rest of the `Object` accessors; `font_standard.odin` + carried its own hex-digit decoder alongside `object.odin`'s `hex_value`. +- One internal contradiction, fixed: the bare `/Pattern` singleton declared + `n = 1` directly under a comment saying a colour in it has no components. + `n` is what tells the interpreter how many operands `scn` takes, and `/P1 scn` + takes none, so it is 0. + +From the second batch: + +- The seven compose with the primitives and with each other. A font dictionary + with an embedded TrueType program was driven through `font_load -> + font_decode -> font_glyph_path -> path_flatten -> raster_fill -> blend_span` + and the ink checked pixel by pixel against where the glyph's em square lands + under a real text matrix; an image was driven through `image_load -> + image_decode -> blend_span` as a stencil and through `blend_span_rgba8` as + colour; an axial shading and the same shading behind a `/PatternType 2` were + painted and checked against exact pixel values computed by hand. Every one + fitted first time. +- One parallel-authorship seam, fixed: `image.odin` carried its own + `img_byte` — the 0..1-to-byte quantiser, NaN handling and half-up rounding + and all — beside `blend.odin`'s `blend_quantize`, which is the same procedure + and which `shading.odin`, written by a different author against the same + contract, had correctly reused. `image.odin` now calls `blend_quantize` too. + Quantising a colour to a byte is `blend.odin`'s to own: it is the file whose + header states the byte conventions the whole device layer meets on. +- The per-format byte readers (`cff_u16`, `tt_u16`, `mesh_bits`, `img_sample`) + were checked against each other and left alone. They look alike and are not: + each font-program reader is required to be self-contained, and the two + sample unpackers differ in kind — one is indexed and capped at 16 bits, the + other is a sequential bitstream reader up to 32. +- `corpus_test.odin` grew `test_corpus_resources`, which loads and drives every + font, image, shading and pattern named by every page of every corpus file, + recursing into form XObjects and tiling-pattern resources. Over 71 documents + it reaches 894 distinct fonts and 1579 images, probes 123 000 glyph outlines + and decodes 372 000 character codes, and found no failure of any kind. That + is the first time the library met real subsetted font programs at scale, and + the invariants it holds them to are the ones the interpreter will depend on: + a Font always comes back, an embedded program either has glyphs or sets + `program_failed`, every outline is finite, `font_decode` consumes exactly the + bytes it is given, and an image either decodes to a bitmap of its own + dimensions or degrades to an error with no bitmap at all. +- Three things the sweep taught, all recorded here because they will otherwise + be rediscovered as bugs: a subsetter leaves `maxp.numGlyphs` at the original + count while emptying `loca`, so a font can declare 22 026 glyphs and hold 25 + — sampling glyph ids rather than scanning them will find none of them; a + Type1C subset is routinely stripped to `.notdef` and `space`, both of which + are a single `endchar` byte, so "no glyph in this font draws" is a normal + file and not a defect; and a font with no embedded program sets neither a + glyph count nor `program_failed`, because nothing failed — the standard-14 + metrics are the whole of what the file gave. +- 296 of the 976 tests belong to these seven files. None are vacuous: the ones + that appear to assert little all go through a local helper that asserts, and + the two that end in `expect(t, true)` are the truncation and bit-flip fuzzers, + where reaching the last line is the property under test — the same spelling + `xref_test.odin` and `objstm_test.odin` have used since the COS layer. + +From the third batch — the render layer, and the first time anything in this +repository produced a picture: + +- The seven files fitted together with no conflicts to resolve. Nothing had to + be moved between files, no procedure was duplicated across two authors, and + no frozen type had to change: `check.sh` and the 1294-test suite were both + green on the first run after the seven landed. That is what freezing + `Device`, `Bitmap`, `Render_Options` and the `Span_Proc` contract ahead of + the code bought. +- **The render sweep** (`test_corpus_render` in `corpus_test.odin`) is the + check this layer exists for and the most informative thing run on the project + so far. It renders the first page of every corpus file — every page of every + file under twenty — at 72 dpi, holds each render to `page_size`'s dimensions, + a 4-component straight-alpha bitmap and a time budget, and then measures the + fraction of non-background pixels. Over 71 documents and 315 pages: zero + errors, zero leaks, mean ink 18.3% (Downloads) and 7.1% (the ISO corpus), + slowest page 1.4 s, no page's dimensions disagreeing with `page_size`. +- **Blankness is the invariant that finds real bugs, and it found one.** A page + that draws nothing is invisible to every other check: it parses, loads every + font, decodes every image, and returns `.None`. So the sweep also asks the + *text* device what the interpreter saw on any page under 1% ink, and reports + three different things where a bare ink number reports one: a page that draws + nothing and extracts nothing is an empty sheet, a page that draws nothing and + extracts 3 000 characters is a defect, and a page with a paragraph on it + under 1% ink is the partial version of the same defect. Of the 19 light pages + in the two corpora, 6 are genuinely blank paper and 14 are the non-embedded + font gap. **Not one is an interpreter, clip or base-matrix bug** — which was + the thing most likely to be wrong and is the reason the sweep was written. +- The diagnosis is unambiguous on + `Videcom/VRS XML Service 2020 02FEB2023.pdf` page 1, the clearest case: the + embedded Courier New draws its code blocks perfectly, every heading rule and + the hyperlink rule land exactly where they belong — and every word set in + Arial or Times New Roman, neither of which the file embeds, is missing. The + geometry, the clipping, the colour and the stroking are all correct; the + outlines simply are not in the file and are not in this library either. +- **Text extraction** (`test_corpus_text`) runs the same interpreter into the + text device over every page of every file: 3 066 pages, 5.5 MB, 5.37 M runes. + The ISO corpus extracts text from 100% of its 1 234 pages with **zero** + U+FFFD. The Downloads corpus yields text on 85% of pages at 0.55% U+FFFD, and + every one of the 21 pages above 10% U+FFFD is in a single Arabic-language ITU + recommendation whose subset fonts name their glyphs by contextual + presentation form — `lamwithaleffinal`, `hahwithmeeminitial` — which is in + neither the AGL nor the file's own `/ToUnicode`. Those glyphs *render* + correctly; there is simply nothing in the file that says what they mean, so + U+FFFD is the honest answer and not a defect. The unicode chain is otherwise + clean across the whole corpus. +- `examples/pdfrender` and `examples/pdftext` were added and are compiled by + `check.sh` alongside `pdfinfo`, which is what keeps `render_page`, + `extract_text`, `Bitmap`, `bitmap_row` and `scale_matrix` genuinely public + rather than accidentally reachable only from inside the package. +- 307 of the 1294 tests belong to these seven files. None are vacuous: the 21 + that carry no `testing.expect` of their own all go through a local helper + that asserts (`rt_expect`, `pt_expect_point`, `ol_expect_label`), the same + spelling every batch before them used. + +`docs/corpus-notes.md` records what the real-world corpus actually uses, which +is what the ordering above is based on; `docs/build-plan.md` is the plan. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4badd66 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# pdf-odin + +A library for reading PDF files written in Odin. + +#### Note: This entire library was written by Claude Opus 5. diff --git a/annot.odin b/annot.odin new file mode 100644 index 0000000..eee656a --- /dev/null +++ b/annot.odin @@ -0,0 +1,477 @@ +package pdf + +import "core:mem/virtual" + +// Annotations and their appearance streams (ISO 32000-2, 12.5). +// +// An annotation is a dictionary in a page's /Annots array: a link, a form +// field, a stamp, a signature, a highlight, a sticky note. None of them say +// what they look like. What is drawn is an APPEARANCE STREAM -- a form XObject +// under /AP -- mapped onto the annotation's /Rect. /Annots is in 53% of the +// files in the corpus, so a renderer that stops at the page's content stream +// draws the page perfectly and loses every form field and every stamp on it. +// +// THIS LIBRARY NEVER SYNTHESISES AN APPEARANCE +// +// An annotation with no /AP draws nothing at all: no border for a /Square, no +// leader line for a /Line, no laid-out text for a /FreeText or for a widget's +// /DA string. That is a deliberate scope decision, not an oversight. +// Generating appearances is the whole of 12.5.6's per-subtype geometry plus +// 12.7.4.3's variable-text layout, which is what a form-*filling* library +// owes; a reader owes what the file says the page looks like. Every producer +// that expects its annotations to be seen writes an /AP, and the ones that do +// not set /NeedAppearances, which announces that the consumer is expected to +// be an editor. +// +// PLACEMENT IS THE WHOLE JOB +// +// `annot_matrix` is the algorithm of 12.5.5, step by named step. Getting it +// wrong does not fail loudly -- it silently misplaces or wrongly scales every +// stamp and form field on the page -- so each step is named in the code and +// pinned by tests against hand-computed matrices. +// +// Once placed, the appearance is drawn by the ordinary content interpreter, +// through two synthesised tokens: `/Ap Do`. Invoking a form XObject is exactly +// what `Do` is for, and going through the interpreter rather than around it is +// what gets the /BBox clip, a transparency /Group, the form's own /Matrix, the +// /OC test and the self-reference guard right -- all of it already written and +// tested in content.odin. + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +// Entries read from one page's /Annots. A table of contents can carry a link +// per line and a form can carry a field per cell, so this is generous; past it +// the array is not a page's annotations. +@(private) +MAX_ANNOTS :: 4096 + +// --------------------------------------------------------------------------- +// Flags +// --------------------------------------------------------------------------- + +// Annot_Flag is one bit of /F (12.5.3, table 167). +// +// The format numbers its flag bits from 1; these values are the same bits +// numbered from 0, which is what a bit_set indexes by. `.Hidden` is therefore +// the format's "bit position 2". +Annot_Flag :: enum u32 { + Invisible = 0, + Hidden = 1, + Print = 2, + No_Zoom = 3, + No_Rotate = 4, + No_View = 5, + Read_Only = 6, + Locked = 7, + Toggle_No_View = 8, + Locked_Contents = 9, +} + +Annot_Flags :: bit_set[Annot_Flag; u32] + +// annot_flags_read turns /F into a set. Bits above the ten the format defines +// are dropped rather than kept: they are reserved, and a set that holds +// members no enumerator names prints as garbage. +@(private) +annot_flags_read :: proc(raw: i64) -> (f: Annot_Flags) { + for bit in 0 ..< len(Annot_Flag) { + if raw & (i64(1) << uint(bit)) != 0 { + f += {Annot_Flag(bit)} + } + } + return +} + +// --------------------------------------------------------------------------- +// Annot +// --------------------------------------------------------------------------- + +// Annot is one annotation of a page with its appearance already chosen. +// +// It borrows from the Document: `dict`, `ap`, `subtype` and `state` all point +// into the Document's arena or the file bytes and become invalid when `close` +// is called. The struct is a value and holds no state of its own. +Annot :: struct { + // The annotation dictionary itself, for the many per-subtype entries this + // library does not interpret (/Contents, /T, /Dest, /FT, /V ...). + dict: ^Dict, + // Object number, or {0,0} when /Annots held the dictionary directly. + ref: Ref, + // /Subtype verbatim: "Link", "Widget", "Stamp", "Popup", ... Empty when + // the file omitted it, which is malformed and does happen. + subtype: string, + // /Rect, with its corners sorted: the format allows either order and + // producers use both. + rect: Rect, + flags: Annot_Flags, + + // The appearance stream to draw, after /AS has chosen among the states of + // an /AP /N sub-dictionary. nil when the annotation has no appearance, and + // then nothing is drawn -- see the note at the top of this file. + ap: ^Stream, + // The appearance state /AS asked for, or the sole state of a /N + // sub-dictionary that had only one. Empty when /N was a stream outright. + // It is set even when the state named nothing and `ap` is therefore nil, + // which is the ordinary way an unticked checkbox is written. + state: string, + + // False when this annotation is not part of the page's visible content: + // /F hides it, or it is a popup. It says nothing about /OC, which is + // resolved when the appearance runs -- a text-extraction pass deliberately + // draws layers a renderer would leave switched off, and that choice is + // `Content_Options.ignore_optional_content`, made per run and not here. + visible: bool, +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// annot_load reads a page's /Annots. +// +// Every entry that resolves to a dictionary comes back, including the ones +// that will not be drawn: a viewer needs a /Link's rectangle to make it +// clickable and a /Widget's dictionary to fill it in, whether or not either +// has an appearance. `visible` and `ap` say what `annot_run` will do with it. +// +// The slice is the caller's, allocated with `allocator`, and is released with +// one `delete`; everything the Annots point at is Document memory and must not +// be freed. An error comes back only for a nil argument -- a page with no +// /Annots, or with an /Annots that is not an array, has no annotations, which +// is not a failure. +annot_load :: proc(doc: ^Document, page: ^Page, allocator := context.allocator) -> ([]Annot, Error) { + if doc == nil || page == nil || page.dict == nil { + return nil, .Invalid_Argument + } + arr := dict_array(doc, page.dict, "Annots") + n := min(array_len(arr), MAX_ANNOTS) + if n == 0 { + return nil, .None + } + + list := make([dynamic]Annot, 0, min(n, 64), allocator) + defer delete(list) + + for i in 0 ..< n { + raw := array_get_raw(arr, i) + d, ok := to_dict(resolve(doc, raw)) + if !ok { + // A null entry, or a reference to an object that is gone, is how + // a producer deletes an annotation without rewriting the array. + continue + } + a := annot_read(doc, d) + a.ref, _ = to_ref(raw) + append(&list, a) + } + + // Handed back at its exact length: a caller frees this with `delete`, and + // freeing a slice that was sized as a dynamic array's capacity is a + // mismatch for every allocator that checks. + out := make([]Annot, len(list), allocator) + copy(out, list[:]) + return out, .None +} + +@(private) +annot_read :: proc(doc: ^Document, d: ^Dict) -> (a: Annot) { + a.dict = d + a.subtype = dict_name(doc, d, "Subtype") + // object_to_rect sorts the corners and rejects a non-finite one, so + // everything downstream can subtract without checking. + a.rect = dict_rect(doc, d, "Rect") + a.flags = annot_flags_read(dict_int(doc, d, "F", 0)) + a.ap, a.state = annot_appearance(doc, d) + + // 12.5.3: bit 2 Hidden means do not render it anywhere. Bit 6 NoView means + // do not render it on a screen -- and this library renders for viewing, so + // it is honoured the same way; a consumer producing a print stream is the + // one that would want the opposite and would have to say so. + // + // Bit 1 Invisible does NOT suppress an appearance stream. It applies only + // to an annotation whose subtype the reader has no handler for, and it + // says "then draw nothing rather than a placeholder"; an annotation with + // an /AP is handled by definition (12.5.3, table 167). + a.visible = !(.Hidden in a.flags) && !(.No_View in a.flags) + + // A popup is the window a markup annotation opens when its parent is + // clicked. It is never part of the page's own appearance, even though it + // sits in /Annots and carries a /Rect (12.5.6.14). + if a.subtype == "Popup" { + a.visible = false + } + + // /NoZoom and /NoRotate ask for an appearance that ignores the viewer's + // zoom and the page's /Rotate. Both are properties of an interactive + // view, not of the page: honouring them needs the zoom factor teased back + // out of the CTM, and a static render has no zoom to be independent of. + return +} + +// annot_appearance picks the stream to draw out of /AP. +// +// Only /N is page content. /R (rollover) and /D (down) are responses to a +// pointer that is not there and never appear in a rendering of the page +// (12.5.5). +@(private) +annot_appearance :: proc(doc: ^Document, d: ^Dict) -> (^Stream, string) { + n := dict_get(doc, dict_dict(doc, d, "AP"), "N") + if stm, ok := to_stream(n); ok { + return stm, "" + } + + // A sub-dictionary instead of a stream is keyed by appearance state: it is + // how a checkbox carries both its /Off and its /Yes look in one place and + // /AS says which of them it is showing. + sub, is_dict := to_dict(n) + if !is_dict { + return nil, "" + } + + if state := dict_name(doc, d, "AS"); state != "" { + // A state that names nothing draws nothing. A checkbox whose /N holds + // only /Yes and whose /AS is /Off is unchecked, and the absent entry + // IS its appearance; falling back to the only stream present would + // tick every empty box on the page. + stm, ok := to_stream(dict_get(doc, sub, state)) + return ok ? stm : nil, state + } + + // /AS is required as soon as /N is a sub-dictionary and is nonetheless + // left out. With exactly one state there is nothing to disambiguate and + // every viewer draws it; with more there is no way to choose, and drawing + // the wrong state is worse than drawing none. + if len(sub^) == 1 { + for k, v in sub^ { + if stm, ok := to_stream(resolve(doc, v)); ok { + return stm, k + } + } + } + if len(sub^) > 1 { + add_repair(doc, .Bad_Resource, 0) + } + return nil, "" +} + +// --------------------------------------------------------------------------- +// Placement -- ISO 32000-2, 12.5.5, "Algorithm: appearance streams" +// --------------------------------------------------------------------------- + +// annot_matrix maps an appearance stream onto its annotation's rectangle. +// +// The algorithm, exactly as the clause gives it: +// +// 1. Map the appearance's /BBox through its /Matrix -- transform its four +// corners. +// 2. Take the bounding box of those four transformed points. For a /Matrix +// that rotates or skews this is bigger than the shape inside it, and +// that is the whole reason the step exists. +// 3. Compute A: the matrix that maps that box onto the annotation's /Rect. +// 4. The appearance is drawn under /Matrix * A * the page's CTM. +// +// `form` is that final matrix -- the one the appearance's own coordinates are +// drawn under, and what a caller asking where an annotation landed wants. +// `invoke` is the same thing without the form's own /Matrix (A * page_ctm), +// because the interpreter applies /Matrix itself when it runs the stream and +// applying it here as well would apply it twice. +// +// `ok` is false when there is nothing to place: an annotation with no area, or +// a transformed appearance box with no width or no height. The scale factor of +// step 3 would be a division by zero, and a zero-area appearance has no +// picture in it to stretch. +@(private) +annot_matrix :: proc(doc: ^Document, a: ^Annot, page_ctm: Matrix) -> (form, invoke: Matrix, ok: bool) { + if doc == nil || a == nil || a.ap == nil { + return MATRIX_IDENTITY, MATRIX_IDENTITY, false + } + if rect_is_empty(a.rect) { + // A missing /Rect reads as the empty rectangle, and an annotation with + // no area shows nothing however its appearance is scaled. + return MATRIX_IDENTITY, MATRIX_IDENTITY, false + } + + // A /Matrix that is short, non-numeric or infinite arrives as the identity: + // object_to_matrix is the finiteness boundary for a matrix out of a file, + // and the identity is both the format's default and the only safe guess. + mtx := dict_matrix(doc, a.ap.dict, "Matrix") + + place := MATRIX_IDENTITY + if bbox, has_bbox := object_to_rect(doc, dict_get(doc, a.ap.dict, "BBox")); has_bbox { + // Steps 1 and 2. transform_rect maps the four corners and returns + // their bounding box, which is precisely what the two steps ask for. + tb := transform_rect(mtx, bbox) + tw := rect_width(tb) + th := rect_height(tb) + if !(tw > 0) || !(th > 0) { + return MATRIX_IDENTITY, MATRIX_IDENTITY, false + } + + // Step 3. A maps tb onto /Rect: scale each axis by the ratio of the + // sides, then translate tb's low corner onto /Rect's. + sx := rect_width(a.rect) / tw + sy := rect_height(a.rect) / th + place = Matrix{sx, 0, 0, sy, a.rect.x0 - tb.x0 * sx, a.rect.y0 - tb.y0 * sy} + } else { + // /BBox is required of every form XObject and there is nothing to map + // onto the /Rect without it. Drawing the appearance where its own + // coordinates put it is what content.odin does with the same damage, + // and it shows something that is at least in the right units. + add_repair(doc, .Bad_Resource, 0) + } + + // Step 4. + invoke = matrix_mul(place, page_ctm) + form = matrix_mul(mtx, invoke) + if !matrix_is_finite(form) || !matrix_is_finite(invoke) { + return MATRIX_IDENTITY, MATRIX_IDENTITY, false + } + return form, invoke, true +} + +// --------------------------------------------------------------------------- +// Drawing +// --------------------------------------------------------------------------- + +// The synthesised content stream that invokes the appearance, and the names it +// uses. The names are looked up only in the dictionary built below, so they +// cannot collide with anything in the file. +@(private = "file") +ANNOT_INVOKE :: "/Ap Do" +@(private = "file") +ANNOT_INVOKE_OC :: "/OC /Oc BDC /Ap Do EMC" +@(private = "file") +ANNOT_AP_NAME :: "Ap" +@(private = "file") +ANNOT_OC_NAME :: "Oc" + +// annot_run draws one annotation's appearance stream. +// +// `page_ctm` is the same matrix the page's own content stream was run with: +// annotations live in default user space, so the page flip, /Rotate and zoom +// apply to them unchanged. +// +// An annotation with no appearance, or one /F or its subtype hides, draws +// nothing and is not an error -- most pages are mostly those. `opts` is passed +// through to the interpreter, which is how a caller shares one scratch arena +// across a page and how a text-extraction pass asks for switched-off layers. +annot_run :: proc(doc: ^Document, a: ^Annot, dev: ^Device, page_ctm: Matrix, + opts := Content_Options{}) -> Error { + if doc == nil || a == nil { + return .Invalid_Argument + } + if !matrix_is_finite(page_ctm) { + return .Invalid_Argument + } + if a.ap == nil || !a.visible { + return .None + } + + // `form` is the complete appearance matrix; the interpreter arrives at it + // itself by multiplying the form's /Matrix onto the CTM it is invoked + // with, which is why it is `invoke` that is handed over. + _, invoke, ok := annot_matrix(doc, a, page_ctm) + if !ok { + return .None + } + + o := opts + arena: virtual.Arena + if o.allocator.procedure == nil { + // Same contract as run_content: the run frees nothing individually, so + // its allocator has to be one that reclaims in bulk. + if arena_err := virtual.arena_init_growing(&arena); arena_err != nil { + return .Out_Of_Memory + } + o.allocator = virtual.arena_allocator(&arena) + } + defer if opts.allocator.procedure == nil { + virtual.arena_destroy(&arena) + } + + // The appearance is a form XObject, so it is placed the way every form is + // placed: by invoking it with `Do` from a stream whose CTM is the + // placement matrix. + // + // The dictionary this is looked up in holds nothing but the appearance + // itself. An appearance stream that has no /Resources of its own therefore + // resolves no names at all, and that is right twice over: an annotation is + // not page content and inherits nothing from the page (12.5.5), and a + // dictionary that dies with this call must not be reachable from anything + // that outlives it -- a tiling pattern found through the invoking + // resources keeps them on the Document (pattern.odin), which would leave a + // dangling pointer behind every annotation. + res := new_dict(o.allocator, 2) + xobj := new_dict(o.allocator, 1) + dict_set(xobj, ANNOT_AP_NAME, a.ap) + dict_set(res, "XObject", xobj) + + // An annotation may carry its own optional-content membership (12.5.2). + // Wrapping the invocation in a marked-content section hands it to the code + // that already evaluates a page's layers, OCMD policies and all, instead + // of growing a second copy of it here. + src := ANNOT_INVOKE + if oc := dict_get_raw(a.dict, "OC"); oc != nil { + props := new_dict(o.allocator, 1) + dict_set(props, ANNOT_OC_NAME, oc) + dict_set(res, "Properties", props) + src = ANNOT_INVOKE_OC + } + + return run_content(doc, dev, transmute([]byte)src, res, invoke, o) +} + +// annot_run_page draws every drawable annotation of a page, in /Annots order, +// which is the order they paint over one another in. +// +// This is the whole of what a renderer has to call: `render_page` runs the +// page's content stream and then this, with the same CTM. +annot_run_page :: proc(doc: ^Document, page: ^Page, dev: ^Device, page_ctm: Matrix, + opts := Content_Options{}) -> Error { + if doc == nil || page == nil { + return .Invalid_Argument + } + if !matrix_is_finite(page_ctm) { + return .Invalid_Argument + } + + // The annotation list is scratch. When the caller supplied an allocator it + // is theirs -- a per-page arena reclaimed in one call -- and freeing out of + // it here would be at best a no-op and at worst a mismatch. + alloc := opts.allocator.procedure != nil ? opts.allocator : context.allocator + annots, err := annot_load(doc, page, alloc) + if err != .None { + return err + } + defer if opts.allocator.procedure == nil { + delete(annots, alloc) + } + + n := 0 + for &a in annots { + if a.visible && a.ap != nil { + n += 1 + } + } + if n == 0 { + return .None + } + + // The page's annotations share the token budget the page itself gets. A + // file with five hundred annotations all naming one enormous appearance + // stream is otherwise five hundred times the work of the page it is on, + // and it costs three lines to write. + o := opts + budget := opts.max_ops > 0 ? opts.max_ops : DEFAULT_CONTENT_FUEL + o.max_ops = max(budget / n, 1) + + for &a in annots { + // One annotation cannot fail the page. The only error `annot_run` can + // return from here is a scratch arena it could not reserve, and the + // next annotation may well get one. + annot_run(doc, &a, dev, page_ctm, o) + } + return .None +} diff --git a/annot_test.odin b/annot_test.odin new file mode 100644 index 0000000..2f9a2d2 --- /dev/null +++ b/annot_test.odin @@ -0,0 +1,1494 @@ +package pdf + +import "core:fmt" +import "core:log" +import "core:mem/virtual" +import "core:os" +import "core:path/filepath" +import "core:slice" +import "core:strings" +import "core:testing" + +// Annotation tests. +// +// These build whole files rather than Documents by hand: an annotation is a +// dictionary that names an appearance stream that names a resource, and the +// cases worth testing -- an /AS that picks one of two states, a /Rect written +// backwards -- only exist once the references are real. `Pdf_Builder` comes +// from xref_test.odin. +// +// Everything that draws is checked through `An_Dev`, a Device that records the +// calls it is handed. The interpreter's output IS its sequence of device +// calls, so pinning the calls pins the behaviour; the appearance matrix is +// then checked twice over, once against a hand-computed value out of +// `annot_matrix` and once against where the ink actually landed. + +// --------------------------------------------------------------------------- +// The recording device +// --------------------------------------------------------------------------- + +@(private = "file") +An_Kind :: enum u8 { + Fill, + Stroke, + Clip, + Pop_Clip, + Begin_Group, + End_Group, + Begin_Layer, + End_Layer, +} + +@(private = "file") +An_Rec :: struct { + kind: An_Kind, + ctm: Matrix, + // Bounding box of the path in DEVICE space: where the ink went, which is + // what the placement algorithm is ultimately about. + box: Rect, + // The area a group was opened over. + area: Rect, +} + +@(private = "file") +An_Dev :: struct { + dev: Device, + log: [dynamic]An_Rec, +} + +// an_box is the device-space bound of a path. Nothing is cloned: the +// interpreter reuses the path after the call, and a bounding box is all these +// tests ever ask of it. +@(private = "file") +an_box :: proc(p: ^Path, ctm: Matrix) -> (r: Rect) { + if p == nil || len(p.points) == 0 { + return RECT_EMPTY + } + first := transform_point(ctm, p.points[0]) + r = Rect{first.x, first.y, first.x, first.y} + for pt in p.points { + d := transform_point(ctm, pt) + r.x0 = min(r.x0, d.x) + r.y0 = min(r.y0, d.y) + r.x1 = max(r.x1, d.x) + r.y1 = max(r.y1, d.y) + } + return +} + +@(private = "file") +an_add :: proc(dev: ^Device, rec: An_Rec) { + append(&(^An_Dev)(dev.user).log, rec) +} + +@(private = "file") +an_fill :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + an_add(dev, An_Rec{kind = .Fill, ctm = ctm, box = an_box(p, ctm)}) +} + +@(private = "file") +an_stroke :: proc(dev: ^Device, p: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + an_add(dev, An_Rec{kind = .Stroke, ctm = ctm, box = an_box(p, ctm)}) +} + +@(private = "file") +an_clip :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix) { + an_add(dev, An_Rec{kind = .Clip, ctm = ctm, box = an_box(p, ctm)}) +} + +@(private = "file") +an_pop_clip :: proc(dev: ^Device) { + an_add(dev, An_Rec{kind = .Pop_Clip}) +} + +@(private = "file") +an_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + an_add(dev, An_Rec{kind = .Begin_Group, area = area}) +} + +@(private = "file") +an_end_group :: proc(dev: ^Device) { + an_add(dev, An_Rec{kind = .End_Group}) +} + +@(private = "file") +an_begin_layer :: proc(dev: ^Device, name: string) { + an_add(dev, An_Rec{kind = .Begin_Layer}) +} + +@(private = "file") +an_end_layer :: proc(dev: ^Device) { + an_add(dev, An_Rec{kind = .End_Layer}) +} + +@(private = "file") +an_dev_init :: proc(d: ^An_Dev) { + d.log = make([dynamic]An_Rec, 0, 16) + d.dev = Device{ + user = d, + fill_path = an_fill, + stroke_path = an_stroke, + clip_path = an_clip, + pop_clip = an_pop_clip, + begin_group = an_begin_group, + end_group = an_end_group, + begin_layer = an_begin_layer, + end_layer = an_end_layer, + } +} + +@(private = "file") +an_dev_destroy :: proc(d: ^An_Dev) { + delete(d.log) + d.log = nil +} + +@(private = "file") +an_count :: proc(d: ^An_Dev, kind: An_Kind) -> (n: int) { + for e in d.log { + if e.kind == kind { + n += 1 + } + } + return +} + +@(private = "file") +an_nth :: proc(d: ^An_Dev, kind: An_Kind, n := 0) -> ^An_Rec { + seen := 0 + for &e in d.log { + if e.kind != kind { + continue + } + if seen == n { + return &e + } + seen += 1 + } + return nil +} + +// --------------------------------------------------------------------------- +// Assertions +// --------------------------------------------------------------------------- + +@(private = "file") +AN_EPS :: 1e-9 + +@(private = "file") +an_expect_matrix :: proc(t: ^testing.T, got, want: Matrix, what: string, loc := #caller_location) { + ok := abs(got.a - want.a) <= AN_EPS && abs(got.b - want.b) <= AN_EPS && + abs(got.c - want.c) <= AN_EPS && abs(got.d - want.d) <= AN_EPS && + abs(got.e - want.e) <= AN_EPS && abs(got.f - want.f) <= AN_EPS + testing.expectf(t, ok, "%s: matrix %v, wanted %v", what, got, want, loc = loc) +} + +@(private = "file") +an_expect_rect :: proc(t: ^testing.T, got, want: Rect, what: string, tol := AN_EPS, + loc := #caller_location) { + ok := abs(got.x0 - want.x0) <= tol && abs(got.y0 - want.y0) <= tol && + abs(got.x1 - want.x1) <= tol && abs(got.y1 - want.y1) <= tol + testing.expectf(t, ok, "%s: rect %v, wanted %v", what, got, want, loc = loc) +} + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +// The appearance every placement test draws: a rectangle filling the whole of +// its own /BBox, so the ink the device records is exactly the box the +// algorithm mapped onto the /Rect. +@(private = "file") +AN_FILL_BBOX :: "0 0 100 50 re f" + +// an_page writes the one-page skeleton these tests share: catalog 1, page tree +// 2, page 3. Test objects start at 4. +@(private = "file") +an_page :: proc(pb: ^Pdf_Builder, annots: string, catalog_extra := "") { + pb_obj(pb, 1, fmt.tprintf("<< /Type /Catalog /Pages 2 0 R %s >>", catalog_extra)) + pb_obj(pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(pb, 3, fmt.tprintf( + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Annots %s >>", annots)) +} + +@(private = "file") +an_finish :: proc(pb: ^Pdf_Builder) { + at := pb_classic_xref(pb) + pb_str(pb, "trailer\n<< /Size ") + pb_num(pb, len(pb.offs)) + pb_str(pb, " /Root 1 0 R >>\n") + pb_startxref(pb, at) +} + +// an_ap writes an appearance stream object. +@(private = "file") +an_ap :: proc(pb: ^Pdf_Builder, num: u32, dict_head: string, content := AN_FILL_BBOX) { + pb_stream_obj(pb, num, fmt.tprintf("/Type /XObject /Subtype /Form %s", dict_head), + transmute([]byte)content) +} + +@(private = "file") +an_open :: proc(t: ^testing.T, pb: ^Pdf_Builder, loc := #caller_location) -> (^Document, Page) { + an_finish(pb) + doc, err := open_bytes(pb.buf[:]) + testing.expectf(t, err == .None, "open failed: %v", err, loc = loc) + if doc == nil { + return nil, Page{} + } + page, perr := page_get(doc, 0) + testing.expectf(t, perr == .None, "page_get failed: %v", perr, loc = loc) + return doc, page +} + +// an_one loads the page's annotations and returns the first, for the many +// tests that build exactly one. +@(private = "file") +an_one :: proc(t: ^testing.T, doc: ^Document, page: ^Page, loc := #caller_location) -> (Annot, bool) { + annots, err := annot_load(doc, page) + defer delete(annots) + testing.expectf(t, err == .None, "annot_load failed: %v", err, loc = loc) + if len(annots) != 1 { + testing.expectf(t, false, "loaded %d annotations, wanted 1", len(annots), loc = loc) + return Annot{}, false + } + return annots[0], true +} + +// --------------------------------------------------------------------------- +// The appearance matrix -- ISO 32000-2, 12.5.5 +// --------------------------------------------------------------------------- + +// A 100x50 appearance in a 200x100 rectangle at (20, 30): scaled by two in +// both directions and moved to the rectangle's corner. +@(test) +test_annot_matrix_bbox_to_rect :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + + form, invoke, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, mok, "annot_matrix refused a well-formed appearance") + an_expect_matrix(t, form, Matrix{2, 0, 0, 2, 20, 30}, "form matrix") + // No /Matrix on the form, so the two are the same here; the rotating case + // below is where they part company. + an_expect_matrix(t, invoke, Matrix{2, 0, 0, 2, 20, 30}, "invoke matrix") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + + fill := an_nth(&dev, .Fill) + testing.expect(t, fill != nil, "appearance did not draw") + if fill == nil { return } + an_expect_matrix(t, fill.ctm, Matrix{2, 0, 0, 2, 20, 30}, "ctm the fill was handed") + // The appearance fills its own /BBox, so its ink must land on exactly the + // annotation's rectangle. + an_expect_rect(t, fill.box, Rect{20, 30, 220, 130}, "ink") +} + +// A /Matrix that rotates 90 degrees. The transformed appearance box is 50 wide +// and 100 tall where the /BBox is 100 wide and 50 tall, so a placement that +// skipped the bounding-box step would scale both axes wrongly and land the ink +// outside the rectangle. +@(test) +test_annot_matrix_rotated :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 50 100] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50] /Matrix [0 1 -1 0 0 0]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + + // By hand: the corners (0,0) (100,0) (0,50) (100,50) rotate to (0,0) + // (0,100) (-50,0) (-50,100), whose bounding box is [-50 0 0 100] -- 50 by + // 100, the same size as the /Rect. A is therefore a pure translation of + // +50 in x, and the full matrix is /Matrix * A. + form, invoke, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, mok, "annot_matrix refused a rotated appearance") + an_expect_matrix(t, invoke, Matrix{1, 0, 0, 1, 50, 0}, "A") + an_expect_matrix(t, form, Matrix{0, 1, -1, 0, 50, 0}, "form matrix") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + + fill := an_nth(&dev, .Fill) + testing.expect(t, fill != nil, "rotated appearance did not draw") + if fill == nil { return } + an_expect_matrix(t, fill.ctm, Matrix{0, 1, -1, 0, 50, 0}, "ctm the fill was handed") + an_expect_rect(t, fill.box, Rect{0, 0, 50, 100}, "ink") +} + +// 7.9.5: a rectangle's corners may be given in either order. A /Rect written +// backwards must place the appearance exactly as the same rectangle written +// forwards does. +@(test) +test_annot_rect_corners_reversed :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [220 130 20 30] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + an_expect_rect(t, a.rect, Rect{20, 30, 220, 130}, "loaded /Rect") + + form, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, mok, "annot_matrix refused a reversed /Rect") + an_expect_matrix(t, form, Matrix{2, 0, 0, 2, 20, 30}, "form matrix") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + fill := an_nth(&dev, .Fill) + testing.expect(t, fill != nil, "appearance did not draw") + if fill != nil { + an_expect_rect(t, fill.box, Rect{20, 30, 220, 130}, "ink") + } +} + +// The page CTM is the last multiplication of step 4, and a renderer's is +// always a flip: y-up page space to y-down device space. +@(test) +test_annot_page_ctm_applies :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + + flip := Matrix{1, 0, 0, -1, 0, 400} + form, _, mok := annot_matrix(doc, &a, flip) + testing.expect(t, mok, "annot_matrix refused a flipped page") + an_expect_matrix(t, form, Matrix{2, 0, 0, -2, 20, 370}, "form matrix under the page flip") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, flip), Error.None) + fill := an_nth(&dev, .Fill) + testing.expect(t, fill != nil, "appearance did not draw") + if fill != nil { + // The rectangle's y range 30..130 in page space is 270..370 in a + // 400-point-tall device space. + an_expect_rect(t, fill.box, Rect{20, 270, 220, 370}, "ink") + } +} + +// A /BBox with no width has nothing in it to stretch onto the rectangle, and +// step 3's scale factor would divide by zero. +@(test) +test_annot_degenerate_bbox_draws_nothing :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R 6 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 0 50]") + // A /Matrix that collapses an honest /BBox is the same defect written the + // other way round, and the bounding-box step is where it shows up. + pb_obj(&pb, 6, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 7 0 R >> >>") + an_ap(&pb, 7, "/BBox [0 0 100 50] /Matrix [1 0 0 0 0 0]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + annots, err := annot_load(doc, &page) + defer delete(annots) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(annots), 2) + if len(annots) != 2 { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + for &a in annots { + _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, !mok, "annot_matrix accepted a degenerate appearance box") + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + } + testing.expectf(t, len(dev.log) == 0, "a degenerate appearance drew %d calls", len(dev.log)) +} + +// An annotation whose /Rect has no area shows nothing however the appearance +// is scaled -- and a missing /Rect reads as exactly that rectangle. +@(test) +test_annot_empty_rect_draws_nothing :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, !mok, "annot_matrix accepted an annotation with no /Rect") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "an annotation with no /Rect drew %d calls", len(dev.log)) +} + +// /BBox is required and is nonetheless sometimes missing. There is nothing to +// map onto the /Rect then, so the appearance is drawn where its own +// coordinates put it rather than not at all. +@(test) +test_annot_missing_bbox_draws_unplaced :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/Matrix [1 0 0 1 7 9]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + + before := len(doc.repairs) + form, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, mok, "a form with no /BBox should still be placed") + an_expect_matrix(t, form, Matrix{1, 0, 0, 1, 7, 9}, "form matrix with no /BBox") + testing.expect(t, len(doc.repairs) > before, "a missing /BBox recorded no repair") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + fill := an_nth(&dev, .Fill) + testing.expect(t, fill != nil, "a form with no /BBox drew nothing") + if fill != nil { + an_expect_rect(t, fill.box, Rect{7, 9, 107, 59}, "ink") + } + // With no /BBox there is no box to clip to either. + testing.expect_value(t, an_count(&dev, .Clip), 0) +} + +// A scale factor that overflows to infinity is the finiteness boundary this +// file owes the rest of the library: nothing below the Device line may see a +// NaN, and a 1e300-wide rectangle over a 1e-300-wide box produces one. +@(test) +test_annot_matrix_overflow_rejected :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 1e300 1e300] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 1e-300 1e-300]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) + testing.expect(t, !mok, "annot_matrix returned a non-finite placement") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "a non-finite placement drew %d calls", len(dev.log)) +} + +// --------------------------------------------------------------------------- +// Choosing an appearance -- /AP, /N and /AS +// --------------------------------------------------------------------------- + +// The two states a checkbox holds. They are told apart by how many times they +// fill, which is a property of the stream that ran and not of where it landed. +@(private = "file") +AN_STATE_OFF :: "0 0 100 50 re f" +@(private = "file") +AN_STATE_YES :: "0 0 100 50 re f 10 10 20 20 re f" + +@(private = "file") +an_states :: proc(pb: ^Pdf_Builder, as_entry: string) { + an_page(pb, "[4 0 R]") + pb_obj(pb, 4, fmt.tprintf( + "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] %s "+ + "/AP << /N << /Off 5 0 R /Yes 6 0 R >> >> >>", as_entry)) + an_ap(pb, 5, "/BBox [0 0 100 50]", AN_STATE_OFF) + an_ap(pb, 6, "/BBox [0 0 100 50]", AN_STATE_YES) +} + +// /AS is how a checkbox says which of its two looks it is wearing. +@(test) +test_annot_as_selects_state :: proc(t: ^testing.T) { + Case :: struct { state: string, fills: int } + // A composite literal cannot open a `for ... in`: the parser would read its + // brace as the loop body. + cases := []Case{{"Off", 1}, {"Yes", 2}} + for c in cases { + state, want_fills := c.state, c.fills + pb := pb_make() + defer pb_destroy(&pb) + an_states(&pb, fmt.tprintf("/AS /%s", state)) + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { continue } + + a, ok := an_one(t, doc, &page) + if !ok { continue } + testing.expectf(t, a.state == state, "/AS %v selected state %q", state, a.state) + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, an_count(&dev, .Fill) == want_fills, + "/AS %v drew %d fills, wanted %d", state, an_count(&dev, .Fill), want_fills) + } +} + +// The unchecked checkbox: /N holds only /Yes and /AS says /Off. The missing +// entry IS the appearance of the off state, and falling back to the only +// stream present would tick every empty box on the page. +@(test) +test_annot_as_names_missing_state :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] /AS /Off "+ + "/AP << /N << /Yes 5 0 R >> >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + testing.expect(t, a.ap == nil, "/AS /Off selected an appearance that is not there") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "an unchecked box drew %d calls", len(dev.log)) +} + +// With one state and no /AS there is nothing to disambiguate, and viewers all +// draw it. With two there is no way to choose, and drawing the wrong one is +// worse than drawing none. +@(test) +test_annot_no_as_falls_back_to_sole_state :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] "+ + "/AP << /N << /Yes 5 0 R >> >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + testing.expect(t, a.ap != nil, "the only appearance state was not selected") + testing.expectf(t, a.state == "Yes", "state %q, wanted Yes", a.state) + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, an_count(&dev, .Fill), 1) +} + +@(test) +test_annot_no_as_with_two_states_draws_nothing :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_states(&pb, "") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + before := len(doc.repairs) + a, ok := an_one(t, doc, &page) + if !ok { return } + testing.expect(t, a.ap == nil, "an ambiguous appearance was drawn anyway") + testing.expect(t, len(doc.repairs) > before, "an ambiguous appearance recorded no repair") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "an ambiguous appearance drew %d calls", len(dev.log)) +} + +// An annotation with no /AP draws nothing: this library does not synthesise +// appearances. It is still loaded, because a link's rectangle is worth having +// whether or not anything is painted in it. +@(test) +test_annot_without_ap_is_loaded_but_not_drawn :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R 5 0 R]") + // A link with a border a synthesising reader would draw. + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [0 0 100 50] /Border [0 0 3] >>") + // Only /N is page content: a /D and an /R are answers to a pointer. + pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] "+ + "/AP << /D 6 0 R /R 6 0 R >> >>") + an_ap(&pb, 6, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + annots, err := annot_load(doc, &page) + defer delete(annots) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(annots), 2) + if len(annots) != 2 { return } + testing.expect(t, annots[0].ap == nil, "a /Link with no /AP found an appearance") + testing.expect(t, annots[1].ap == nil, "/D was drawn as if it were /N") + testing.expect(t, annots[0].visible, "an annotation with no /AP is still visible content") + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "annotations with no /AP drew %d calls", len(dev.log)) +} + +// --------------------------------------------------------------------------- +// /F flags and subtypes +// --------------------------------------------------------------------------- + +@(test) +test_annot_flags_read :: proc(t: ^testing.T) { + testing.expect_value(t, annot_flags_read(0), Annot_Flags{}) + // The format numbers its bits from 1, so /F 2 is the second bit. + testing.expect_value(t, annot_flags_read(1), Annot_Flags{.Invisible}) + testing.expect_value(t, annot_flags_read(2), Annot_Flags{.Hidden}) + testing.expect_value(t, annot_flags_read(4), Annot_Flags{.Print}) + testing.expect_value(t, annot_flags_read(32), Annot_Flags{.No_View}) + testing.expect_value(t, annot_flags_read(6), Annot_Flags{.Hidden, .Print}) + testing.expect_value(t, annot_flags_read(0x3ff), Annot_Flags{ + .Invisible, .Hidden, .Print, .No_Zoom, .No_Rotate, + .No_View, .Read_Only, .Locked, .Toggle_No_View, .Locked_Contents, + }) + // Bits above the ten the format defines are reserved, and a set holding + // members no enumerator names is not something to hand a caller. + testing.expect_value(t, annot_flags_read(1 << 20), Annot_Flags{}) + testing.expect_value(t, annot_flags_read(-1), annot_flags_read(0x3ff)) +} + +// Hidden and NoView suppress the drawing; Invisible does not, because it only +// governs an annotation the reader has no handler for and an appearance stream +// is a handler. +@(test) +test_annot_flags_suppress_drawing :: proc(t: ^testing.T) { + Case :: struct { f: string, subtype: string, draws: bool } + cases := []Case{ + {"/F 0", "Stamp", true}, + {"/F 1", "Stamp", true}, // Invisible + {"/F 2", "Stamp", false}, // Hidden + {"/F 3", "Stamp", false}, // Invisible + Hidden + {"/F 4", "Stamp", true}, // Print + {"/F 32", "Stamp", false}, // NoView + {"/F 36", "Stamp", false}, // Print + NoView + {"", "Stamp", true}, + // A popup is the window a markup annotation opens, never page content. + {"/F 4", "Popup", false}, + } + for c in cases { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, fmt.tprintf( + "<< /Type /Annot /Subtype /%s /Rect [0 0 100 50] %s /AP << /N 5 0 R >> >>", + c.subtype, c.f)) + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { continue } + + a, ok := an_one(t, doc, &page) + if !ok { continue } + testing.expectf(t, a.visible == c.draws, "%s /%s: visible %v, wanted %v", + c.f, c.subtype, a.visible, c.draws) + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) + drew := an_count(&dev, .Fill) > 0 + testing.expectf(t, drew == c.draws, "%s /%s: drew %v, wanted %v", + c.f, c.subtype, drew, c.draws) + } +} + +// --------------------------------------------------------------------------- +// Loading a page's /Annots +// --------------------------------------------------------------------------- + +@(test) +test_annot_load_page :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + // A null entry and a number are both how a producer leaves a hole in the + // array rather than rewriting it; a direct dictionary is legal and has no + // object number of its own. + an_page(&pb, "[4 0 R null 5 0 R 7 << /Type /Annot /Subtype /Text /Rect [1 2 3 4] >>]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [10 20 110 70] /F 4 >>") + pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] /F 2 "+ + "/AP << /N 6 0 R >> >>") + an_ap(&pb, 6, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + annots, err := annot_load(doc, &page) + defer delete(annots) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(annots), 3) + if len(annots) != 3 { return } + + testing.expectf(t, annots[0].subtype == "Link", "subtype %q", annots[0].subtype) + an_expect_rect(t, annots[0].rect, Rect{10, 20, 110, 70}, "link /Rect") + testing.expect_value(t, annots[0].flags, Annot_Flags{.Print}) + testing.expect_value(t, annots[0].ref, Ref{4, 0}) + testing.expect(t, annots[0].dict != nil, "the annotation dictionary was not kept") + + testing.expectf(t, annots[1].subtype == "Widget", "subtype %q", annots[1].subtype) + testing.expect(t, annots[1].ap != nil, "the widget's appearance was not found") + testing.expect(t, !annots[1].visible, "/F 2 left the widget visible") + testing.expect_value(t, annots[1].ref, Ref{5, 0}) + + // A dictionary written into the array directly has no object number. + testing.expectf(t, annots[2].subtype == "Text", "subtype %q", annots[2].subtype) + testing.expect_value(t, annots[2].ref, Ref{}) +} + +// A page with no /Annots, and one whose /Annots is not an array, both have no +// annotations. Neither is a failure. +@(test) +test_annot_load_no_annots :: proc(t: ^testing.T) { + sources := []string{"", "/Annots 99 0 R", "/Annots << /Bad true >>", "/Annots []"} + for src in sources { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, fmt.tprintf( + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] %s >>", src)) + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { continue } + + annots, err := annot_load(doc, &page) + defer delete(annots) + testing.expectf(t, err == .None, "%q: annot_load failed: %v", src, err) + testing.expectf(t, len(annots) == 0, "%q: loaded %d annotations", src, len(annots)) + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, len(dev.log) == 0, "%q: drew %d calls", src, len(dev.log)) + } +} + +// annot_run_page draws the drawable ones in /Annots order, which is the order +// they paint over one another in, and silently steps over the rest. +@(test) +test_annot_run_page_order :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R 5 0 R 6 0 R 7 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 10 10] /AP << /N 8 0 R >> >>") + pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 10 10] /F 2 /AP << /N 8 0 R >> >>") + pb_obj(&pb, 6, "<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] >>") + pb_obj(&pb, 7, "<< /Type /Annot /Subtype /Stamp /Rect [100 100 110 110] /AP << /N 8 0 R >> >>") + an_ap(&pb, 8, "/BBox [0 0 10 10]", "0 0 10 10 re f") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + + testing.expect_value(t, an_count(&dev, .Fill), 2) + if an_count(&dev, .Fill) != 2 { return } + an_expect_rect(t, an_nth(&dev, .Fill, 0).box, Rect{0, 0, 10, 10}, "first stamp") + an_expect_rect(t, an_nth(&dev, .Fill, 1).box, Rect{100, 100, 110, 110}, "second stamp") +} + +// --------------------------------------------------------------------------- +// What the appearance gets from the interpreter +// --------------------------------------------------------------------------- + +// A form XObject's /BBox clips its contents (8.10.2), and an appearance stream +// is a form XObject. The clip has to be pushed before the paint and popped +// after it, or every annotation after this one draws through it. +@(test) +test_annot_bbox_clips_and_balances :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + // The content spills far outside the /BBox that clips it. + an_ap(&pb, 5, "/BBox [0 0 100 50]", "-500 -500 2000 2000 re f") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + + testing.expect_value(t, len(dev.log), 3) + if len(dev.log) != 3 { return } + testing.expect_value(t, dev.log[0].kind, An_Kind.Clip) + testing.expect_value(t, dev.log[1].kind, An_Kind.Fill) + testing.expect_value(t, dev.log[2].kind, An_Kind.Pop_Clip) + // The clip is the /BBox under the appearance matrix: the annotation's + // rectangle, exactly. + an_expect_rect(t, dev.log[0].box, Rect{20, 30, 220, 130}, "clip") +} + +// An appearance with a transparency group composites as one object, which is +// how a stamp with a drop shadow keeps its alpha from being applied twice. +@(test) +test_annot_transparency_group :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50] /Group << /S /Transparency /I true >>") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + + testing.expect_value(t, an_count(&dev, .Begin_Group), 1) + testing.expect_value(t, an_count(&dev, .End_Group), 1) + testing.expect_value(t, an_count(&dev, .Fill), 1) + grp := an_nth(&dev, .Begin_Group) + if grp != nil { + an_expect_rect(t, grp.area, Rect{20, 30, 220, 130}, "group area") + } +} + +// An appearance that invokes itself is a file that would otherwise render +// until the stack ends. The interpreter's visited set stops it; what this +// pins is that annotations go through that interpreter rather than around it. +@(test) +test_annot_self_referencing_appearance :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") + pb_stream_obj(&pb, 5, + "/Type /XObject /Subtype /Form /BBox [0 0 100 50] "+ + "/Resources << /XObject << /Me 5 0 R >> >>", + transmute([]byte)string("/Me Do 0 0 100 50 re f")) + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, an_count(&dev, .Fill), 1) +} + +// An annotation may belong to an optional-content group (12.5.2). One the +// catalog's default configuration switches off is not drawn -- and a run that +// asks for hidden content, which is what text extraction does, draws it. +@(test) +test_annot_optional_content :: proc(t: ^testing.T) { + build :: proc(pb: ^Pdf_Builder) { + an_page(pb, "[4 0 R]", + "/OCProperties << /OCGs [7 0 R] /D << /OFF [7 0 R] >> >>") + pb_obj(pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] "+ + "/OC 7 0 R /AP << /N 5 0 R >> >>") + an_ap(pb, 5, "/BBox [0 0 100 50]") + pb_obj(pb, 7, "<< /Type /OCG /Name (Watermark) >>") + } + + { + pb := pb_make() + defer pb_destroy(&pb) + build(&pb) + doc, page := an_open(t, &pb) + defer close(doc) + if doc != nil { + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expectf(t, an_count(&dev, .Fill) == 0, + "a switched-off annotation drew %d fills", an_count(&dev, .Fill)) + } + } + { + pb := pb_make() + defer pb_destroy(&pb) + build(&pb) + doc, page := an_open(t, &pb) + defer close(doc) + if doc != nil { + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + opts := Content_Options{ignore_optional_content = true} + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY, opts), + Error.None) + testing.expect_value(t, an_count(&dev, .Fill), 1) + } + } +} + +// An annotation with no /OC must not be wrapped in a layer, or every display +// list gains a pair of calls per annotation. +@(test) +test_annot_without_oc_opens_no_layer :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, an_count(&dev, .Begin_Layer), 0) + testing.expect_value(t, an_count(&dev, .End_Layer), 0) +} + +// --------------------------------------------------------------------------- +// Damage and caller error +// --------------------------------------------------------------------------- + +@(test) +test_annot_invalid_arguments :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") + an_ap(&pb, 5, "/BBox [0 0 100 50]") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + a, ok := an_one(t, doc, &page) + if !ok { return } + + nothing, err := annot_load(nil, &page) + testing.expect_value(t, err, Error.Invalid_Argument) + testing.expect(t, nothing == nil, "annot_load returned a slice for a nil Document") + _, err2 := annot_load(doc, nil) + testing.expect_value(t, err2, Error.Invalid_Argument) + + testing.expect_value(t, annot_run(nil, &a, nil, MATRIX_IDENTITY), Error.Invalid_Argument) + testing.expect_value(t, annot_run(doc, nil, nil, MATRIX_IDENTITY), Error.Invalid_Argument) + testing.expect_value(t, annot_run_page(doc, nil, nil, MATRIX_IDENTITY), Error.Invalid_Argument) + + // A matrix with a NaN or an infinity in it must not reach the geometry. + bad := Matrix{1, 0, 0, 1, 0, max(f64) * 2} + testing.expect_value(t, annot_run(doc, &a, nil, bad), Error.Invalid_Argument) + testing.expect_value(t, annot_run_page(doc, &page, nil, bad), Error.Invalid_Argument) + + // A nil Device runs the whole interpreter and draws nothing, which is how + // the annotation layer is measured and how a caller collects its repairs. + testing.expect_value(t, annot_run(doc, &a, nil, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, annot_run_page(doc, &page, nil, MATRIX_IDENTITY), Error.None) +} + +// Every one of these is a real shape of damage, and none of them may crash, +// hang, or stop the page. +@(test) +test_annot_malformed :: proc(t: ^testing.T) { + Case :: struct { subtype, annot, ap, content: string, draws: bool } + BOX :: "/BBox [0 0 100 50]" + cases := []Case{ + // /AP that is not a dictionary, and /N that is not a stream. + {"/Subtype /Stamp", "/AP 7", BOX, AN_FILL_BBOX, false}, + {"/Subtype /Stamp", "/AP << /N 7 >>", BOX, AN_FILL_BBOX, false}, + {"/Subtype /Stamp", "/AP << /N (text) >>", BOX, AN_FILL_BBOX, false}, + {"/Subtype /Stamp", "/AP << /N << >> >>", BOX, AN_FILL_BBOX, false}, + // /Rect that is not a rectangle, or is not four numbers. Entries that + // are not numbers read as zero, which is an empty rectangle. + {"/Subtype /Stamp", "/Rect (nope) /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, + {"/Subtype /Stamp", "/Rect [0 0 100] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, + {"/Subtype /Stamp", "/Rect [/a /b /c /d] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, + // A /Subtype that is missing entirely, and one nobody has heard of. + // Neither is a reason not to draw what the file says to draw. + {"", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, true}, + {"/Subtype /Hologram", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, true}, + // A /Matrix too short to be one falls back to the identity, and a + // /BBox with its corners the wrong way round is still a box. + {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", + "/BBox [0 0 100 50] /Matrix [1 0 0]", AN_FILL_BBOX, true}, + {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", + "/BBox [100 50 0 0]", AN_FILL_BBOX, true}, + // /F that is not a number reads as no flags at all. + {"/Subtype /Stamp", "/Rect [0 0 100 50] /F (2) /AP << /N 5 0 R >>", + BOX, AN_FILL_BBOX, true}, + // An appearance with no content in it draws nothing and is not damage. + {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, "", false}, + // Operator soup in the appearance: the interpreter steps over it and + // the fill that follows still lands. + {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, + "q 1 0 0 rg /Nope gs ] ] } 0 0 100 50 re f Q", true}, + } + + for c, i in cases { + pb := pb_make() + defer pb_destroy(&pb) + an_page(&pb, "[4 0 R]") + pb_obj(&pb, 4, fmt.tprintf("<< /Type /Annot %s %s >>", c.subtype, c.annot)) + an_ap(&pb, 5, c.ap, c.content) + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { continue } + + dev: An_Dev + an_dev_init(&dev) + defer an_dev_destroy(&dev) + err := annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY) + testing.expectf(t, err == .None, "case %d: annot_run_page returned %v", i, err) + drew := an_count(&dev, .Fill) > 0 + testing.expectf(t, drew == c.draws, "case %d (%s | %s): drew %v, wanted %v", + i, c.annot, c.ap, drew, c.draws) + } +} + +// An /Annots array far longer than any page has is clamped rather than walked. +@(test) +test_annot_load_clamps :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + + refs := make([dynamic]byte) + defer delete(refs) + append(&refs, '[') + for _ in 0 ..< MAX_ANNOTS + 100 { + append(&refs, ..transmute([]byte)string("4 0 R ")) + } + append(&refs, ']') + + an_page(&pb, string(refs[:])) + pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] >>") + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + annots, err := annot_load(doc, &page) + defer delete(annots) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(annots), MAX_ANNOTS) +} + +// --------------------------------------------------------------------------- +// Corpus +// +// Every annotation of every page of every real PDF in the directory named by +// PDF_ODIN_CORPUS, skipped silently when that is unset. There is no correct +// answer to compare a real annotation against, so what is asserted is the +// contract: loading terminates and never fails, every rectangle and matrix +// that comes out is finite, the device stacks balance, and -- the property the +// whole file exists for -- the appearance's own bounding box lands on exactly +// the annotation's /Rect. +// --------------------------------------------------------------------------- + +@(private = "file") +AC_MAX_FILE :: 64 << 20 +@(private = "file") +AC_MAX_DEPTH :: 8 + +@(private = "file") +Ac_Dev :: struct { + dev: Device, + // Live nesting, so an unbalanced pop is caught where it happens. + clip, group, layer: int, + negative: bool, + fills, strokes, images, shades, texts: int, +} + +@(private = "file") +ac_self :: proc(dev: ^Device) -> ^Ac_Dev { return (^Ac_Dev)(dev.user) } + +@(private = "file") +ac_fill :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + ac_self(dev).fills += 1 +} +@(private = "file") +ac_stroke :: proc(dev: ^Device, p: ^Path, s: ^Stroke_State, ctm: Matrix, paint: Paint) { + ac_self(dev).strokes += 1 +} +@(private = "file") +ac_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { + ac_self(dev).texts += len(txt.items) +} +@(private = "file") +ac_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + ac_self(dev).texts += len(txt.items) +} +@(private = "file") +ac_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + ac_self(dev).images += 1 +} +@(private = "file") +ac_shade :: proc(dev: ^Device, sh: ^Shading, ctm: Matrix, paint: Paint) { + ac_self(dev).shades += 1 +} +@(private = "file") +ac_clip :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix) { + ac_self(dev).clip += 1 +} +@(private = "file") +ac_clip_stroke :: proc(dev: ^Device, p: ^Path, s: ^Stroke_State, ctm: Matrix) { + ac_self(dev).clip += 1 +} +@(private = "file") +ac_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + ac_self(dev).clip += 1 +} +@(private = "file") +ac_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + ac_self(dev).clip += 1 +} +@(private = "file") +ac_pop_clip :: proc(dev: ^Device) { + d := ac_self(dev) + d.clip -= 1 + if d.clip < 0 { d.negative = true } +} +@(private = "file") +ac_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + ac_self(dev).group += 1 +} +@(private = "file") +ac_end_group :: proc(dev: ^Device) { + d := ac_self(dev) + d.group -= 1 + if d.group < 0 { d.negative = true } +} +@(private = "file") +ac_begin_layer :: proc(dev: ^Device, name: string) { + ac_self(dev).layer += 1 +} +@(private = "file") +ac_end_layer :: proc(dev: ^Device) { + d := ac_self(dev) + d.layer -= 1 + if d.layer < 0 { d.negative = true } +} + +@(private = "file") +ac_init :: proc(d: ^Ac_Dev) { + d.dev = Device{ + user = d, + fill_path = ac_fill, + stroke_path = ac_stroke, + fill_text = ac_text, + stroke_text = proc(dev: ^Device, txt: ^Text, s: ^Stroke_State, ctm: Matrix, paint: Paint) { + ac_self(dev).texts += len(txt.items) + }, + ignore_text = ac_ignore_text, + fill_image = ac_image, + fill_image_mask = ac_image, + fill_shade = ac_shade, + clip_path = ac_clip, + clip_stroke_path = ac_clip_stroke, + clip_text = ac_clip_text, + clip_image_mask = ac_clip_image_mask, + pop_clip = ac_pop_clip, + begin_group = ac_begin_group, + end_group = ac_end_group, + begin_layer = ac_begin_layer, + end_layer = ac_end_layer, + } +} + +@(private = "file") +ac_corpus_dir :: proc(allocator := context.allocator) -> string { + raw := os.get_env("PDF_ODIN_CORPUS", allocator) + trimmed := strings.trim_space(raw) + out := strings.clone(trimmed, allocator) + delete(raw, allocator) + return out +} + +@(private = "file") +ac_walk :: proc(dir: string, out: ^[dynamic]string, depth := 0) { + if depth > AC_MAX_DEPTH { return } + entries, err := os.read_directory_by_path(dir, -1, context.allocator) + if err != nil { return } + defer os.file_info_slice_delete(entries, context.allocator) + slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { return a.name < b.name }) + for e in entries { + if e.type == .Directory { + ac_walk(e.fullpath, out, depth + 1) + continue + } + if e.type != .Regular || e.size > AC_MAX_FILE { continue } + lower := strings.to_lower(e.name, context.temp_allocator) + if !strings.has_suffix(lower, ".pdf") { continue } + append(out, strings.clone(e.fullpath)) + } +} + +@(test) +test_annot_corpus :: proc(t: ^testing.T) { + dir := ac_corpus_dir() + defer delete(dir) + if dir == "" { + log.info("PDF_ODIN_CORPUS is unset; skipping the annotation corpus test") + return + } + if !os.is_dir(dir) { + log.infof("PDF_ODIN_CORPUS is %q, which is not a directory; skipping", dir) + return + } + + paths := make([dynamic]string) + defer { + for path in paths { delete(path) } + delete(paths) + } + ac_walk(dir, &paths) + if len(paths) == 0 { + log.infof("no PDFs under %q; skipping", dir) + return + } + + // One arena for the whole sweep, reset between pages: the way a renderer + // is meant to drive this. + arena: virtual.Arena + if aerr := virtual.arena_init_growing(&arena); aerr != nil { + testing.expect(t, false, "could not make a scratch arena") + return + } + defer virtual.arena_destroy(&arena) + opts := Content_Options{allocator = virtual.arena_allocator(&arena)} + + total: Ac_Dev + files, pages, n_annots, n_ap, n_drawn, n_hidden, n_states, n_oc, n_placed := 0, 0, 0, 0, 0, 0, 0, 0, 0 + for path in paths { + doc, err := open_file(path) + if err != .None || doc == nil { continue } + defer close(doc) + files += 1 + name := filepath.base(path) + + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if perr != .None { continue } + pages += 1 + + annots, aerr := annot_load(doc, &page, context.allocator) + defer delete(annots) + testing.expectf(t, aerr == .None, "%s page %d: annot_load returned %v", name, i, aerr) + + for &a in annots { + n_annots += 1 + testing.expectf(t, rect_is_finite(a.rect), "%s page %d: /Rect %v is not finite", + name, i, a.rect) + if a.ap == nil { continue } + n_ap += 1 + if a.state != "" { n_states += 1 } + if dict_get_raw(a.dict, "OC") != nil { n_oc += 1 } + if !a.visible { + n_hidden += 1 + continue + } + + // The placement invariant, in page space: the appearance's own + // bounding box, mapped by the matrix the algorithm produced, + // must be the annotation's rectangle. Everything in 12.5.5 is + // in service of this one equality. + form, _, ok := annot_matrix(doc, &a, MATRIX_IDENTITY) + if !ok { continue } + n_drawn += 1 + testing.expectf(t, matrix_is_finite(form), + "%s page %d: appearance matrix %v is not finite", name, i, form) + bbox, has_bbox := object_to_rect(doc, dict_get(doc, a.ap.dict, "BBox")) + if !has_bbox || rect_is_empty(bbox) { continue } + got := transform_rect(form, bbox) + tol := 1e-6 * max(1, abs(a.rect.x0), abs(a.rect.y0), + abs(a.rect.x1), abs(a.rect.y1)) + placed := abs(got.x0 - a.rect.x0) <= tol && abs(got.y0 - a.rect.y0) <= tol && + abs(got.x1 - a.rect.x1) <= tol && abs(got.y1 - a.rect.y1) <= tol + testing.expectf(t, placed, "%s page %d: appearance landed on %v, not on /Rect %v", + name, i, got, a.rect) + n_placed += 1 + } + + dev: Ac_Dev + ac_init(&dev) + flip := Matrix{1, 0, 0, -1, -page.media_box.x0, page.media_box.y1} + rerr := annot_run_page(doc, &page, &dev.dev, flip, opts) + testing.expectf(t, rerr == .None, "%s page %d: annot_run_page returned %v", name, i, rerr) + testing.expectf(t, dev.clip == 0, "%s page %d: %d clips left open", name, i, dev.clip) + testing.expectf(t, dev.group == 0, "%s page %d: %d groups left open", name, i, dev.group) + testing.expectf(t, dev.layer == 0, "%s page %d: %d layers left open", name, i, dev.layer) + testing.expectf(t, !dev.negative, "%s page %d: a device stack went negative", name, i) + + total.fills += dev.fills + total.strokes += dev.strokes + total.texts += dev.texts + total.images += dev.images + total.shades += dev.shades + free_all(virtual.arena_allocator(&arena)) + } + } + + log.infof("annotation corpus: %d files, %d pages, %d annotations, %d with an appearance "+ + "(%d state-keyed, %d optional-content, %d hidden), %d placed on their /Rect; "+ + "drew %d fills, %d strokes, %d glyphs, %d images, %d shadings", + files, pages, n_annots, n_ap, n_states, n_oc, n_hidden, n_placed, + total.fills, total.strokes, total.texts, total.images, total.shades) + testing.expect(t, pages > 0, "no page of the corpus could be read") + if n_ap == 0 { + // Worth saying out loud rather than passing quietly: a corpus of + // exports from word processors is all /Link annotations with no + // appearance at all, so this sweep has exercised the loading path and + // the "no /AP, draw nothing" path and none of the drawing. The unit + // tests above are then the only thing holding the placement algorithm. + log.warn("no annotation in this corpus has an appearance stream; " + + "the drawing path was not exercised") + } else { + testing.expect(t, n_drawn > 0, "every appearance in the corpus was refused a placement") + testing.expect(t, n_placed > 0, "no appearance in the corpus landed on its /Rect") + testing.expect(t, total.fills + total.strokes + total.texts + total.images + total.shades > 0, + "appearances were found and none of them painted anything") + } +} + +// --------------------------------------------------------------------------- +// A page is not one run +// --------------------------------------------------------------------------- + +// Every annotation's appearance stream is its own `run_content`, so anything +// bounded per RUN is really bounded per run TIMES MAX_ANNOTS. The soft-mask +// budget is the one that costs memory -- a Soft_Mask outlives the operator +// that installed it -- so it is counted on the state the whole page shares, +// and hitting it here is what proves the annotations are counted with the +// page's content rather than each starting fresh. +@(test) +test_annot_soft_masks_share_the_page_budget :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + + N :: MAX_SOFT_MASKS + 64 + refs := strings.builder_make() + defer strings.builder_destroy(&refs) + strings.write_string(&refs, "[") + for i in 0 ..< N { + fmt.sbprintf(&refs, "%d 0 R ", 8 + i) + } + strings.write_string(&refs, "]") + + an_page(&pb, strings.to_string(refs)) + // 4: the appearance, which installs one soft mask. 5: the ExtGState that + // names it. 6: the mask's group. + pb_stream_obj(&pb, 4, + "/Type /XObject /Subtype /Form /BBox [0 0 100 50] " + + "/Resources << /ExtGState << /S 5 0 R >> >>", + transmute([]byte)string("/S gs 0 0 100 50 re f")) + pb_obj(&pb, 5, "<< /Type /ExtGState /SMask << /S /Luminosity /G 6 0 R >> >>") + pb_stream_obj(&pb, 6, + "/Type /XObject /Subtype /Form /BBox [0 0 20 20] " + + "/Group << /S /Transparency /CS /DeviceGray >>", + transmute([]byte)string("0 0 20 20 re f")) + pb_obj(&pb, 7, "<< >>") + // Each annotation sits somewhere else, so each install is a mask under a + // CTM of its own that the "same ExtGState, same CTM" cache cannot answer. + for i in 0 ..< N { + body := fmt.tprintf( + "<< /Type /Annot /Subtype /Widget /F 4 /Rect [%d 0 %d 50] /AP << /N 4 0 R >> >>", + i % 300, i % 300 + 100) + pb_obj(&pb, u32(8 + i), body) + } + + doc, page := an_open(t, &pb) + defer close(doc) + if doc == nil { return } + + repairs := len(doc.repairs) + bmp, err := render_page(&page, Render_Options{}) + defer bitmap_destroy(&bmp) + testing.expect_value(t, err, Error.None) + testing.expect(t, len(doc.repairs) > repairs, + "more annotations than MAX_SOFT_MASKS must exhaust the PAGE's budget") + + // And nothing the render built stays behind: a viewer re-renders the same + // page at every zoom step, and every one of those masks used to be a + // Document allocation that only `close` reclaimed. + before := document_memory_used(doc) + bmp2, err2 := render_page(&page, Render_Options{}) + defer bitmap_destroy(&bmp2) + testing.expect_value(t, err2, Error.None) + growth := document_memory_used(doc) - before + testing.expectf(t, growth < 1 << 16, "the second render added %d bytes", growth) +} diff --git a/bitmap.odin b/bitmap.odin new file mode 100644 index 0000000..8cc8cb2 --- /dev/null +++ b/bitmap.odin @@ -0,0 +1,187 @@ +package pdf + +import "core:mem" + +// Bitmap is an 8-bit-per-component raster image, the thing `render_page` +// hands back and the thing an image XObject decodes into. +// +// Alpha is NOT premultiplied in a Bitmap that leaves the library: that is what +// a caller uploading to a texture expects, and premultiplying would silently +// change colours for anyone who blends with the usual +// (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) setup. Compositing internally does use +// premultiplied values; `bitmap_unpremultiply` is where the conversion back +// happens, and `premultiplied` records which state a bitmap is in. +Bitmap :: struct { + width: int, + height: int, + // Components per pixel: 1 = grey, 2 = grey+alpha, 3 = RGB, 4 = RGBA. + n: int, + // Bytes per row. Rows are contiguous by default but a bitmap can be a + // window onto a larger buffer, so never assume width*n. + stride: int, + pixels: []u8, + premultiplied: bool, + // The allocator `pixels` came from, so `bitmap_destroy` needs no argument. + allocator: mem.Allocator, + // False when `pixels` is borrowed and must not be freed. + owns_pixels: bool, +} + +// bitmap_make allocates a zeroed bitmap. `n` must be 1, 2, 3 or 4. +// +// The returned Bitmap owns its pixels; free it with `bitmap_destroy`. +bitmap_make :: proc(width, height, n: int, allocator := context.allocator) -> (bmp: Bitmap, err: Error) { + if width <= 0 || height <= 0 || n < 1 || n > 4 { + return {}, .Invalid_Argument + } + // Guard the multiplication itself, not just the product: on a 32-bit + // target width*height*n can wrap before the comparison ever runs. + if width > MAX_BITMAP_PIXELS || height > MAX_BITMAP_PIXELS { + return {}, .Bitmap_Too_Large + } + if width * height > MAX_BITMAP_PIXELS { + return {}, .Bitmap_Too_Large + } + + stride := width * n + pixels, alloc_err := make([]u8, stride * height, allocator) + if alloc_err != nil { + return {}, .Out_Of_Memory + } + return Bitmap{ + width = width, + height = height, + n = n, + stride = stride, + pixels = pixels, + allocator = allocator, + owns_pixels = true, + }, .None +} + +bitmap_destroy :: proc(bmp: ^Bitmap) { + if bmp == nil { return } + if bmp.owns_pixels && bmp.pixels != nil { + delete(bmp.pixels, bmp.allocator) + } + bmp^ = {} +} + +// bitmap_row returns row `y`, or nil if it is out of range. +bitmap_row :: proc(bmp: ^Bitmap, y: int) -> []u8 { + if bmp == nil || y < 0 || y >= bmp.height { return nil } + off := y * bmp.stride + if off + bmp.stride > len(bmp.pixels) { return nil } + return bmp.pixels[off:off + bmp.stride] +} + +bitmap_pixel_offset :: proc(bmp: ^Bitmap, x, y: int) -> (int, bool) { + if bmp == nil || x < 0 || y < 0 || x >= bmp.width || y >= bmp.height { + return 0, false + } + off := y * bmp.stride + x * bmp.n + if off + bmp.n > len(bmp.pixels) { return 0, false } + return off, true +} + +// bitmap_clear fills every pixel with `value` in all components. +bitmap_clear :: proc(bmp: ^Bitmap, value: u8 = 0) { + if bmp == nil { return } + for &b in bmp.pixels { b = value } +} + +// bitmap_fill paints a solid colour. Components beyond `bmp.n` are ignored, +// and a missing alpha component means the colour is written opaque. +bitmap_fill :: proc(bmp: ^Bitmap, color: [4]u8) { + if bmp == nil { return } + n := bmp.n + for y in 0 ..< bmp.height { + row := bitmap_row(bmp, y) + for x := 0; x + n <= len(row); x += n { + for c in 0 ..< n { + row[x + c] = color[c] + } + } + } +} + +// bitmap_unpremultiply converts a premultiplied bitmap to straight alpha in +// place. A no-op on a bitmap that is already straight, or has no alpha. +bitmap_unpremultiply :: proc(bmp: ^Bitmap) { + if bmp == nil || !bmp.premultiplied { return } + if bmp.n != 2 && bmp.n != 4 { + bmp.premultiplied = false + return + } + ai := bmp.n - 1 + for y in 0 ..< bmp.height { + row := bitmap_row(bmp, y) + for x := 0; x + bmp.n <= len(row); x += bmp.n { + a := row[x + ai] + if a == 0 { + // Fully transparent: colour is unrecoverable, and leaving the + // multiplied zeros makes bilinear filtering pull black halos + // into the edges. Zero is the least-bad choice and matches + // what every other renderer does. + for c in 0 ..< ai { row[x + c] = 0 } + continue + } + if a == 255 { continue } + for c in 0 ..< ai { + v := (int(row[x + c]) * 255 + int(a) / 2) / int(a) + row[x + c] = u8(min(v, 255)) + } + } + } + bmp.premultiplied = false +} + +// bitmap_premultiply converts straight alpha to premultiplied in place. +bitmap_premultiply :: proc(bmp: ^Bitmap) { + if bmp == nil || bmp.premultiplied { return } + if bmp.n != 2 && bmp.n != 4 { + bmp.premultiplied = true + return + } + ai := bmp.n - 1 + for y in 0 ..< bmp.height { + row := bitmap_row(bmp, y) + for x := 0; x + bmp.n <= len(row); x += bmp.n { + a := int(row[x + ai]) + if a == 255 { continue } + for c in 0 ..< ai { + row[x + c] = u8((int(row[x + c]) * a + 127) / 255) + } + } + } + bmp.premultiplied = true +} + +// bitmap_to_rgba returns an RGBA8 copy, expanding grey and adding an opaque +// alpha channel as needed. Callers uploading to a GPU texture want this. +// The result is allocated with `allocator` and owned by the caller. +bitmap_to_rgba :: proc(bmp: ^Bitmap, allocator := context.allocator) -> (out: Bitmap, err: Error) { + if bmp == nil { return {}, .Invalid_Argument } + out = bitmap_make(bmp.width, bmp.height, 4, allocator) or_return + for y in 0 ..< bmp.height { + src := bitmap_row(bmp, y) + dst := bitmap_row(&out, y) + for x in 0 ..< bmp.width { + si := x * bmp.n + di := x * 4 + if si + bmp.n > len(src) || di + 4 > len(dst) { break } + switch bmp.n { + case 1: + dst[di] = src[si]; dst[di + 1] = src[si]; dst[di + 2] = src[si]; dst[di + 3] = 255 + case 2: + dst[di] = src[si]; dst[di + 1] = src[si]; dst[di + 2] = src[si]; dst[di + 3] = src[si + 1] + case 3: + dst[di] = src[si]; dst[di + 1] = src[si + 1]; dst[di + 2] = src[si + 2]; dst[di + 3] = 255 + case 4: + dst[di] = src[si]; dst[di + 1] = src[si + 1]; dst[di + 2] = src[si + 2]; dst[di + 3] = src[si + 3] + } + } + } + out.premultiplied = bmp.premultiplied + return out, .None +} diff --git a/bitmap_test.odin b/bitmap_test.odin new file mode 100644 index 0000000..55efe1d --- /dev/null +++ b/bitmap_test.odin @@ -0,0 +1,128 @@ +package pdf + +import "core:testing" + +@(test) +test_bitmap_make_and_bounds :: proc(t: ^testing.T) { + bmp, err := bitmap_make(4, 3, 4) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expect_value(t, bmp.stride, 16) + testing.expect_value(t, len(bmp.pixels), 48) + testing.expect_value(t, len(bitmap_row(&bmp, 0)), 16) + testing.expect_value(t, len(bitmap_row(&bmp, 2)), 16) + testing.expect_value(t, len(bitmap_row(&bmp, 3)), 0) // out of range + testing.expect_value(t, len(bitmap_row(&bmp, -1)), 0) + + off, ok := bitmap_pixel_offset(&bmp, 3, 2) + testing.expect(t, ok, "in-range pixel") + testing.expect_value(t, off, 2 * 16 + 3 * 4) + _, ok2 := bitmap_pixel_offset(&bmp, 4, 0) + testing.expect(t, !ok2, "x == width is out of range") +} + +@(test) +test_bitmap_rejects_bad_sizes :: proc(t: ^testing.T) { + _, e1 := bitmap_make(0, 10, 4) + testing.expect_value(t, e1, Error.Invalid_Argument) + _, e2 := bitmap_make(10, 10, 5) + testing.expect_value(t, e2, Error.Invalid_Argument) + _, e3 := bitmap_make(-1, 10, 4) + testing.expect_value(t, e3, Error.Invalid_Argument) + // A size whose product overflows must be refused, not wrapped. + _, e4 := bitmap_make(1 << 20, 1 << 20, 4) + testing.expect_value(t, e4, Error.Bitmap_Too_Large) +} + +@(test) +test_bitmap_fill_and_clear :: proc(t: ^testing.T) { + bmp, _ := bitmap_make(2, 2, 4) + defer bitmap_destroy(&bmp) + + bitmap_fill(&bmp, {1, 2, 3, 4}) + testing.expect_value(t, bmp.pixels[0], 1) + testing.expect_value(t, bmp.pixels[3], 4) + testing.expect_value(t, bmp.pixels[len(bmp.pixels) - 1], 4) + + bitmap_clear(&bmp, 9) + for b in bmp.pixels { + testing.expect_value(t, b, 9) + } +} + +// Premultiply then unpremultiply must return the original colour for every +// alpha where the information survives the round trip. +@(test) +test_bitmap_premultiply_round_trip :: proc(t: ^testing.T) { + bmp, _ := bitmap_make(1, 1, 4) + defer bitmap_destroy(&bmp) + + for a in ([]u8{255, 200, 128, 64, 1}) { + bmp.pixels[0] = 200 + bmp.pixels[1] = 100 + bmp.pixels[2] = 50 + bmp.pixels[3] = a + bmp.premultiplied = false + + bitmap_premultiply(&bmp) + testing.expect(t, bmp.premultiplied, "should be marked premultiplied") + testing.expect(t, bmp.pixels[0] <= 200, "premultiplied value cannot exceed the original") + + bitmap_unpremultiply(&bmp) + testing.expect(t, !bmp.premultiplied, "should be marked straight") + // Rounding through 8 bits at low alpha is lossy; the tolerance scales + // with 255/alpha, which is the quantisation step of the round trip. + tol := int(255 / max(int(a), 1)) + 1 + testing.expect(t, abs(int(bmp.pixels[0]) - 200) <= tol, "red survives the round trip") + testing.expect(t, abs(int(bmp.pixels[1]) - 100) <= tol, "green survives the round trip") + testing.expect_value(t, bmp.pixels[3], a) + } +} + +// Fully transparent pixels lose their colour, deliberately: leaving the +// multiplied zeros in place makes bilinear filtering drag black into edges. +@(test) +test_bitmap_unpremultiply_zero_alpha :: proc(t: ^testing.T) { + bmp, _ := bitmap_make(1, 1, 4) + defer bitmap_destroy(&bmp) + + bmp.pixels[0] = 77 + bmp.pixels[1] = 88 + bmp.pixels[2] = 99 + bmp.pixels[3] = 0 + bmp.premultiplied = true + bitmap_unpremultiply(&bmp) + testing.expect_value(t, bmp.pixels[0], 0) + testing.expect_value(t, bmp.pixels[1], 0) + testing.expect_value(t, bmp.pixels[2], 0) + testing.expect_value(t, bmp.pixels[3], 0) +} + +@(test) +test_bitmap_to_rgba_expands :: proc(t: ^testing.T) { + grey, _ := bitmap_make(2, 1, 1) + defer bitmap_destroy(&grey) + grey.pixels[0] = 10 + grey.pixels[1] = 20 + + rgba, err := bitmap_to_rgba(&grey) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&rgba) + + testing.expect_value(t, rgba.n, 4) + testing.expect_value(t, rgba.pixels[0], 10) + testing.expect_value(t, rgba.pixels[1], 10) + testing.expect_value(t, rgba.pixels[2], 10) + testing.expect_value(t, rgba.pixels[3], 255) + testing.expect_value(t, rgba.pixels[4], 20) + testing.expect_value(t, rgba.pixels[7], 255) +} + +@(test) +test_bitmap_destroy_is_idempotent :: proc(t: ^testing.T) { + bmp, _ := bitmap_make(2, 2, 3) + bitmap_destroy(&bmp) + bitmap_destroy(&bmp) + testing.expect_value(t, len(bmp.pixels), 0) +} diff --git a/blend.odin b/blend.odin new file mode 100644 index 0000000..8d25978 --- /dev/null +++ b/blend.odin @@ -0,0 +1,738 @@ +package pdf + +import "core:math" + +// Blend modes and compositing (ISO 32000-2, 11.3.5 and 11.3.6). +// +// --------------------------------------------------------------------------- +// Which representation each procedure expects +// +// Mixing premultiplied and straight alpha is the classic source of dark +// fringes, so the rule here is stated once and never bent: +// +// * Every `Bitmap` this file reads or writes -- `dst` and `src` in +// `blend_span`, `blend_span_rgba8` and `blend_bitmap` -- is PREMULTIPLIED. +// That is the internal representation the whole renderer composites in. +// `bitmap_unpremultiply` is called once, at the very end, on the bitmap +// that leaves the library. +// * Every colour passed as an `f64` -- `rgba` in `blend_span`, the arguments +// to `blend_separable` and `blend_non_separable`, `backdrop` in +// `mask_from_bitmap` -- is STRAIGHT, in 0..1, with alpha separate. That is +// what a `Paint` carries (`paint.rgb`, `paint.alpha`) and what the spec's +// blend formulas are written in terms of. +// * `blend_span_rgba8`'s `src` bytes are STRAIGHT RGBA8, because that is what +// `image_decode` produces. +// * Coverage and mask bytes are plain 0..255 fractions; they multiply into +// the source alpha and are never premultiplied by anything. +// +// A freshly zeroed bitmap is simultaneously valid as straight and as +// premultiplied data, which is why the raster device can start from +// `bitmap_make` without a conversion. Compositing into a bitmap sets its +// `premultiplied` flag, so a device that forgets to set it cannot end up +// handing straight-labelled premultiplied pixels to the caller. A bitmap that +// already holds STRAIGHT content must be run through `bitmap_premultiply` +// before anything here touches it. +// +// The inner loops are f64 throughout. There is deliberately no separate +// integer fast path for Normal: two paths that round differently would +// disagree by a least-significant bit depending on which one a given span took, +// and a seam down the middle of a fill is far worse than the cycles saved. +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// The separable blend functions (11.3.5.2) +// +// All arguments and results are in 0..1. `cb` is the backdrop colour, `cs` the +// source colour, one component at a time. +// --------------------------------------------------------------------------- + +// blend_separable evaluates B(cb, cs) for one component. +// +// The four non-separable modes cannot be evaluated one component at a time; +// passing one here returns `cs`, i.e. Normal. Callers dispatch on +// `blend_mode_is_separable` rather than relying on that, but a mode that +// arrives here by accident must degrade to the identity rather than to noise. +@(private) +blend_separable :: proc(mode: Blend_Mode, cb, cs: f64) -> f64 { + switch mode { + case .Normal: + return cs + case .Multiply: + return cb * cs + case .Screen: + return cb + cs - cb * cs + case .Overlay: + // Defined as HardLight with the operands exchanged. + return blend_hard_light(cs, cb) + case .Darken: + return min(cb, cs) + case .Lighten: + return max(cb, cs) + case .Color_Dodge: + // ISO 32000-2 rewrote this case split; ISO 32000-1 said only + // "cs < 1 ? min(1, cb/(1-cs)) : 1", which lights a black backdrop up + // to white when the source is white. Testing cb == 0 first is what + // stops that, and it is what every current implementation does. + if cb <= 0 { return 0 } + if cs >= 1 { return 1 } + return min(1, cb / (1 - cs)) + case .Color_Burn: + // Mirror image of ColorDodge: cb == 1 is tested before cs == 0, so a + // white backdrop stays white instead of being burned to black. + if cb >= 1 { return 1 } + if cs <= 0 { return 0 } + return 1 - min(1, (1 - cb) / cs) + case .Hard_Light: + return blend_hard_light(cb, cs) + case .Soft_Light: + return blend_soft_light(cb, cs) + case .Difference: + return abs(cb - cs) + case .Exclusion: + return cb + cs - 2 * cb * cs + case .Hue, .Saturation, .Color, .Luminosity: + return cs + } + return cs +} + +@(private) +blend_hard_light :: proc(cb, cs: f64) -> f64 { + if cs <= 0.5 { + return cb * (2 * cs) // Multiply(cb, 2*cs) + } + d := 2 * cs - 1 + return cb + d - cb * d // Screen(cb, 2*cs - 1) +} + +// blend_soft_light is the mode implementations most often get wrong: D(x) is +// piecewise, and the low branch is a cubic, not sqrt(x). The two agree at +// x = 0.25 (both give 0.5); they are 0.05 apart at x = 0.05, which is dark +// shadow detail and plainly visible. +@(private) +blend_soft_light :: proc(cb, cs: f64) -> f64 { + if cs <= 0.5 { + return cb - (1 - 2 * cs) * cb * (1 - cb) + } + return cb + (2 * cs - 1) * (blend_soft_light_d(cb) - cb) +} + +@(private) +blend_soft_light_d :: proc(x: f64) -> f64 { + if x <= 0.25 { + return ((16 * x - 12) * x + 4) * x + } + if x <= 0 { return 0 } + return math.sqrt(x) +} + +// --------------------------------------------------------------------------- +// The non-separable blend functions (11.3.5.3) +// +// These act on all three components at once, through the spec's Lum, ClipColor, +// SetLum, Sat and SetSat helpers. They are defined for RGB; a greyscale +// destination is handled by replicating its single component into all three, +// which is exactly what the formulas reduce to anyway. +// --------------------------------------------------------------------------- + +@(private) +blend_lum :: proc(c: [3]f64) -> f64 { + return 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2] +} + +// blend_clip_color pulls a colour back inside the unit cube without changing +// its luminosity, by scaling it towards the grey axis. +// +// L, n and x are all taken from the ORIGINAL colour: the second test uses the +// pre-clip maximum, not the maximum after the first branch has run. Recomputing +// them between the branches is a real and easy mistake, and it changes the +// result whenever both branches fire. +@(private) +blend_clip_color :: proc(c: [3]f64) -> [3]f64 { + r := c + l := blend_lum(c) + n := min(c[0], c[1], c[2]) + x := max(c[0], c[1], c[2]) + if n < 0 { + d := l - n + // d == 0 needs all three components equal to l, and then n < 0 means + // the colour is a uniform negative grey. Nothing can be scaled towards + // the axis it is already on, so clamp instead of dividing by zero. + if d <= 0 { return [3]f64{0, 0, 0} } + for i in 0 ..< 3 { r[i] = l + ((r[i] - l) * l) / d } + } + if x > 1 { + d := x - l + if d <= 0 { return [3]f64{1, 1, 1} } + for i in 0 ..< 3 { r[i] = l + ((r[i] - l) * (1 - l)) / d } + } + return r +} + +@(private) +blend_set_lum :: proc(c: [3]f64, l: f64) -> [3]f64 { + d := l - blend_lum(c) + return blend_clip_color([3]f64{c[0] + d, c[1] + d, c[2] + d}) +} + +@(private) +blend_sat :: proc(c: [3]f64) -> f64 { + return max(c[0], c[1], c[2]) - min(c[0], c[1], c[2]) +} + +// blend_set_sat rescales a colour to the given saturation, keeping the relative +// ordering of its components and pinning the smallest to zero. The luminosity +// it destroys is put back by the SetLum that always follows it. +@(private) +blend_set_sat :: proc(c: [3]f64, s: f64) -> [3]f64 { + // Sort the three component indices, so the result can be written back to + // the component it came from. + lo, mid, hi := 0, 1, 2 + if c[lo] > c[mid] { lo, mid = mid, lo } + if c[mid] > c[hi] { mid, hi = hi, mid } + if c[lo] > c[mid] { lo, mid = mid, lo } + + r: [3]f64 + if c[hi] > c[lo] { + r[mid] = ((c[mid] - c[lo]) * s) / (c[hi] - c[lo]) + r[hi] = s + } else { + r[mid] = 0 + r[hi] = 0 + } + r[lo] = 0 + return r +} + +// blend_non_separable evaluates B(Cb, Cs) for Hue, Saturation, Color and +// Luminosity. A separable mode passed here is applied component by component, +// so this is a complete B() for any mode. +@(private) +blend_non_separable :: proc(mode: Blend_Mode, cb, cs: [3]f64) -> [3]f64 { + #partial switch mode { + case .Hue: + return blend_set_lum(blend_set_sat(cs, blend_sat(cb)), blend_lum(cb)) + case .Saturation: + return blend_set_lum(blend_set_sat(cb, blend_sat(cs)), blend_lum(cb)) + case .Color: + return blend_set_lum(cs, blend_lum(cb)) + case .Luminosity: + return blend_set_lum(cb, blend_lum(cs)) + } + return [3]f64{ + blend_separable(mode, cb[0], cs[0]), + blend_separable(mode, cb[1], cs[1]), + blend_separable(mode, cb[2], cs[2]), + } +} + +// --------------------------------------------------------------------------- +// The compositing formula (11.3.6) +// --------------------------------------------------------------------------- + +// blend_over composites one source colour onto one backdrop colour. +// +// Both inputs are STRAIGHT: `cb`/`ab` is the backdrop, `cs`/`as_` the source, +// all in 0..1. The result is PREMULTIPLIED colour plus its alpha, because that +// is the form the destination bitmap stores and it is the form in which the +// union formula +// +// co = as*(1-ab)*Cs + as*ab*B(Cb,Cs) + (1-as)*ab*Cb +// ao = as + ab*(1-as) +// +// is actually written. This is the single reference implementation: every span +// and bitmap loop below goes through it, so there is no second rounding path to +// drift away from. +@(private) +blend_over :: proc(cb: [3]f64, ab: f64, cs: [3]f64, as_: f64, mode: Blend_Mode) -> (co: [3]f64, ao: f64) { + b := cs + // With no backdrop there is nothing to blend with: the B() term is + // multiplied by ab, so skipping it is exact, not an approximation. + if mode != .Normal && ab > 0 { + if blend_mode_is_separable(mode) { + b[0] = blend_separable(mode, cb[0], cs[0]) + b[1] = blend_separable(mode, cb[1], cs[1]) + b[2] = blend_separable(mode, cb[2], cs[2]) + } else { + b = blend_non_separable(mode, cb, cs) + } + } + for i in 0 ..< 3 { + co[i] = as_ * ((1 - ab) * cs[i] + ab * b[i]) + (1 - as_) * ab * cb[i] + } + ao = as_ + ab * (1 - as_) + return +} + +// --------------------------------------------------------------------------- +// Pixel access +// --------------------------------------------------------------------------- + +// blend_quantize turns a 0..1 value into a byte, rounding half up. NaN maps to +// 0: `!(v > 0)` is true for NaN, which is the point of writing the test that +// way round. +@(private) +blend_quantize :: proc(v: f64) -> u8 { + if !(v > 0) { return 0 } + if v >= 1 { return 255 } + return u8(v * 255 + 0.5) +} + +// blend_clamp01 forces a colour or alpha coming from a content stream into +// 0..1. NaN maps to 0 and the infinities to the end they point at, which is +// what clamping means for a value that merely overshot; `!(v > 0)` is the test +// that catches NaN, since every comparison with a NaN is false. +@(private) +blend_clamp01 :: proc(v: f64) -> f64 { + if !(v > 0) { return 0 } + if !(v < 1) { return 1 } + return v +} + +// blend_has_alpha reports whether a component count carries an alpha channel: +// 2 is grey+alpha and 4 is RGBA, while 1 and 3 are opaque. +@(private) +blend_has_alpha :: proc(n: int) -> bool { + return n == 2 || n == 4 +} + +// blend_read_straight unpacks one PREMULTIPLIED pixel into a straight RGB +// triple and its alpha. A grey pixel is replicated across all three +// components; a pixel with no alpha channel reads as fully opaque. +@(private) +blend_read_straight :: proc(row: []u8, off, n: int) -> (c: [3]f64, a: f64) { + if blend_has_alpha(n) { + a8 := row[off + n - 1] + a = f64(a8) / 255 + if a8 == 0 { + // The colour of a fully transparent pixel is not recoverable, and + // it is multiplied by ab = 0 everywhere it is used. + return [3]f64{0, 0, 0}, 0 + } + inv := 1 / f64(a8) + if n == 2 { + g := f64(row[off]) * inv + return [3]f64{g, g, g}, a + } + return [3]f64{f64(row[off]) * inv, f64(row[off + 1]) * inv, f64(row[off + 2]) * inv}, a + } + if n == 1 { + g := f64(row[off]) / 255 + return [3]f64{g, g, g}, 1 + } + return [3]f64{f64(row[off]) / 255, f64(row[off + 1]) / 255, f64(row[off + 2]) / 255}, 1 +} + +// blend_write_premul stores a PREMULTIPLIED result. For a greyscale +// destination the three components are equal by construction -- the source was +// reduced to grey before blending -- so component 0 is the whole answer. +@(private) +blend_write_premul :: proc(row: []u8, off, n: int, co: [3]f64, ao: f64) { + if n <= 2 { + row[off] = blend_quantize(co[0]) + } else { + row[off] = blend_quantize(co[0]) + row[off + 1] = blend_quantize(co[1]) + row[off + 2] = blend_quantize(co[2]) + } + if blend_has_alpha(n) { + row[off + n - 1] = blend_quantize(ao) + } +} + +// blend_source_for reduces a straight RGB source to the destination's colour +// model. A greyscale destination gets the luminosity, which is the same +// weighting the non-separable modes use, so grey output stays consistent with +// what those modes would have produced. +@(private) +blend_source_for :: proc(n: int, rgb: [3]f64) -> [3]f64 { + if n <= 2 { + g := blend_lum(rgb) + return [3]f64{g, g, g} + } + return rgb +} + +// --------------------------------------------------------------------------- +// Spans +// --------------------------------------------------------------------------- + +// blend_span composites one horizontal run of coverage in a single colour onto +// `dst`, which must be PREMULTIPLIED. +// +// y, x0 the first pixel of the run, in `dst` pixel coordinates +// coverage anti-aliasing coverage, 0..255, one byte per pixel from x0 +// rgba the source colour: STRAIGHT RGB in 0..1 plus its alpha, which is +// the constant alpha of the paint operation +// mask an optional soft-mask row, 0..255, aligned with `coverage` so +// that mask[i] modulates the same pixel coverage[i] does. nil means +// no mask. Bytes past the end of a short mask read as 255 rather +// than erasing content that has no mask data. +// +// The effective source alpha of a pixel is rgba.a * coverage/255 * mask/255. +// Runs that fall partly or wholly outside `dst` are clipped, not rejected: the +// rasteriser is allowed to hand over spans that start left of zero. +@(private) +blend_span :: proc(dst: ^Bitmap, y, x0: int, coverage: []u8, rgba: [4]f64, + mode: Blend_Mode, mask: []u8 = nil) { + if dst == nil || len(coverage) == 0 { return } + + sa := blend_clamp01(rgba[3]) + if sa <= 0 { return } + + row := bitmap_row(dst, y) + if row == nil { return } + + // Clip the run to the bitmap. `skip` is how many coverage bytes fall off + // the left edge, so the mask stays in step with the coverage. + skip := 0 + x := x0 + if x < 0 { + skip = -x + x = 0 + } + if skip >= len(coverage) { return } + count := min(len(coverage) - skip, dst.width - x) + if count <= 0 { return } + + n := dst.n + cs := blend_source_for(n, [3]f64{ + blend_clamp01(rgba[0]), blend_clamp01(rgba[1]), blend_clamp01(rgba[2]), + }) + + for i in 0 ..< count { + cov := coverage[skip + i] + if cov == 0 { continue } + m := u8(255) + if mask != nil { + mi := skip + i + m = mi < len(mask) ? mask[mi] : 255 + } + if m == 0 { continue } + + as_ := sa * (f64(cov) / 255) * (f64(m) / 255) + if as_ <= 0 { continue } + + off := (x + i) * n + if off + n > len(row) { break } + cb, ab := blend_read_straight(row, off, n) + co, ao := blend_over(cb, ab, cs, as_, mode) + blend_write_premul(row, off, n, co, ao) + } + + if blend_has_alpha(n) { dst.premultiplied = true } +} + +// blend_span_rgba8 is blend_span for a run whose colour varies per pixel: what +// an image or a shading paints. +// +// `src` is STRAIGHT RGBA8, four bytes per pixel, starting at destination pixel +// x0; that is the form `image_decode` hands back. `alpha` is the constant alpha +// of the paint operation, multiplied into every pixel's own alpha. `coverage` +// may be nil, meaning fully covered; when present it must be one byte per +// source pixel. The run length is len(src)/4. +@(private) +blend_span_rgba8 :: proc(dst: ^Bitmap, y, x0: int, src: []u8, alpha: f64, + mode: Blend_Mode, coverage: []u8 = nil, mask: []u8 = nil) { + if dst == nil || len(src) < 4 { return } + + ga := blend_clamp01(alpha) + if ga <= 0 { return } + + row := bitmap_row(dst, y) + if row == nil { return } + + skip := 0 + x := x0 + if x < 0 { + skip = -x + x = 0 + } + total := len(src) / 4 + if skip >= total { return } + count := min(total - skip, dst.width - x) + if count <= 0 { return } + + n := dst.n + for i in 0 ..< count { + si := (skip + i) * 4 + sa8 := src[si + 3] + if sa8 == 0 { continue } + + cov := u8(255) + if coverage != nil { + ci := skip + i + cov = ci < len(coverage) ? coverage[ci] : 255 + } + if cov == 0 { continue } + + m := u8(255) + if mask != nil { + mi := skip + i + m = mi < len(mask) ? mask[mi] : 255 + } + if m == 0 { continue } + + as_ := ga * (f64(sa8) / 255) * (f64(cov) / 255) * (f64(m) / 255) + if as_ <= 0 { continue } + + cs := blend_source_for(n, [3]f64{ + f64(src[si]) / 255, f64(src[si + 1]) / 255, f64(src[si + 2]) / 255, + }) + + off := (x + i) * n + if off + n > len(row) { break } + cb, ab := blend_read_straight(row, off, n) + co, ao := blend_over(cb, ab, cs, as_, mode) + blend_write_premul(row, off, n, co, ao) + } + + if blend_has_alpha(n) { dst.premultiplied = true } +} + +// --------------------------------------------------------------------------- +// Whole-bitmap compositing: transparency groups +// --------------------------------------------------------------------------- + +// blend_bitmap_restore puts `src` BACK onto `dst`, replacing rather than +// compositing, and interpolating towards what `dst` already holds wherever the +// mask is not opaque. +// +// This is what finishes a NON-ISOLATED transparency group (ISO 32000-1, +// 11.4.6). Such a group is handed a copy of its backdrop as its initial +// contents, so its finished bitmap already IS "contents over backdrop" -- and +// compositing that over the backdrop a second time blends the backdrop with +// itself. `C over C` under Normal gives ao = a + a(1-a) = 2a - a^2, so a +// backdrop pixel at alpha 0.5 comes back at 0.75, which is wrong everywhere the +// backdrop is not already opaque: any group nested inside another group, and +// every page rendered with a transparent background. Replacing is exact for +// both, and reduces to the old composite wherever the backdrop was opaque. +// +// `mask` is optional and is aligned with `src`, exactly as `blend_bitmap`'s is. +// Both bitmaps are PREMULTIPLIED. +@(private) +blend_bitmap_restore :: proc(dst, src: ^Bitmap, x, y: int, mask: ^Bitmap) { + if dst == nil || src == nil { return } + + dn := dst.n + sn := src.n + + sx0 := max(0, -x) + sy0 := max(0, -y) + sx1 := min(src.width, dst.width - x) + sy1 := min(src.height, dst.height - y) + if sx1 <= sx0 || sy1 <= sy0 { return } + + for sy in sy0 ..< sy1 { + srow := bitmap_row(src, sy) + drow := bitmap_row(dst, y + sy) + if srow == nil || drow == nil { continue } + mrow: []u8 + if mask != nil { mrow = bitmap_row(mask, sy) } + + for sx in sx0 ..< sx1 { + soff := sx * sn + if soff + sn > len(srow) { break } + doff := (x + sx) * dn + if doff + dn > len(drow) { break } + + m := u8(255) + if mask != nil { + moff := sx * mask.n + if mrow != nil && moff < len(mrow) { m = mrow[moff] } else { m = 255 } + } + if m == 0 { continue } + + cs_raw, sa := blend_read_straight(srow, soff, sn) + cs := blend_source_for(dn, cs_raw) + if m == 255 { + blend_write_premul(drow, doff, dn, [3]f64{cs[0] * sa, cs[1] * sa, cs[2] * sa}, sa) + continue + } + // Interpolate in PREMULTIPLIED space: a transparent pixel has no + // recoverable colour, and lerping straight colour towards it would + // drag a black fringe in. + t := f64(m) / 255 + cb, ab := blend_read_straight(drow, doff, dn) + ao := ab + (sa - ab) * t + co: [3]f64 + for i in 0 ..< 3 { + co[i] = cb[i] * ab + (cs[i] * sa - cb[i] * ab) * t + } + blend_write_premul(drow, doff, dn, co, ao) + } + } + + if blend_has_alpha(dn) { dst.premultiplied = true } +} + +// blend_bitmap composites `src` onto `dst` with its top-left corner at +// (x, y) in `dst`. This is what `end_group` does with a rendered transparency +// group (11.4.6): the group was rendered into its own bitmap, and its constant +// alpha, blend mode and soft mask are applied once, here, to the group as a +// whole rather than to each operation inside it. +// +// Both bitmaps are PREMULTIPLIED. `alpha` is the group's constant alpha. +// +// `mask` is optional and is aligned with `src`, not with `dst`: mask pixel +// (i, j) modulates src pixel (i, j). A device holding a device-space soft mask +// passes a borrowed window onto it, which `Bitmap`'s independent `stride` +// exists to allow. Only component 0 of the mask is read, which is what +// `mask_from_bitmap` produces for both mask types. Pixels outside the mask read +// as 255, so a mask that is accidentally too small shows content rather than +// erasing it. +@(private) +blend_bitmap :: proc(dst, src: ^Bitmap, x, y: int, alpha: f64, mode: Blend_Mode, mask: ^Bitmap) { + if dst == nil || src == nil { return } + + ga := blend_clamp01(alpha) + if ga <= 0 { return } + + dn := dst.n + sn := src.n + + // Clip the placement to the destination once, rather than testing every + // pixel: a group whose bitmap hangs off the page is the normal case. + sx0 := max(0, -x) + sy0 := max(0, -y) + sx1 := min(src.width, dst.width - x) + sy1 := min(src.height, dst.height - y) + if sx1 <= sx0 || sy1 <= sy0 { return } + + for sy in sy0 ..< sy1 { + srow := bitmap_row(src, sy) + drow := bitmap_row(dst, y + sy) + if srow == nil || drow == nil { continue } + mrow: []u8 + if mask != nil { mrow = bitmap_row(mask, sy) } + + for sx in sx0 ..< sx1 { + soff := sx * sn + if soff + sn > len(srow) { break } + + cs_raw, sa := blend_read_straight(srow, soff, sn) + if sa <= 0 { continue } + + m := u8(255) + if mask != nil { + moff := sx * mask.n + if mrow != nil && moff < len(mrow) { m = mrow[moff] } else { m = 255 } + } + if m == 0 { continue } + + as_ := sa * ga * (f64(m) / 255) + if as_ <= 0 { continue } + + doff := (x + sx) * dn + if doff + dn > len(drow) { break } + cs := blend_source_for(dn, cs_raw) + cb, ab := blend_read_straight(drow, doff, dn) + co, ao := blend_over(cb, ab, cs, as_, mode) + blend_write_premul(drow, doff, dn, co, ao) + } + } + + if blend_has_alpha(dn) { dst.premultiplied = true } +} + +// --------------------------------------------------------------------------- +// Soft masks +// --------------------------------------------------------------------------- + +// mask_from_bitmap turns a rendered group bitmap into a one-component mask +// (ISO 32000-2, 11.6.5). +// +// luminosity false selects an alpha mask -- the mask value is the group's +// alpha and its colour is ignored. true selects a luminosity +// mask: the group is composited onto an opaque backdrop and the +// mask value is the luminosity of the result. +// backdrop the /BC colour, already converted to RGB, used only by a +// luminosity mask. Black is the default the format specifies, +// and it is what makes the area a group does not cover mask +// everything out. +// transfer the /TR function, or nil. It is evaluated once per possible +// byte value into a 256-entry table rather than once per pixel: +// a type 4 PostScript transfer over a full page would otherwise +// run the calculator several million times. +// +// `src` may be premultiplied or straight; its `premultiplied` flag decides. +// The result is a fresh `n = 1` Bitmap the same size as `src`, allocated with +// `allocator` and OWNED BY THE CALLER -- free it with `bitmap_destroy`. +@(private) +mask_from_bitmap :: proc(src: ^Bitmap, luminosity: bool, backdrop: [3]f64, + transfer: ^Function, allocator := context.allocator) -> (out: Bitmap, err: Error) { + if src == nil || src.width <= 0 || src.height <= 0 { return {}, .Invalid_Argument } + + out = bitmap_make(src.width, src.height, 1, allocator) or_return + + lut := blend_transfer_table(transfer) + + bd := [3]f64{blend_clamp01(backdrop[0]), blend_clamp01(backdrop[1]), blend_clamp01(backdrop[2])} + bd_lum_v := blend_quantize(blend_lum(bd)) + + sn := src.n + has_a := blend_has_alpha(sn) + + for j in 0 ..< src.height { + srow := bitmap_row(src, j) + drow := bitmap_row(&out, j) + if srow == nil || drow == nil { continue } + + for i in 0 ..< src.width { + soff := i * sn + if soff + sn > len(srow) { break } + + v: u8 + if !luminosity { + v = has_a ? srow[soff + sn - 1] : 255 + } else { + a := has_a ? f64(srow[soff + sn - 1]) / 255 : 1.0 + if a <= 0 { + // Nothing was painted here, so the mask is the backdrop. + v = bd_lum_v + } else { + c: [3]f64 + if sn <= 2 { + g := f64(srow[soff]) / 255 + c = {g, g, g} + } else { + c = {f64(srow[soff]) / 255, f64(srow[soff + 1]) / 255, f64(srow[soff + 2]) / 255} + } + // Composite over the opaque backdrop. A premultiplied + // source already carries the a*C factor; a straight one + // does not. + if !src.premultiplied { + c = {c[0] * a, c[1] * a, c[2] * a} + } + inv := 1 - a + v = blend_quantize(blend_lum([3]f64{ + c[0] + inv * bd[0], c[1] + inv * bd[1], c[2] + inv * bd[2], + })) + } + } + drow[i] = lut[v] + } + } + return out, .None +} + +// blend_transfer_table precomputes fn over the 256 byte values. An absent, +// malformed or wrongly-shaped function yields the identity, because a soft +// mask with an unusable /TR must still mask (11.6.5.2) rather than vanish. +@(private) +blend_transfer_table :: proc(fn: ^Function) -> (lut: [256]u8) { + for i in 0 ..< 256 { lut[i] = u8(i) } + if fn == nil || fn.n_in != 1 || fn.n_out < 1 { return } + + in_buf: [1]f64 + // Sized for the widest function the library will build, so a function that + // reports one output but writes n_out cannot run off the end. + out_buf: [MAX_FUNCTION_OUTPUTS]f64 + for i in 0 ..< 256 { + in_buf[0] = f64(i) / 255 + out_buf = {} + function_eval(fn, in_buf[:], out_buf[:]) + lut[i] = blend_quantize(out_buf[0]) + } + return +} diff --git a/blend_test.odin b/blend_test.odin new file mode 100644 index 0000000..c8e5afa --- /dev/null +++ b/blend_test.odin @@ -0,0 +1,1037 @@ +package pdf + +import "core:math" +import "core:slice" +import "core:testing" + +@(private = "file") +EPS :: 1e-12 + +@(private = "file") +expect_near :: proc(t: ^testing.T, got, want: f64, label: string, eps := EPS, loc := #caller_location) { + testing.expectf(t, abs(got - want) <= eps, "%s: got %.17g, want %.17g", label, got, want, loc = loc) +} + +@(private = "file") +expect_near3 :: proc(t: ^testing.T, got, want: [3]f64, label: string, eps := EPS, loc := #caller_location) { + ok := abs(got[0] - want[0]) <= eps && abs(got[1] - want[1]) <= eps && abs(got[2] - want[2]) <= eps + testing.expectf(t, ok, "%s: got %.17g, want %.17g", label, got, want, loc = loc) +} + +// A one-pixel-tall RGBA bitmap whose pixels are set from premultiplied bytes. +@(private = "file") +make_bmp :: proc(t: ^testing.T, w, h, n: int, fill: []u8, loc := #caller_location) -> Bitmap { + bmp, err := bitmap_make(w, h, n, context.allocator) + testing.expect_value(t, err, Error.None, loc = loc) + bmp.premultiplied = true + if fill != nil { + for i in 0 ..< len(bmp.pixels) { + bmp.pixels[i] = fill[i % len(fill)] + } + } + return bmp +} + +// --------------------------------------------------------------------------- +// Separable blend functions (ISO 32000-2, 11.3.5.2) +// --------------------------------------------------------------------------- + +@(private = "file") +Sep_Case :: struct { + mode: Blend_Mode, + cb, cs: f64, + want: f64, +} + +@(test) +test_blend_separable_boundaries :: proc(t: ^testing.T) { + cases := []Sep_Case{ + // Normal: the source, whatever the backdrop. + {.Normal, 0, 0, 0}, {.Normal, 0, 1, 1}, {.Normal, 1, 0, 0}, {.Normal, 0.5, 0.25, 0.25}, + + // Multiply: cb * cs. + {.Multiply, 0, 0, 0}, {.Multiply, 1, 1, 1}, {.Multiply, 0.5, 0.5, 0.25}, + {.Multiply, 1, 0.5, 0.5}, {.Multiply, 0, 1, 0}, + + // Screen: cb + cs - cb*cs. + {.Screen, 0, 0, 0}, {.Screen, 1, 1, 1}, {.Screen, 0.5, 0.5, 0.75}, + {.Screen, 0, 0.5, 0.5}, {.Screen, 1, 0, 1}, + + // Overlay = HardLight with the operands exchanged, so the branch is on + // the BACKDROP: dark backdrops multiply, light ones screen. + {.Overlay, 0.5, 0.5, 0.5}, {.Overlay, 0.25, 0.5, 0.25}, {.Overlay, 0.75, 0.5, 0.75}, + {.Overlay, 0, 1, 0}, {.Overlay, 1, 0, 1}, {.Overlay, 0.25, 1, 0.5}, + + {.Darken, 0.3, 0.7, 0.3}, {.Darken, 1, 0, 0}, {.Darken, 0.5, 0.5, 0.5}, + {.Lighten, 0.3, 0.7, 0.7}, {.Lighten, 1, 0, 1}, {.Lighten, 0.5, 0.5, 0.5}, + + // HardLight branches on the SOURCE. + {.Hard_Light, 0.5, 0.25, 0.25}, {.Hard_Light, 0.5, 0.75, 0.75}, + {.Hard_Light, 0.5, 0.5, 0.5}, {.Hard_Light, 0.2, 1, 1}, {.Hard_Light, 0.8, 0, 0}, + // Just above the threshold, where multiplying and screening disagree: + // Screen(0.25, 0.1) = 0.325, while the wrong branch gives 0.275. + {.Hard_Light, 0.25, 0.55, 0.325}, {.Overlay, 0.55, 0.25, 0.325}, + + // SoftLight. cs == 0.5 is the identity; cs == 0 darkens by cb*(1-cb); + // cs == 1 lightens to D(cb). + {.Soft_Light, 0.5, 0.5, 0.5}, {.Soft_Light, 0.5, 0, 0.25}, + {.Soft_Light, 0.5, 1, 0.70710678118654752}, {.Soft_Light, 0.25, 1, 0.5}, + {.Soft_Light, 0.05, 1, 0.172}, + {.Soft_Light, 0, 0, 0}, {.Soft_Light, 1, 0, 1}, + {.Soft_Light, 0, 1, 0}, {.Soft_Light, 1, 1, 1}, + + {.Difference, 0.25, 0.75, 0.5}, {.Difference, 1, 0, 1}, {.Difference, 0.5, 0.5, 0}, + {.Difference, 0, 0, 0}, {.Difference, 1, 1, 0}, + + {.Exclusion, 0.5, 0.5, 0.5}, {.Exclusion, 1, 1, 0}, {.Exclusion, 0, 0.25, 0.25}, + {.Exclusion, 1, 0, 1}, {.Exclusion, 0, 0, 0}, + } + for c in cases { + got := blend_separable(c.mode, c.cb, c.cs) + testing.expectf(t, abs(got - c.want) <= EPS, + "%v(cb=%v, cs=%v): got %.17g, want %.17g", c.mode, c.cb, c.cs, got, c.want) + } +} + +// ColorDodge and ColorBurn are where the division by zero lives. ISO 32000-2 +// orders the case split so that a black backdrop stays black under dodge and a +// white backdrop stays white under burn; ISO 32000-1 tested the source first +// and produced the opposite (and visibly wrong) answer at those two corners. +@(test) +test_blend_color_dodge_edges :: proc(t: ^testing.T) { + cases := []Sep_Case{ + {.Color_Dodge, 0, 1, 0}, // the corner ISO 32000-1 got wrong (it said 1) + {.Color_Dodge, 0, 0, 0}, + {.Color_Dodge, 0, 0.5, 0}, + {.Color_Dodge, 0.5, 1, 1}, // cs == 1 with a non-black backdrop: white + {.Color_Dodge, 1, 1, 1}, + {.Color_Dodge, 0.5, 0, 0.5}, // cs == 0 is the identity + {.Color_Dodge, 1, 0, 1}, + {.Color_Dodge, 0.5, 0.5, 1}, // cb/(1-cs) == 1 + {.Color_Dodge, 0.25, 0.5, 0.5}, + {.Color_Dodge, 0.1, 0.5, 0.2}, + {.Color_Dodge, 0.75, 0.5, 1}, // clamped by min(1, ...) + } + for c in cases { + got := blend_separable(c.mode, c.cb, c.cs) + testing.expectf(t, abs(got - c.want) <= EPS, + "ColorDodge(cb=%v, cs=%v): got %.17g, want %.17g", c.cb, c.cs, got, c.want) + } +} + +@(test) +test_blend_color_burn_edges :: proc(t: ^testing.T) { + cases := []Sep_Case{ + {.Color_Burn, 1, 0, 1}, // the corner ISO 32000-1 got wrong (it said 0) + {.Color_Burn, 1, 1, 1}, + {.Color_Burn, 1, 0.5, 1}, + {.Color_Burn, 0, 0, 0}, // cs == 0 with a non-white backdrop: black + {.Color_Burn, 0.5, 0, 0}, + {.Color_Burn, 0.5, 1, 0.5}, // cs == 1 is the identity + {.Color_Burn, 0, 1, 0}, + {.Color_Burn, 0.5, 0.5, 0}, // (1-cb)/cs == 1 + {.Color_Burn, 0.75, 0.5, 0.5}, + {.Color_Burn, 0.9, 0.5, 0.8}, + {.Color_Burn, 0.25, 0.5, 0}, // clamped by min(1, ...) + } + for c in cases { + got := blend_separable(c.mode, c.cb, c.cs) + testing.expectf(t, abs(got - c.want) <= EPS, + "ColorBurn(cb=%v, cs=%v): got %.17g, want %.17g", c.cb, c.cs, got, c.want) + } +} + +// D(x) is piecewise: a cubic below 0.25 and sqrt above. Using sqrt everywhere +// is the common bug, so the test pins a point where the two disagree by 0.05 -- +// far more than any tolerance could hide. +@(test) +test_blend_soft_light_transfer :: proc(t: ^testing.T) { + expect_near(t, blend_soft_light_d(0), 0, "D(0)") + expect_near(t, blend_soft_light_d(1), 1, "D(1)") + expect_near(t, blend_soft_light_d(0.05), 0.172, "D(0.05) is the cubic") + testing.expect(t, abs(blend_soft_light_d(0.05) - math.sqrt(f64(0.05))) > 0.04, + "the low branch must be the cubic, not sqrt") + + // The two branches meet at 0.25: both give exactly 0.5. + expect_near(t, blend_soft_light_d(0.25), 0.5, "D(0.25) from the cubic") + expect_near(t, blend_soft_light_d(0.25 + 1e-9), 0.5, "D just above 0.25", 1e-8) + + // 0.1 is a second point on the cubic: ((1.6-12)*0.1+4)*0.1 = 0.296. + expect_near(t, blend_soft_light_d(0.1), 0.296, "D(0.1)") + expect_near(t, blend_soft_light_d(0.81), 0.9, "D(0.81) is sqrt") +} + +// Overlay is defined as HardLight with the operands exchanged; checking the +// identity over a grid is stronger than any handful of sample points. +@(test) +test_blend_overlay_is_hard_light_swapped :: proc(t: ^testing.T) { + for i in 0 ..= 8 { + for j in 0 ..= 8 { + cb := f64(i) / 8 + cs := f64(j) / 8 + expect_near(t, blend_separable(.Overlay, cb, cs), + blend_separable(.Hard_Light, cs, cb), "Overlay == HardLight swapped") + } + } +} + +// Every separable mode must stay inside the unit interval and never produce a +// NaN, including at the corners where a division by zero lurks. +@(test) +test_blend_separable_stays_in_range :: proc(t: ^testing.T) { + for mode in Blend_Mode { + if !blend_mode_is_separable(mode) { continue } + for i in 0 ..= 16 { + for j in 0 ..= 16 { + cb := f64(i) / 16 + cs := f64(j) / 16 + v := blend_separable(mode, cb, cs) + testing.expectf(t, is_finite(v), "%v(%v,%v) is not finite: %v", mode, cb, cs, v) + testing.expectf(t, v >= -EPS && v <= 1 + EPS, "%v(%v,%v) = %v out of range", mode, cb, cs, v) + } + } + } +} + +// --------------------------------------------------------------------------- +// The non-separable helpers (11.3.5.3) +// --------------------------------------------------------------------------- + +@(test) +test_blend_lum_and_sat :: proc(t: ^testing.T) { + expect_near(t, blend_lum({1, 0, 0}), 0.3, "Lum(red)") + expect_near(t, blend_lum({0, 1, 0}), 0.59, "Lum(green)") + expect_near(t, blend_lum({0, 0, 1}), 0.11, "Lum(blue)") + expect_near(t, blend_lum({1, 1, 1}), 1, "Lum(white)") + expect_near(t, blend_lum({0.5, 0.5, 0.5}), 0.5, "Lum(mid grey)") + + expect_near(t, blend_sat({0.2, 0.9, 0.5}), 0.7, "Sat") + expect_near(t, blend_sat({0.4, 0.4, 0.4}), 0, "Sat(grey)") + expect_near(t, blend_sat({0, 0, 1}), 1, "Sat(blue)") +} + +// SetSat keeps each component in its own slot: the largest becomes s, the +// smallest becomes 0, the middle is interpolated. An implementation that sorts +// the values but writes them back in order passes a sorted input and fails +// this one. +@(test) +test_blend_set_sat :: proc(t: ^testing.T) { + expect_near3(t, blend_set_sat({1, 0.5, 0}, 0.4), {0.4, 0.2, 0}, "SetSat descending") + expect_near3(t, blend_set_sat({0, 0.5, 1}, 1), {0, 0.5, 1}, "SetSat ascending") + // 0.6 > 0.4 > 0.2 lives in components r > b > g, and must stay there. + expect_near3(t, blend_set_sat({0.6, 0.2, 0.4}, 1), {1, 0, 0.5}, "SetSat unsorted") + // A grey has no ordering to preserve and no range to scale by: all zero. + expect_near3(t, blend_set_sat({0.5, 0.5, 0.5}, 0.8), {0, 0, 0}, "SetSat of a grey") + expect_near(t, blend_sat(blend_set_sat({0.6, 0.2, 0.4}, 0.25)), 0.25, "SetSat sets Sat") +} + +// ClipColor scales a colour towards the grey axis without changing its +// luminosity. n, x and L all come from the colour as it arrived: recomputing +// the maximum after the low clip has run changes the answer. +@(test) +test_blend_clip_color :: proc(t: ^testing.T) { + expect_near3(t, blend_clip_color({0.5, 0.25, 0.75}), {0.5, 0.25, 0.75}, "in gamut: unchanged") + + // Lum = 0.5, x = 1.2, so everything scales by (1-L)/(x-L) = 5/7. + got := blend_clip_color({1.2, 0.2, 0.2}) + expect_near3(t, got, {1, 0.2857142857142857, 0.2857142857142857}, "clip high") + expect_near(t, blend_lum(got), 0.5, "clipping preserves luminosity") + + // Lum = 0.29, n = -0.3, so everything scales by L/(L-n) = 0.29/0.59. + got2 := blend_clip_color({-0.3, 0.7, -0.3}) + expect_near3(t, got2, {0, 0.49152542372881356, 0}, "clip low") + expect_near(t, blend_lum(got2), 0.29, "clipping preserves luminosity") + + // Both branches can fire for the same colour, and the second one uses the + // maximum of the ORIGINAL colour, not the maximum left by the first. Here + // the low clip brings the blue down to 0.765; recomputing the maximum + // there would skip the high clip entirely and stop at that value, whereas + // the spec keeps compressing with x = 1.5 and lands on 0.574. + both := blend_clip_color({-0.5, 0.5, 1.5}) + expect_near3(t, both, + {0.13025210084033614, 0.35216308745720551, 0.57407407407407407}, "both branches") + expect_near(t, blend_lum(both), 0.31, "luminosity survives both clips") + + // A uniform out-of-gamut grey drives L - n and x - L to zero. The constants + // matter: at -0.5 the rounding of the Lum weights leaves a denominator of + // ~2e-17 and the division still lands on the right answer by luck, but at + // -0.001 the weighted sum rounds to exactly the component, the denominator + // is exactly zero, and an unguarded divide yields 0/0 = NaN. + expect_near3(t, blend_clip_color({-0.001, -0.001, -0.001}), {0, 0, 0}, "uniform negative") + expect_near3(t, blend_clip_color({1.001, 1.001, 1.001}), {1, 1, 1}, "uniform above one") + expect_near3(t, blend_clip_color({-0.5, -0.5, -0.5}), {0, 0, 0}, "uniform negative (large)") + expect_near3(t, blend_clip_color({1.5, 1.5, 1.5}), {1, 1, 1}, "uniform above one (large)") +} + +@(test) +test_blend_set_lum :: proc(t: ^testing.T) { + expect_near3(t, blend_set_lum({0.5, 0.5, 0.5}, 0.3), {0.3, 0.3, 0.3}, "SetLum of a grey") + got := blend_set_lum({1, 0, 0}, 0.5) + expect_near3(t, got, {1, 0.2857142857142857, 0.2857142857142857}, "SetLum needing a clip") + expect_near(t, blend_lum(got), 0.5, "SetLum sets Lum") + expect_near(t, blend_lum(blend_set_lum({0.2, 0.4, 0.6}, 0.9)), 0.9, "SetLum sets Lum (2)") +} + +// Worked examples derived by hand from the spec's own pseudo-code. +@(test) +test_blend_non_separable_worked_examples :: proc(t: ^testing.T) { + cb := [3]f64{0.2, 0.4, 0.6} // Lum 0.362, Sat 0.4 + cs := [3]f64{1, 0.5, 0} // Lum 0.59, Sat 1 + + // Hue: SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb)) + // SetSat((1,0.5,0), 0.4) = (0.4, 0.2, 0), Lum 0.238 + // + (0.362 - 0.238) = (0.524, 0.324, 0.124), in gamut. + expect_near3(t, blend_non_separable(.Hue, cb, cs), {0.524, 0.324, 0.124}, "Hue") + + // Saturation: SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb)) + // SetSat((0.2,0.4,0.6), 1) = (0, 0.5, 1), Lum 0.405 + // + (0.362 - 0.405) = (-0.043, 0.457, 0.957), which clips low by + // L/(L-n) = 0.362/0.405. + expect_near3(t, blend_non_separable(.Saturation, cb, cs), + {0, 0.44691358024691358, 0.89382716049382716}, "Saturation") + + grey := [3]f64{0.5, 0.5, 0.5} + red := [3]f64{1, 0, 0} + // Color: SetLum(Cs, Lum(Cb)) -- red pushed to luminosity 0.5, clipped high. + expect_near3(t, blend_non_separable(.Color, grey, red), + {1, 0.2857142857142857, 0.2857142857142857}, "Color") + // Luminosity: SetLum(Cb, Lum(Cs)) -- grey pulled down to red's 0.3. + expect_near3(t, blend_non_separable(.Luminosity, grey, red), {0.3, 0.3, 0.3}, "Luminosity") + + // Luminosity to full white: SetLum((1,0,0), 1) needs the high clip, and + // (1-L)/(x-L) is exactly zero there, so every component lands on L. + expect_near3(t, blend_non_separable(.Luminosity, red, {1, 1, 1}), {1, 1, 1}, "Luminosity to white") + // ... and to black, through the low clip. + expect_near3(t, blend_non_separable(.Luminosity, red, {0, 0, 0}), {0, 0, 0}, "Luminosity to black") +} + +// The defining identities: Hue and Color take the backdrop's luminosity, Hue +// and Saturation take a stated saturation, Luminosity takes the source's. +@(test) +test_blend_non_separable_identities :: proc(t: ^testing.T) { + cb := [3]f64{0.15, 0.55, 0.35} + cs := [3]f64{0.8, 0.1, 0.45} + + expect_near(t, blend_lum(blend_non_separable(.Hue, cb, cs)), blend_lum(cb), "Hue keeps Lum(Cb)") + expect_near(t, blend_lum(blend_non_separable(.Saturation, cb, cs)), blend_lum(cb), "Sat keeps Lum(Cb)") + expect_near(t, blend_lum(blend_non_separable(.Color, cb, cs)), blend_lum(cb), "Color keeps Lum(Cb)") + expect_near(t, blend_lum(blend_non_separable(.Luminosity, cb, cs)), blend_lum(cs), "Luminosity takes Lum(Cs)") + + // Saturation survives exactly only while the result stays inside the unit + // cube. Hue does here; Saturation with this pair lands at -0.0435 in red + // and ClipColor compresses it back towards the grey axis, which can only + // reduce the saturation. Both halves of that are asserted. + expect_near(t, blend_sat(blend_non_separable(.Hue, cb, cs)), blend_sat(cb), "Hue takes Sat(Cb)") + low_sat := [3]f64{0.5, 0.45, 0.5} // Sat 0.05: small enough to stay in gamut + expect_near(t, blend_sat(blend_non_separable(.Saturation, cb, low_sat)), blend_sat(low_sat), + "Saturation takes Sat(Cs)") + testing.expect(t, blend_sat(blend_non_separable(.Saturation, cb, cs)) < blend_sat(cs) - 0.01, + "clipping an out-of-gamut result reduces its saturation") + + // A grey source has no hue and no saturation to donate. + expect_near3(t, blend_non_separable(.Hue, cb, {0.5, 0.5, 0.5}), + {blend_lum(cb), blend_lum(cb), blend_lum(cb)}, "Hue of a grey source") +} + +@(test) +test_blend_non_separable_falls_back_for_separable_modes :: proc(t: ^testing.T) { + cb := [3]f64{0.2, 0.5, 0.8} + cs := [3]f64{0.5, 0.5, 0.5} + expect_near3(t, blend_non_separable(.Multiply, cb, cs), {0.1, 0.25, 0.4}, "componentwise Multiply") + expect_near3(t, blend_non_separable(.Normal, cb, cs), cs, "componentwise Normal") +} + +@(test) +test_blend_non_separable_stays_in_range :: proc(t: ^testing.T) { + for mode in Blend_Mode { + if blend_mode_is_separable(mode) { continue } + for i in 0 ..= 5 { + for j in 0 ..= 5 { + for k in 0 ..= 5 { + cb := [3]f64{f64(i) / 5, f64(j) / 5, f64(k) / 5} + cs := [3]f64{f64(k) / 5, f64(i) / 5, f64(j) / 5} + v := blend_non_separable(mode, cb, cs) + for c in v { + testing.expectf(t, is_finite(c), "%v produced %v", mode, c) + testing.expectf(t, c >= -1e-9 && c <= 1 + 1e-9, "%v produced %v out of range", mode, c) + } + } + } + } + } +} + +// --------------------------------------------------------------------------- +// The compositing formula (11.3.6) +// --------------------------------------------------------------------------- + +// With Normal the union formula must collapse to plain source-over, because +// B(Cb,Cs) = Cs makes the first two terms sum to as*Cs. +@(test) +test_blend_over_normal_is_source_over :: proc(t: ^testing.T) { + for i in 0 ..= 4 { + for j in 0 ..= 4 { + ab := f64(i) / 4 + as_ := f64(j) / 4 + cb := [3]f64{0.2, 0.5, 0.9} + cs := [3]f64{0.7, 0.1, 0.4} + co, ao := blend_over(cb, ab, cs, as_, .Normal) + for c in 0 ..< 3 { + want := as_ * cs[c] + (1 - as_) * ab * cb[c] + expect_near(t, co[c], want, "source-over colour") + } + expect_near(t, ao, as_ + ab * (1 - as_), "source-over alpha") + } + } +} + +// A fully transparent backdrop leaves only the as*(1-ab)*Cs term, whatever the +// blend mode: the B() term is multiplied by ab. +@(test) +test_blend_over_transparent_backdrop_gives_source :: proc(t: ^testing.T) { + cs := [3]f64{0.25, 0.5, 0.75} + for mode in Blend_Mode { + for j in 0 ..= 4 { + as_ := f64(j) / 4 + co, ao := blend_over({0, 0, 0}, 0, cs, as_, mode) + expect_near3(t, co, {as_ * cs[0], as_ * cs[1], as_ * cs[2]}, "premultiplied source") + expect_near(t, ao, as_, "alpha is the source alpha") + } + } +} + +// A source alpha of zero must leave the backdrop exactly as it was, whatever +// the blend mode: this is what makes `0 gs` invisible rather than merely faint. +@(test) +test_blend_over_zero_source_alpha_is_a_no_op :: proc(t: ^testing.T) { + cb := [3]f64{0.2, 0.6, 0.9} + for mode in Blend_Mode { + for i in 0 ..= 4 { + ab := f64(i) / 4 + co, ao := blend_over(cb, ab, {1, 1, 1}, 0, mode) + expect_near3(t, co, {ab * cb[0], ab * cb[1], ab * cb[2]}, "backdrop unchanged") + expect_near(t, ao, ab, "alpha unchanged") + } + } +} + +// Over an opaque backdrop the result is opaque and is exactly B(Cb, Cs) when +// the source is opaque too -- the blend function alone, with no interpolation. +@(test) +test_blend_over_opaque_is_the_blend_function :: proc(t: ^testing.T) { + cb := [3]f64{0.25, 0.5, 0.75} + cs := [3]f64{0.5, 0.5, 0.5} + for mode in Blend_Mode { + co, ao := blend_over(cb, 1, cs, 1, mode) + want := blend_mode_is_separable(mode) \ + ? [3]f64{blend_separable(mode, cb[0], cs[0]), + blend_separable(mode, cb[1], cs[1]), + blend_separable(mode, cb[2], cs[2])} \ + : blend_non_separable(mode, cb, cs) + expect_near3(t, co, want, "opaque over opaque is B()") + expect_near(t, ao, 1, "opaque stays opaque") + } +} + +@(test) +test_blend_over_never_produces_nan :: proc(t: ^testing.T) { + for mode in Blend_Mode { + for i in 0 ..= 3 { + for j in 0 ..= 3 { + cb := [3]f64{f64(i) / 3, 1 - f64(i) / 3, 0} + cs := [3]f64{f64(j) / 3, 0, 1 - f64(j) / 3} + co, ao := blend_over(cb, f64(i) / 3, cs, f64(j) / 3, mode) + testing.expect(t, is_finite(ao), "alpha is finite") + for c in co { + testing.expectf(t, is_finite(c), "%v produced %v", mode, c) + // Premultiplied output can never exceed its own alpha. + testing.expectf(t, c >= -EPS && c <= ao + 1e-9, "%v produced %v with ao %v", mode, c, ao) + } + } + } + } +} + +// --------------------------------------------------------------------------- +// blend_span +// --------------------------------------------------------------------------- + +// Painting onto nothing gives back the source, premultiplied. 0.25/0.5/0.75 +// are exact in binary, so the bytes are exact too. +@(test) +test_blend_span_onto_transparent_gives_source :: proc(t: ^testing.T) { + dst := make_bmp(t, 1, 1, 4, nil) + defer bitmap_destroy(&dst) + + blend_span(&dst, 0, 0, {255}, {0.25, 0.5, 0.75, 0.5}, .Normal) + testing.expect_value(t, dst.pixels[0], 32) // 0.25 * 0.5 * 255 = 31.875 + testing.expect_value(t, dst.pixels[1], 64) // 0.5 * 0.5 * 255 = 63.75 + testing.expect_value(t, dst.pixels[2], 96) // 0.75 * 0.5 * 255 = 95.625 + testing.expect_value(t, dst.pixels[3], 128) // 0.5 * 255 = 127.5 + + // Undoing the premultiplication recovers the colour that went in. + bitmap_unpremultiply(&dst) + testing.expect_value(t, dst.pixels[0], 64) // 0.25 -> 63.75 + testing.expect_value(t, dst.pixels[1], 128) // 0.5 -> 127.5 + testing.expect_value(t, dst.pixels[2], 191) // 0.75 -> 191.25 + testing.expect_value(t, dst.pixels[3], 128) +} + +@(test) +test_blend_span_sets_premultiplied :: proc(t: ^testing.T) { + dst, _ := bitmap_make(1, 1, 4) + defer bitmap_destroy(&dst) + testing.expect(t, !dst.premultiplied, "bitmap_make starts straight") + blend_span(&dst, 0, 0, {255}, {1, 1, 1, 1}, .Normal) + testing.expect(t, dst.premultiplied, "compositing leaves premultiplied pixels") +} + +@(test) +test_blend_span_alpha_and_coverage_scale :: proc(t: ^testing.T) { + // Opaque black background, opaque white source at half alpha. + dst := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 0, {255}, {1, 1, 1, 0.5}, .Normal) + testing.expect_value(t, dst.pixels[0], 128) + testing.expect_value(t, dst.pixels[3], 255) + + // Half coverage of a fully opaque source is the same thing. + dst2 := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) + defer bitmap_destroy(&dst2) + blend_span(&dst2, 0, 0, {128}, {1, 1, 1, 1}, .Normal) + testing.expect_value(t, dst2.pixels[0], 128) + testing.expect_value(t, dst2.pixels[3], 255) +} + +@(test) +test_blend_span_zero_alpha_changes_nothing :: proc(t: ^testing.T) { + before := []u8{40, 80, 120, 255} + dst := make_bmp(t, 1, 1, 4, before) + defer bitmap_destroy(&dst) + + blend_span(&dst, 0, 0, {255}, {1, 1, 1, 0}, .Normal) + testing.expect(t, slice.equal(dst.pixels, before), "alpha 0 must not change the destination") + + // Nor may zero coverage, whatever the alpha. + blend_span(&dst, 0, 0, {0}, {1, 1, 1, 1}, .Multiply) + testing.expect(t, slice.equal(dst.pixels, before), "coverage 0 must not change the destination") +} + +// A mask of all 255 must be byte-for-byte identical to no mask at all: any +// rounding introduced by the mask multiply would show as a seam at the edge of +// a masked region. +@(test) +test_blend_span_full_mask_is_a_no_op :: proc(t: ^testing.T) { + fill := []u8{40, 80, 120, 200} + a := make_bmp(t, 6, 1, 4, fill) + defer bitmap_destroy(&a) + b := make_bmp(t, 6, 1, 4, fill) + defer bitmap_destroy(&b) + + cov := []u8{255, 200, 128, 64, 1, 0} + mask := []u8{255, 255, 255, 255, 255, 255} + for mode in Blend_Mode { + blend_span(&a, 0, 0, cov, {0.3, 0.6, 0.9, 0.7}, mode) + blend_span(&b, 0, 0, cov, {0.3, 0.6, 0.9, 0.7}, mode, mask) + } + testing.expect(t, slice.equal(a.pixels, b.pixels), "an all-255 mask must change nothing") +} + +@(test) +test_blend_span_mask_scales_and_erases :: proc(t: ^testing.T) { + before := []u8{0, 0, 0, 255} + dst := make_bmp(t, 2, 1, 4, before) + defer bitmap_destroy(&dst) + + // Mask 0 erases the first pixel entirely; mask 128 halves the second. + blend_span(&dst, 0, 0, {255, 255}, {1, 1, 1, 1}, .Normal, {0, 128}) + testing.expect_value(t, dst.pixels[0], 0) + testing.expect_value(t, dst.pixels[3], 255) + testing.expect_value(t, dst.pixels[4], 128) + testing.expect_value(t, dst.pixels[7], 255) +} + +// A span that starts left of the bitmap keeps its coverage and its mask in +// step: pixel 0 must take coverage[2] and mask[2], not coverage[0]. +@(test) +test_blend_span_clips_left_edge_in_step :: proc(t: ^testing.T) { + dst := make_bmp(t, 3, 1, 1, {0}) + defer bitmap_destroy(&dst) + + blend_span(&dst, 0, -2, {0, 0, 255, 128}, {1, 1, 1, 1}, .Normal) + testing.expect_value(t, dst.pixels[0], 255) + testing.expect_value(t, dst.pixels[1], 128) + testing.expect_value(t, dst.pixels[2], 0) // past the end of the coverage run + + dst2 := make_bmp(t, 3, 1, 1, {0}) + defer bitmap_destroy(&dst2) + blend_span(&dst2, 0, -2, {0, 0, 255, 128}, {1, 1, 1, 1}, .Normal, {255, 255, 0, 255}) + testing.expect_value(t, dst2.pixels[0], 0) // mask[2] = 0 + testing.expect_value(t, dst2.pixels[1], 128) // mask[3] = 255 +} + +@(test) +test_blend_span_clips_right_edge :: proc(t: ^testing.T) { + dst := make_bmp(t, 3, 1, 1, {0}) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 2, {255, 255, 255}, {1, 1, 1, 1}, .Normal) + testing.expect_value(t, dst.pixels[0], 0) + testing.expect_value(t, dst.pixels[1], 0) + testing.expect_value(t, dst.pixels[2], 255) +} + +@(test) +test_blend_span_out_of_range_is_ignored :: proc(t: ^testing.T) { + before := []u8{7, 7, 7, 7} + dst := make_bmp(t, 2, 2, 1, before) + defer bitmap_destroy(&dst) + + blend_span(&dst, -1, 0, {255, 255}, {1, 1, 1, 1}, .Normal) + blend_span(&dst, 2, 0, {255, 255}, {1, 1, 1, 1}, .Normal) + blend_span(&dst, 0, 2, {255, 255}, {1, 1, 1, 1}, .Normal) + blend_span(&dst, 0, -5, {255, 255}, {1, 1, 1, 1}, .Normal) + blend_span(&dst, 0, 0, {}, {1, 1, 1, 1}, .Normal) + blend_span(nil, 0, 0, {255}, {1, 1, 1, 1}, .Normal) + testing.expect(t, slice.equal(dst.pixels, before), "out-of-range spans must not write") +} + +// Numbers from a content stream reach the paint colour, so a NaN or an +// infinity must be absorbed here rather than turning a pixel into garbage. +@(test) +test_blend_span_rejects_non_finite_colour :: proc(t: ^testing.T) { + nan := math.nan_f64() + inf := math.INF_F64 + + before := []u8{0, 0, 0, 255} + dst := make_bmp(t, 1, 1, 4, before) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 0, {255}, {1, 1, 1, nan}, .Normal) + testing.expect(t, slice.equal(dst.pixels, before), "a NaN alpha paints nothing") + + // A non-finite or out-of-range component clamps to the end of the range it + // points at instead of escaping into the pixel. + blend_span(&dst, 0, 0, {255}, {inf, -inf, nan, 1}, .Normal) + testing.expect_value(t, dst.pixels[0], 255) + testing.expect_value(t, dst.pixels[1], 0) + testing.expect_value(t, dst.pixels[2], 0) + testing.expect_value(t, dst.pixels[3], 255) +} + +@(test) +test_blend_span_modes_on_opaque_grey :: proc(t: ^testing.T) { + // Multiply by white and Screen by black are both the identity; Multiply by + // black is black and Screen by white is white. + check :: proc(t: ^testing.T, mode: Blend_Mode, src: f64, want: u8, loc := #caller_location) { + dst := make_bmp(t, 1, 1, 1, {128}, loc) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 0, {255}, {src, src, src, 1}, mode) + testing.expectf(t, dst.pixels[0] == want, "%v with source %v: got %v, want %v", + mode, src, dst.pixels[0], want, loc = loc) + } + check(t, .Multiply, 1, 128) + check(t, .Multiply, 0, 0) + check(t, .Screen, 0, 128) + check(t, .Screen, 1, 255) + check(t, .Darken, 1, 128) + check(t, .Lighten, 0, 128) + check(t, .Difference, 0, 128) + check(t, .Normal, 1, 255) +} + +// A greyscale destination takes the source's luminosity, not its red channel. +@(test) +test_blend_span_grey_destination_uses_luminosity :: proc(t: ^testing.T) { + dst := make_bmp(t, 3, 1, 1, {0}) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 0, {255}, {1, 0, 0, 1}, .Normal) // Lum 0.3 -> 76.5 + blend_span(&dst, 0, 1, {255}, {0, 1, 0, 1}, .Normal) // Lum 0.59 -> 150.45 + blend_span(&dst, 0, 2, {255}, {0, 0, 1, 1}, .Normal) // Lum 0.11 -> 28.05 + testing.expect(t, dst.pixels[0] == 76 || dst.pixels[0] == 77, "red luminosity is 0.3") + testing.expect_value(t, dst.pixels[1], 150) + testing.expect_value(t, dst.pixels[2], 28) +} + +@(test) +test_blend_span_rgb_destination_has_no_alpha :: proc(t: ^testing.T) { + dst := make_bmp(t, 1, 1, 3, {255, 255, 255}) + defer bitmap_destroy(&dst) + blend_span(&dst, 0, 0, {255}, {1, 0, 0, 0.5}, .Normal) + testing.expect_value(t, dst.pixels[0], 255) + testing.expect_value(t, dst.pixels[1], 128) + testing.expect_value(t, dst.pixels[2], 128) +} + +// --------------------------------------------------------------------------- +// blend_span_rgba8 +// --------------------------------------------------------------------------- + +@(test) +test_blend_span_rgba8_places_pixels :: proc(t: ^testing.T) { + dst := make_bmp(t, 4, 1, 4, nil) + defer bitmap_destroy(&dst) + + src := []u8{255, 0, 0, 255, 0, 255, 0, 128} + blend_span_rgba8(&dst, 0, 1, src, 1, .Normal) + + testing.expect_value(t, dst.pixels[0], 0) // untouched + testing.expect_value(t, dst.pixels[4], 255) + testing.expect_value(t, dst.pixels[7], 255) + testing.expect_value(t, dst.pixels[8], 0) + testing.expect_value(t, dst.pixels[9], 128) // 0.5 premultiplied green + testing.expect_value(t, dst.pixels[11], 128) + testing.expect_value(t, dst.pixels[12], 0) // untouched +} + +@(test) +test_blend_span_rgba8_alpha_coverage_and_mask :: proc(t: ^testing.T) { + src := []u8{255, 255, 255, 255, 255, 255, 255, 255} + + // Each of the three multipliers must reach the same place in the product. + for k in 0 ..< 3 { + dst := make_bmp(t, 2, 1, 4, {0, 0, 0, 255}) + defer bitmap_destroy(&dst) + switch k { + case 0: blend_span_rgba8(&dst, 0, 0, src, 0.5, .Normal) + case 1: blend_span_rgba8(&dst, 0, 0, src, 1, .Normal, {128, 128}) + case 2: blend_span_rgba8(&dst, 0, 0, src, 1, .Normal, nil, {128, 128}) + } + testing.expectf(t, dst.pixels[0] == 128, "case %d: got %v", k, dst.pixels[0]) + } +} + +@(test) +test_blend_span_rgba8_clips :: proc(t: ^testing.T) { + before := []u8{9, 9, 9, 255, 9, 9, 9, 255} + dst := make_bmp(t, 2, 1, 4, before) + defer bitmap_destroy(&dst) + + blend_span_rgba8(&dst, 1, 0, {255, 255, 255, 255}, 1, .Normal) // y out of range + blend_span_rgba8(&dst, 0, 2, {255, 255, 255, 255}, 1, .Normal) // x past the edge + blend_span_rgba8(&dst, 0, 0, {}, 1, .Normal) + blend_span_rgba8(nil, 0, 0, {255, 255, 255, 255}, 1, .Normal) + testing.expect(t, slice.equal(dst.pixels, before), "clipped runs must not write") + + // A run starting left of zero keeps its source pixels in step. + dst2 := make_bmp(t, 2, 1, 4, nil) + defer bitmap_destroy(&dst2) + blend_span_rgba8(&dst2, 0, -1, {255, 0, 0, 255, 0, 0, 255, 255}, 1, .Normal) + testing.expect_value(t, dst2.pixels[2], 255) // the blue pixel landed at x = 0 + testing.expect_value(t, dst2.pixels[0], 0) + + // ... and so do its coverage and its mask. Entry 0 of each belongs to the + // pixel that was clipped away, so reading it would paint what should be + // blank. + white := []u8{255, 255, 255, 255, 255, 255, 255, 255} + dst3 := make_bmp(t, 2, 1, 4, nil) + defer bitmap_destroy(&dst3) + blend_span_rgba8(&dst3, 0, -1, white, 1, .Normal, {255, 0}) + testing.expect_value(t, dst3.pixels[0], 0) + testing.expect_value(t, dst3.pixels[3], 0) + + dst4 := make_bmp(t, 2, 1, 4, nil) + defer bitmap_destroy(&dst4) + blend_span_rgba8(&dst4, 0, -1, white, 1, .Normal, nil, {255, 0}) + testing.expect_value(t, dst4.pixels[0], 0) + testing.expect_value(t, dst4.pixels[3], 0) +} + +// --------------------------------------------------------------------------- +// blend_bitmap +// --------------------------------------------------------------------------- + +@(test) +test_blend_bitmap_onto_transparent_gives_source :: proc(t: ^testing.T) { + src := make_bmp(t, 2, 2, 4, {100, 50, 25, 200}) + defer bitmap_destroy(&src) + dst := make_bmp(t, 4, 4, 4, nil) + defer bitmap_destroy(&dst) + + blend_bitmap(&dst, &src, 1, 1, 1, .Normal, nil) + + for j in 0 ..< 4 { + for i in 0 ..< 4 { + off := j * dst.stride + i * 4 + inside := i >= 1 && i <= 2 && j >= 1 && j <= 2 + want := inside ? []u8{100, 50, 25, 200} : []u8{0, 0, 0, 0} + testing.expectf(t, slice.equal(dst.pixels[off:off + 4], want), + "pixel (%d,%d): got %v, want %v", i, j, dst.pixels[off:off + 4], want) + } + } +} + +@(test) +test_blend_bitmap_constant_alpha_and_mask :: proc(t: ^testing.T) { + src := make_bmp(t, 1, 1, 4, {255, 0, 0, 255}) + defer bitmap_destroy(&src) + + dst := make_bmp(t, 1, 1, 4, nil) + defer bitmap_destroy(&dst) + blend_bitmap(&dst, &src, 0, 0, 0.5, .Normal, nil) + testing.expect_value(t, dst.pixels[0], 128) + testing.expect_value(t, dst.pixels[3], 128) + + // A one-component mask of 128 does the same job as a constant alpha of 0.5. + mask := make_bmp(t, 1, 1, 1, {128}) + defer bitmap_destroy(&mask) + dst2 := make_bmp(t, 1, 1, 4, nil) + defer bitmap_destroy(&dst2) + blend_bitmap(&dst2, &src, 0, 0, 1, .Normal, &mask) + testing.expect(t, slice.equal(dst.pixels, dst2.pixels), "mask 128 == alpha 0.5") + + // ... and a mask of 255 is no mask at all. + full := make_bmp(t, 1, 1, 1, {255}) + defer bitmap_destroy(&full) + dst3 := make_bmp(t, 1, 1, 4, nil) + defer bitmap_destroy(&dst3) + dst4 := make_bmp(t, 1, 1, 4, nil) + defer bitmap_destroy(&dst4) + blend_bitmap(&dst3, &src, 0, 0, 1, .Normal, &full) + blend_bitmap(&dst4, &src, 0, 0, 1, .Normal, nil) + testing.expect(t, slice.equal(dst3.pixels, dst4.pixels), "an all-255 mask must change nothing") + + // Zero alpha leaves the destination untouched. + dst5 := make_bmp(t, 1, 1, 4, {10, 20, 30, 255}) + defer bitmap_destroy(&dst5) + blend_bitmap(&dst5, &src, 0, 0, 0, .Normal, nil) + testing.expect(t, slice.equal(dst5.pixels, []u8{10, 20, 30, 255}), "alpha 0 changes nothing") +} + +@(test) +test_blend_bitmap_clips_to_destination :: proc(t: ^testing.T) { + src := make_bmp(t, 2, 2, 4, {255, 255, 255, 255}) + defer bitmap_destroy(&src) + + // Overhanging the top-left: only src(1,1) lands, at dst(0,0). + dst := make_bmp(t, 2, 2, 4, nil) + defer bitmap_destroy(&dst) + blend_bitmap(&dst, &src, -1, -1, 1, .Normal, nil) + testing.expect_value(t, dst.pixels[0], 255) + testing.expect_value(t, dst.pixels[4], 0) + testing.expect_value(t, dst.pixels[dst.stride], 0) + + // Entirely outside: nothing at all. + dst2 := make_bmp(t, 2, 2, 4, nil) + defer bitmap_destroy(&dst2) + blend_bitmap(&dst2, &src, 5, 5, 1, .Normal, nil) + blend_bitmap(&dst2, &src, -9, 0, 1, .Normal, nil) + blend_bitmap(&dst2, &src, 0, -9, 1, .Normal, nil) + blend_bitmap(nil, &src, 0, 0, 1, .Normal, nil) + blend_bitmap(&dst2, nil, 0, 0, 1, .Normal, nil) + for b in dst2.pixels { + testing.expect_value(t, b, 0) + } +} + +@(test) +test_blend_bitmap_mixed_component_counts :: proc(t: ^testing.T) { + // An opaque grey group composited onto an RGB page. + src := make_bmp(t, 1, 1, 1, {128}) + defer bitmap_destroy(&src) + dst := make_bmp(t, 1, 1, 3, {0, 0, 0}) + defer bitmap_destroy(&dst) + blend_bitmap(&dst, &src, 0, 0, 1, .Normal, nil) + testing.expect_value(t, dst.pixels[0], 128) + testing.expect_value(t, dst.pixels[1], 128) + testing.expect_value(t, dst.pixels[2], 128) + + // An RGBA group onto a grey page takes the luminosity. + src2 := make_bmp(t, 1, 1, 4, {0, 255, 0, 255}) + defer bitmap_destroy(&src2) + dst2 := make_bmp(t, 1, 1, 1, {0}) + defer bitmap_destroy(&dst2) + blend_bitmap(&dst2, &src2, 0, 0, 1, .Normal, nil) + testing.expect_value(t, dst2.pixels[0], 150) +} + +@(test) +test_blend_bitmap_applies_the_blend_mode :: proc(t: ^testing.T) { + // A white group multiplied onto a grey page is the identity; a black one + // wipes it out. + src := make_bmp(t, 1, 1, 4, {255, 255, 255, 255}) + defer bitmap_destroy(&src) + dst := make_bmp(t, 1, 1, 4, {128, 128, 128, 255}) + defer bitmap_destroy(&dst) + blend_bitmap(&dst, &src, 0, 0, 1, .Multiply, nil) + testing.expect_value(t, dst.pixels[0], 128) + testing.expect_value(t, dst.pixels[3], 255) + + black := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) + defer bitmap_destroy(&black) + dst2 := make_bmp(t, 1, 1, 4, {128, 128, 128, 255}) + defer bitmap_destroy(&dst2) + blend_bitmap(&dst2, &black, 0, 0, 1, .Multiply, nil) + testing.expect_value(t, dst2.pixels[0], 0) + testing.expect_value(t, dst2.pixels[3], 255) +} + +// --------------------------------------------------------------------------- +// mask_from_bitmap (11.6.5) +// --------------------------------------------------------------------------- + +@(test) +test_mask_from_bitmap_luminosity :: proc(t: ^testing.T) { + src := make_bmp(t, 6, 1, 3, nil) + defer bitmap_destroy(&src) + px := [][3]u8{{0, 0, 0}, {128, 128, 128}, {255, 255, 255}, {255, 0, 0}, {0, 255, 0}, {0, 0, 255}} + for p, i in px { + src.pixels[i * 3 + 0] = p[0] + src.pixels[i * 3 + 1] = p[1] + src.pixels[i * 3 + 2] = p[2] + } + + m, err := mask_from_bitmap(&src, true, {0, 0, 0}, nil) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&m) + + testing.expect_value(t, m.n, 1) + testing.expect_value(t, m.width, 6) + testing.expect_value(t, m.height, 1) + testing.expect_value(t, m.pixels[0], 0) + testing.expect_value(t, m.pixels[1], 128) // mid grey stays mid grey + testing.expect_value(t, m.pixels[2], 255) + testing.expect(t, m.pixels[3] == 76 || m.pixels[3] == 77, "red is 0.3") + testing.expect_value(t, m.pixels[4], 150) // 0.59 * 255 = 150.45 + testing.expect_value(t, m.pixels[5], 28) // 0.11 * 255 = 28.05 +} + +// An alpha mask ignores the colour entirely; a bitmap with no alpha channel is +// opaque, so its mask is solid. +@(test) +test_mask_from_bitmap_alpha :: proc(t: ^testing.T) { + src := make_bmp(t, 2, 1, 4, nil) + defer bitmap_destroy(&src) + copy(src.pixels[0:4], []u8{200, 100, 50, 200}) + copy(src.pixels[4:8], []u8{0, 0, 0, 0}) + + m, err := mask_from_bitmap(&src, false, {1, 1, 1}, nil) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&m) + testing.expect_value(t, m.pixels[0], 200) + testing.expect_value(t, m.pixels[1], 0) + + opaque := make_bmp(t, 1, 1, 3, {10, 20, 30}) + defer bitmap_destroy(&opaque) + m2, err2 := mask_from_bitmap(&opaque, false, {0, 0, 0}, nil) + testing.expect_value(t, err2, Error.None) + defer bitmap_destroy(&m2) + testing.expect_value(t, m2.pixels[0], 255) +} + +// A luminosity mask composites the group onto an opaque backdrop first, so the +// backdrop is what shows through wherever the group painted nothing. This is +// why /BC defaults to black: unpainted area masks everything out. +@(test) +test_mask_from_bitmap_luminosity_uses_the_backdrop :: proc(t: ^testing.T) { + src := make_bmp(t, 2, 1, 4, nil) + defer bitmap_destroy(&src) + copy(src.pixels[0:4], []u8{128, 128, 128, 128}) // premultiplied white at 50% + copy(src.pixels[4:8], []u8{0, 0, 0, 0}) // nothing painted + + black, e1 := mask_from_bitmap(&src, true, {0, 0, 0}, nil) + testing.expect_value(t, e1, Error.None) + defer bitmap_destroy(&black) + testing.expect_value(t, black.pixels[0], 128) + testing.expect_value(t, black.pixels[1], 0) + + white, e2 := mask_from_bitmap(&src, true, {1, 1, 1}, nil) + testing.expect_value(t, e2, Error.None) + defer bitmap_destroy(&white) + testing.expect_value(t, white.pixels[0], 255) // 0.5 white + 0.5 white + testing.expect_value(t, white.pixels[1], 255) +} + +// The same pixel written as straight alpha must give the same mask: the flag, +// not the caller, decides whether the a*C factor is already there. +@(test) +test_mask_from_bitmap_honours_the_premultiplied_flag :: proc(t: ^testing.T) { + straight := make_bmp(t, 1, 1, 4, {255, 255, 255, 128}) + defer bitmap_destroy(&straight) + straight.premultiplied = false + + m, err := mask_from_bitmap(&straight, true, {0, 0, 0}, nil) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&m) + testing.expect_value(t, m.pixels[0], 128) + + m2, err2 := mask_from_bitmap(&straight, true, {1, 1, 1}, nil) + testing.expect_value(t, err2, Error.None) + defer bitmap_destroy(&m2) + testing.expect_value(t, m2.pixels[0], 255) +} + +@(test) +test_mask_from_bitmap_rejects_bad_input :: proc(t: ^testing.T) { + _, err := mask_from_bitmap(nil, true, {0, 0, 0}, nil) + testing.expect_value(t, err, Error.Invalid_Argument) + + empty: Bitmap + _, err2 := mask_from_bitmap(&empty, false, {0, 0, 0}, nil) + testing.expect_value(t, err2, Error.Invalid_Argument) +} + +// /TR is applied to every mask value. A type 2 function with C0 = 1, C1 = 0 and +// N = 1 is the inverting transfer, which is the one real files use to turn a +// luminosity mask inside out. +@(test) +test_mask_from_bitmap_transfer_function :: proc(t: ^testing.T) { + domain := [2]f64{0, 1} + range := [2]f64{0, 1} + c0 := [1]f64{1} + c1 := [1]f64{0} + fn := Function{ + kind = .Exponential, + n_in = 1, + n_out = 1, + domain = domain[:], + range = range[:], + c0 = c0[:], + c1 = c1[:], + exponent = 1, + } + + src := make_bmp(t, 3, 1, 4, nil) + defer bitmap_destroy(&src) + copy(src.pixels[0:4], []u8{0, 0, 0, 0}) + copy(src.pixels[4:8], []u8{0, 0, 0, 200}) + copy(src.pixels[8:12], []u8{0, 0, 0, 255}) + + m, err := mask_from_bitmap(&src, false, {0, 0, 0}, &fn) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&m) + testing.expect_value(t, m.pixels[0], 255) + testing.expect_value(t, m.pixels[1], 55) + testing.expect_value(t, m.pixels[2], 0) + + // A function with the wrong arity is ignored rather than obeyed: a broken + // /TR must not blank the mask. + bad := fn + bad.n_in = 2 + m2, err2 := mask_from_bitmap(&src, false, {0, 0, 0}, &bad) + testing.expect_value(t, err2, Error.None) + defer bitmap_destroy(&m2) + testing.expect_value(t, m2.pixels[1], 200) +} + +@(test) +test_blend_transfer_table_identity :: proc(t: ^testing.T) { + lut := blend_transfer_table(nil) + for i in 0 ..< 256 { + testing.expect_value(t, lut[i], u8(i)) + } +} + +// --------------------------------------------------------------------------- +// Quantisation +// --------------------------------------------------------------------------- + +@(test) +test_blend_quantize :: proc(t: ^testing.T) { + testing.expect_value(t, blend_quantize(0), 0) + testing.expect_value(t, blend_quantize(1), 255) + testing.expect_value(t, blend_quantize(0.5), 128) // 127.5 rounds up + testing.expect_value(t, blend_quantize(2), 255) // clamped + testing.expect_value(t, blend_quantize(-1), 0) // clamped + testing.expect_value(t, blend_quantize(math.nan_f64()), 0) // NaN must not wrap + testing.expect_value(t, blend_quantize(math.INF_F64), 255) + testing.expect_value(t, blend_quantize(1.0 / 255), 1) +} diff --git a/cmap.odin b/cmap.odin new file mode 100644 index 0000000..d29ff7a --- /dev/null +++ b/cmap.odin @@ -0,0 +1,957 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:slice" +import "core:strings" +import "core:unicode/utf8" + +// CMaps (ISO 32000-2, 9.7.5): the mapping from bytes in a string to CIDs, and +// the /ToUnicode mapping from character codes to text. +// +// A CMap is variable-width: the codespace ranges say how many bytes the next +// code takes, and the same font can mix one- and two-byte codes. Decoding a +// string therefore cannot be a simple loop over bytes. +// +// The file is written in a PostScript-like syntax, which the COS tokeniser in +// lexer.odin already reads: a CMap is a run of ` dst` triples wrapped +// in `beginXXX`/`endXXX` keywords. Only the sections that carry a mapping are +// interpreted; the surrounding `findresource`/`dict`/`begin`/`def` scaffolding +// is stepped over, and so is any section this reader does not know. +// +// PREDEFINED CMAPS -- THE ONE DELIBERATE GAP +// +// Identity-H and Identity-V are built in and exact. The CJK registries +// (Adobe-Japan1-*, Adobe-GB1-*, Adobe-CNS1-*, Adobe-Korea1-*, UniJIS-UCS2-H +// and the rest) are tables of tens of thousands of entries each; shipping them +// would multiply the size of this library several times over for a class of +// document it cannot otherwise render anyway, so they are NOT included. Naming +// one of them yields a two-byte identity CMap with `predefined_fallback` set +// on the returned Cmap and a `.Bad_Resource` repair recorded on the Document. +// That is the right shape for the overwhelmingly common case -- a two-byte +// encoding whose CIDs happen to be within a constant of the codes -- and wrong +// in detail. A caller that cares must test the flag; nothing else in the +// library can tell the difference. +// +// Everything a Cmap points at is allocated from the allocator passed to +// `cmap_parse` / `cmap_identity`, or from the Document's arena when it came +// from `cmap_load`. Nothing here is freed individually. + +// How far a /UseCMap chain is followed, and how deep a nest of CMap streams +// `cmap_load` will open. Legitimate files use one level; more than this is a +// file trying to make the loader recurse. +MAX_CMAP_USE_DEPTH :: 8 +// Tokens read from one CMap before parsing gives up. A CMap is a data file, +// not a program, so this only ever fires on hostile input. +MAX_CMAP_TOKENS :: 1 << 22 +// Entries of each kind (singles, ranges, texts) one CMap may hold. +MAX_CMAP_ENTRIES :: 1 << 17 +// Bytes of UTF-8 destination text one CMap may hold. +MAX_CMAP_TEXT_BYTES :: 1 << 22 +// Codes one `bfrange` may be expanded into. `<0000> <0020>` is legal +// syntax and would otherwise cost 65536 entries per line; a file may write +// many such lines. +MAX_CMAP_RANGE_CODES :: 1 << 16 +// UTF-16 code units in one destination string. A ligature or a decomposed +// sequence is a handful; anything longer is not text. +MAX_CMAP_DST_RUNES :: 32 +// Overlapping ranges are malformed but occur, so a range lookup that lands +// between two entries scans back this far before giving up. +CMAP_RANGE_BACKSCAN :: 32 + +// Cmap_Codespace is one codespace range, all of whose codes are `nbytes` long. +Cmap_Codespace :: struct { + lo: u32, + hi: u32, + nbytes: int, +} + +// Cmap_Range maps a contiguous run of codes onto consecutive destinations. +Cmap_Range :: struct { + lo: u32, + hi: u32, + dst: u32, +} + +// Cmap_Single maps one code. Kept separate from ranges because `cidchar` and +// `bfchar` sections are common and dense enough that a binary search over +// singles beats widening every one into a range. +Cmap_Single :: struct { + code: u32, + dst: u32, +} + +// Cmap_Text maps one code to a string, which /ToUnicode needs: a single glyph +// can stand for a ligature ("ffi") or a decomposed sequence. +Cmap_Text :: struct { + code: u32, + // Offset and length into Cmap.text_pool, as UTF-8. + off: u32, + len: u32, +} + +Cmap :: struct { + name: string, + // 0 horizontal, 1 vertical. + wmode: int, + + // Sorted, so lookup is a binary search. + codespace: []Cmap_Codespace, + singles: []Cmap_Single, + ranges: []Cmap_Range, + + // /ToUnicode only. + texts: []Cmap_Text, + text_pool: []u8, + + // /UseCMap, applied when this CMap has no entry of its own. + parent: ^Cmap, + // True for the Identity-H / Identity-V CMaps, which need no tables. + identity: bool, + // True when a predefined CMap this library does not ship was asked for and + // a two-byte identity mapping was substituted. See the note above: the + // result is usable but is not the mapping the file named. + predefined_fallback: bool, +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// cmap_load builds a CMap from the object a font names it with: a `Name` for a +// predefined CMap, or a stream holding an embedded one. The result belongs to +// the Document. +// +// A name this library does not ship falls back to a two-byte identity mapping +// (see the file comment); only an object that is neither a name nor a stream +// is an error, because there is then genuinely nothing to hand back. +cmap_load :: proc(doc: ^Document, obj: Object) -> (^Cmap, Error) { + ctx := Cmap_Load_Ctx{} + return cmap_load_ctx(doc, obj, &ctx) +} + +// cmap_parse reads a CMap out of bytes -- an embedded CMap stream's decoded +// payload, or a /ToUnicode stream. +// +// `doc` is used only to resolve a `usecmap` operand and to record repairs, and +// may be nil. The returned Cmap and everything it points at are allocated with +// `allocator` and belong to the caller. +cmap_parse :: proc(doc: ^Document, data: []byte, allocator := context.allocator) -> (^Cmap, Error) { + return cmap_parse_data(doc, data, nil, allocator) +} + +// cmap_identity returns the Identity-H (or Identity-V) CMap: two-byte codes, +// CID equal to code. Allocated with `allocator` and owned by the caller. +cmap_identity :: proc(vertical: bool, allocator := context.allocator) -> ^Cmap { + cm := new(Cmap, allocator) + if cm == nil { return nil } + cs := make([]Cmap_Codespace, 1, allocator) + if cs == nil { + free(cm, allocator) + return nil + } + cs[0] = Cmap_Codespace{lo = 0, hi = 0xFFFF, nbytes = 2} + cm.name = vertical ? "Identity-V" : "Identity-H" + cm.wmode = vertical ? 1 : 0 + cm.identity = true + cm.codespace = cs + return cm +} + +@(private = "file") +Cmap_Load_Ctx :: struct { + depth: int, + // Stream objects already open further up the stack. A /UseCMap cycle is + // two streams naming each other, which the depth cap alone would also + // stop, but stopping at the repeat keeps the diagnosis honest. + seen: [MAX_CMAP_USE_DEPTH]rawptr, + nseen: int, +} + +@(private = "file") +cmap_load_ctx :: proc(doc: ^Document, obj: Object, ctx: ^Cmap_Load_Ctx) -> (^Cmap, Error) { + allocator := context.allocator + if doc != nil { + allocator = doc.allocator + } + + #partial switch v in resolve(doc, obj) { + case Name: + cm := cmap_predefined(doc, string(v), allocator) + if cm == nil { return nil, .Out_Of_Memory } + return cm, .None + + case ^Stream: + if ctx.depth >= MAX_CMAP_USE_DEPTH { + add_repair(doc, .Recursion_Limit_Hit, 0, v.ref) + return nil, .Bad_Cmap + } + for i in 0 ..< ctx.nseen { + if ctx.seen[i] == rawptr(v) { + add_repair(doc, .Bad_Resource, 0, v.ref) + return nil, .Bad_Cmap + } + } + if ctx.nseen < len(ctx.seen) { + ctx.seen[ctx.nseen] = rawptr(v) + ctx.nseen += 1 + } + ctx.depth += 1 + defer ctx.depth -= 1 + + // The dictionary's /UseCMap is read before the body so that the body's + // own codespace ranges, if it has any, win over the inherited ones. + parent: ^Cmap + if use := dict_get_raw(v.dict, "UseCMap"); use != nil { + parent, _ = cmap_load_ctx(doc, use, ctx) + } + + data, derr := stream_data(doc, v) + if derr != .None { + return nil, .Bad_Cmap + } + cm, perr := cmap_parse_data(doc, data, parent, allocator) + if cm == nil { + return nil, perr + } + // ISO 32000-2, 9.7.5.3: the stream dictionary may carry /WMode and + // /CMapName too. The body has already spoken if it set them. + if w, ok := to_int(dict_get(doc, v.dict, "WMode")); ok && cm.wmode == 0 { + cm.wmode = w != 0 ? 1 : 0 + } + if cm.name == "" { + if n := dict_name(doc, v.dict, "CMapName"); n != "" { + cm.name = n + } + } + return cm, .None + } + + return nil, .Bad_Cmap +} + +// cmap_predefined resolves a predefined CMap name. Only the two identity CMaps +// are shipped; see the file comment for why, and what the others become. +@(private = "file") +cmap_predefined :: proc(doc: ^Document, name: string, allocator: mem.Allocator) -> ^Cmap { + switch name { + case "Identity-H": return cmap_identity(false, allocator) + case "Identity-V": return cmap_identity(true, allocator) + } + // Every predefined vertical CMap's name ends in "-V", which is enough to + // get the writing mode right even when the mapping cannot be. + cm := cmap_identity(strings.has_suffix(name, "-V"), allocator) + if cm == nil { return nil } + cm.name = strings.clone(name, allocator) + cm.predefined_fallback = true + add_repair(doc, .Bad_Resource, 0) + return cm +} + +// --------------------------------------------------------------------------- +// Lookup +// --------------------------------------------------------------------------- + +// cmap_lookup maps a character code to its destination value: a CID for an +// encoding CMap, and nothing at all for a /ToUnicode CMap, whose destinations +// are strings and are read with `cmap_lookup_text`. +// +// A code with no entry of its own is looked for in the /UseCMap parent. +cmap_lookup :: proc(cm: ^Cmap, code: u32) -> (dst: u32, ok: bool) { + c := cm + for _ in 0 ..< MAX_CMAP_USE_DEPTH { + if c == nil { break } + if d, found := cmap_lookup_local(c, code); found { + return d, true + } + // An identity CMap has no tables at all; it answers for every code. + if c.identity { + return code, true + } + c = c.parent + } + return 0, false +} + +// cmap_lookup_local answers from this CMap's own tables only. +@(private = "file") +cmap_lookup_local :: proc(c: ^Cmap, code: u32) -> (dst: u32, ok: bool) { + if i, found := cmap_find_single(c.singles, code); found { + return c.singles[i].dst, true + } + if i, found := cmap_find_range(c.ranges, code); found { + r := c.ranges[i] + // In u64 so that a range whose destination runs off the top of u32 + // cannot overflow the arithmetic on the way. + return u32(u64(r.dst) + u64(code - r.lo)), true + } + return 0, false +} + +// cmap_lookup_text maps a character code to the text it stands for, as UTF-8. +// This is the /ToUnicode direction, and the string may be several code points +// long: one glyph can be a ligature or a decomposed sequence. +// +// The returned string is a view into the Cmap and lives exactly as long as it +// does. It is not allocated, and must not be freed. +cmap_lookup_text :: proc(cm: ^Cmap, code: u32) -> (utf8: string, ok: bool) { + c := cm + for _ in 0 ..< MAX_CMAP_USE_DEPTH { + if c == nil { break } + if i, found := cmap_find_text(c.texts, code); found { + t := c.texts[i] + end := int(t.off) + int(t.len) + if end <= len(c.text_pool) { + return string(c.text_pool[t.off:end]), true + } + } + c = c.parent + } + return "", false +} + +// cmap_next_code reads the next character code from a string. +// +// The code's width is whatever the codespace ranges say it is, so a CMap that +// mixes one- and two-byte codes segments correctly. Bytes that match no range +// still consume a whole code's worth, so a caller looping over a string always +// makes progress; `nbytes` is 0 only for an empty string. How wide that +// invalid code is follows ISO 32000-2, 9.7.6.3 -- see below. +cmap_next_code :: proc(cm: ^Cmap, s: []byte) -> (code: u32, nbytes: int) { + if len(s) == 0 { return 0, 0 } + // With no CMap at all a code is a byte, which is what a simple font wants. + if cm == nil { return u32(s[0]), 1 } + + best := 0 + shortest := 0 + for cs in cm.codespace { + if cs.nbytes < 1 || cs.nbytes > 4 { continue } + if shortest == 0 || cs.nbytes < shortest { + shortest = cs.nbytes + } + // The longest match wins: a two-byte range and a one-byte range can + // both admit the first byte, and the wider code is the intended one. + if cs.nbytes > best && cs.nbytes <= len(s) && cmap_codespace_contains(cs, s[:cs.nbytes]) { + best = cs.nbytes + } + } + if best > 0 { + return cmap_be_code(s[:best]), best + } + + // No range admits these bytes as a code. ISO 32000-2, 9.7.6.3 gives two + // rules for that case, and they do not consume the same number of bytes: + // + // a) the first byte matches the first byte of no range at all -- then + // "the range having the shortest codes shall be chosen"; + // b) otherwise there is a partial match, and the code so far is matched + // "against the beginnings of all longer codespace ranges until the + // longest such partial match has been found. The length of the codes + // in the chosen codespace range determines the total number of bytes + // to consume". + // + // Rule (b) is the one that matters: a lead byte that opens a two-byte + // range but is not completed by the byte after it must still consume two. + // Consuming one instead does not merely drop a glyph -- every code after + // it in the string is read at the wrong offset, so one bad byte turns the + // rest of the line into rubbish. + n := 0 + for k := 1; k <= 4 && k <= len(s); k += 1 { + w := 0 + for cs in cm.codespace { + if cs.nbytes <= k || cs.nbytes > 4 { continue } + if !cmap_codespace_prefix(cs, s[:k]) { continue } + // Same tie-break as a complete match above: where two ranges are + // equally partial matches, the wider code is the intended one. + w = max(w, cs.nbytes) + } + if w == 0 { break } + n = w + } + if n == 0 { + n = shortest + if n == 0 { + // No codespace section at all. Two bytes for an identity CMap, + // which is what Identity-H means, and one byte otherwise. + n = cm.identity ? 2 : 1 + } + } + n = min(n, len(s)) + return cmap_be_code(s[:n]), n +} + +@(private = "file") +cmap_codespace_contains :: proc(cs: Cmap_Codespace, s: []byte) -> bool { + if len(s) != cs.nbytes { return false } + return cmap_codespace_bytes_in(cs, s) +} + +// cmap_codespace_prefix reports whether `s` could be the beginning of a code +// in this range: the range is wider than `s`, and every byte of `s` is inside +// its position's interval. That is the partial match of 9.7.6.3 b). +@(private = "file") +cmap_codespace_prefix :: proc(cs: Cmap_Codespace, s: []byte) -> bool { + if len(s) >= cs.nbytes { return false } + return cmap_codespace_bytes_in(cs, s) +} + +// cmap_codespace_bytes_in tests the leading bytes of a code against the range. +// ISO 32000-2, 9.7.6.2: a code is inside a codespace range when *each* of its +// bytes is inside the corresponding byte's range, which is not the same as the +// numeric interval -- <8140> <9ffc> excludes 0x8200. +@(private = "file") +cmap_codespace_bytes_in :: proc(cs: Cmap_Codespace, s: []byte) -> bool { + for i in 0 ..< min(len(s), cs.nbytes) { + sh := uint(8 * (cs.nbytes - 1 - i)) + lo := byte((cs.lo >> sh) & 0xFF) + hi := byte((cs.hi >> sh) & 0xFF) + if s[i] < lo || s[i] > hi { return false } + } + return true +} + +@(private = "file") +cmap_be_code :: proc(s: []byte) -> u32 { + v: u32 + for b in s[:min(len(s), 4)] { + v = v << 8 | u32(b) + } + return v +} + +@(private = "file") +cmap_find_single :: proc(a: []Cmap_Single, code: u32) -> (int, bool) { + lo, hi := 0, len(a) + for lo < hi { + mid := lo + (hi - lo) / 2 + if a[mid].code < code { lo = mid + 1 } else { hi = mid } + } + if lo < len(a) && a[lo].code == code { return lo, true } + return 0, false +} + +@(private = "file") +cmap_find_text :: proc(a: []Cmap_Text, code: u32) -> (int, bool) { + lo, hi := 0, len(a) + for lo < hi { + mid := lo + (hi - lo) / 2 + if a[mid].code < code { lo = mid + 1 } else { hi = mid } + } + if lo < len(a) && a[lo].code == code { return lo, true } + return 0, false +} + +@(private = "file") +cmap_find_range :: proc(a: []Cmap_Range, code: u32) -> (int, bool) { + // The greatest index whose `lo` is at or below the code, then back up: the + // ranges are sorted by `lo` but a malformed CMap may overlap them, so the + // first candidate is not always the one that contains the code. + lo, hi := 0, len(a) + for lo < hi { + mid := lo + (hi - lo) / 2 + if a[mid].lo <= code { lo = mid + 1 } else { hi = mid } + } + steps := 0 + for i := lo - 1; i >= 0 && steps < CMAP_RANGE_BACKSCAN; i -= 1 { + steps += 1 + if a[i].hi >= code { return i, true } + } + return 0, false +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +@(private = "file") +Cmap_Section :: enum u8 { + None, + Codespace, + Cidrange, + Cidchar, + Bfrange, + Bfchar, + // A `beginXXX` this reader does not interpret. Its contents are stepped + // over rather than being mistaken for the enclosing CMap's operands. + Skip, +} + +@(private = "file") +Cmap_Operand_Kind :: enum u8 { None, Num, Str, Nam, Arr } + +@(private = "file") +Cmap_Operand :: struct { + kind: Cmap_Operand_Kind, + num: i64, + str: []byte, + nam: string, + arr: [][]byte, +} + +// Build entries carry the order they were written in, so that a code defined +// twice -- a broad `bfrange` and then a `bfchar` correcting one code out of +// it, which real producers emit -- keeps the later definition. +@(private = "file") +Cmap_Build_Single :: struct { + code: u32, + dst: u32, + seq: u32, +} + +@(private = "file") +Cmap_Build_Text :: struct { + code: u32, + off: u32, + len: u32, + seq: u32, +} + +@(private = "file") +Cmap_Builder :: struct { + doc: ^Document, + allocator: mem.Allocator, // where the finished Cmap goes + scratch: mem.Allocator, // dies with the parse + + codespace: [dynamic]Cmap_Codespace, + singles: [dynamic]Cmap_Build_Single, + ranges: [dynamic]Cmap_Range, + texts: [dynamic]Cmap_Build_Text, + pool: [dynamic]u8, + + name: string, + wmode: int, + has_wmode: bool, + use_name: string, + seq: u32, +} + +@(private = "file") +cmap_parse_data :: proc( + doc: ^Document, + data: []byte, + parent: ^Cmap, + allocator: mem.Allocator, +) -> (^Cmap, Error) { + // Tokenising a CMap materialises every hex string in it, and nearly all of + // those are read for a number and thrown away. They go in an arena that is + // dropped at the end of the parse rather than into the Document's, which + // cannot free them. + scratch: virtual.Arena + if virtual.arena_init_growing(&scratch) != nil { + return nil, .Out_Of_Memory + } + defer virtual.arena_destroy(&scratch) + sa := virtual.arena_allocator(&scratch) + + b := Cmap_Builder{ + doc = doc, + allocator = allocator, + scratch = sa, + codespace = make([dynamic]Cmap_Codespace, sa), + singles = make([dynamic]Cmap_Build_Single, sa), + ranges = make([dynamic]Cmap_Range, sa), + texts = make([dynamic]Cmap_Build_Text, sa), + pool = make([dynamic]u8, sa), + } + cmap_run(&b, data) + + cm := new(Cmap, allocator) + if cm == nil { return nil, .Out_Of_Memory } + cm.parent = parent + + // A `usecmap` operator in the body names a predefined CMap. It cannot name + // an embedded one -- there is no resource machinery here to look it up in + // -- so this can never recurse back into a stream, and the /UseCMap of the + // stream dictionary, which can, has already had its say. + if b.use_name != "" && cm.parent == nil { + cm.parent = cmap_predefined(doc, b.use_name, allocator) + } + + cmap_finish(&b, cm) + return cm, .None +} + +// cmap_run is the whole of the CMap grammar this reader understands. +@(private = "file") +cmap_run :: proc(b: ^Cmap_Builder, data: []byte) { + lex := lexer_make(data) + section := Cmap_Section.None + pend: [3]Cmap_Operand + np := 0 + // The last two operands seen outside a section, which is all that `def` + // and `usecmap` need to look back at. + op0, op1: Cmap_Operand + + for fuel := MAX_CMAP_TOKENS; fuel > 0; fuel -= 1 { + tok := lexer_next(&lex, b.scratch) + if tok.kind == .EOF { + // An unterminated section simply ends here, with whatever it had. + return + } + + if tok.kind == .Keyword { + // A section this reader does not interpret runs to its own + // `endXXX`, or to the next section: the operators inside it belong + // to it, and a `/WMode 1 def` sitting in a `usematrix` section is + // not this CMap's writing mode. + if section == .Skip && + !strings.has_prefix(tok.text, "end") && !strings.has_prefix(tok.text, "begin") { + continue + } + // Any other keyword closes an open section, terminated or not: a + // file that writes `begincidchar` without closing the + // `begincidrange` before it has still said where the first ended. + section = .None + np = 0 + + switch tok.text { + case "endcmap": + return + case "begincodespacerange": + section = .Codespace + case "begincidrange": + section = .Cidrange + case "begincidchar": + section = .Cidchar + case "beginbfrange": + section = .Bfrange + case "beginbfchar": + section = .Bfchar + case "usecmap": + if op1.kind == .Nam { + b.use_name = op1.nam + } + case "def": + if op0.kind == .Nam && op0.nam == "WMode" && op1.kind == .Num { + b.wmode = op1.num != 0 ? 1 : 0 + b.has_wmode = true + } + if op0.kind == .Nam && op0.nam == "CMapName" && op1.kind == .Nam { + b.name = strings.clone(op1.nam, b.allocator) + } + case "begincmap": + // Scaffolding; the sections inside it are what matter. + case: + // `beginnotdefrange`, `beginusematrix`, `begincidsysteminfo`: + // sections whose contents must not be read as anything else. + // Bare `begin` is PostScript scaffolding, not a section. + if len(tok.text) > len("begin") && strings.has_prefix(tok.text, "begin") { + section = .Skip + } + } + op0, op1 = Cmap_Operand{}, Cmap_Operand{} + continue + } + + if section == .Skip { + continue + } + + o := cmap_operand(&lex, tok, b.scratch) + if section == .None { + op0, op1 = op1, o + continue + } + if o.kind == .None { + continue + } + + pend[np] = o + np += 1 + arity := 3 if section == .Cidrange || section == .Bfrange else 2 + if np >= arity { + cmap_emit(b, section, pend[:arity]) + np = 0 + } + } +} + +@(private = "file") +cmap_operand :: proc(lex: ^Lexer, tok: Token, allocator: mem.Allocator) -> Cmap_Operand { + #partial switch tok.kind { + case .String: + return Cmap_Operand{kind = .Str, str = tok.bytes} + case .Integer, .Real: + return Cmap_Operand{kind = .Num, num = tok.int_value} + case .Name: + return Cmap_Operand{kind = .Nam, nam = tok.text} + case .Array_Open: + return cmap_operand_array(lex, allocator) + } + return Cmap_Operand{} +} + +// cmap_operand_array reads the `[ ...]` destination form of bfrange. +@(private = "file") +cmap_operand_array :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Cmap_Operand { + items := make([dynamic][]byte, allocator) + for _ in 0 ..< MAX_CMAP_RANGE_CODES { + tok := lexer_next(lex, allocator) + #partial switch tok.kind { + case .EOF, .Array_Close: + return Cmap_Operand{kind = .Arr, arr = items[:]} + case .String: + append(&items, tok.bytes) + case .Keyword: + // The array never closed. Hand the keyword back to the main loop + // so the section it opens is not swallowed. + lex.pos = tok.start + return Cmap_Operand{kind = .Arr, arr = items[:]} + } + } + return Cmap_Operand{kind = .Arr, arr = items[:]} +} + +@(private = "file") +cmap_emit :: proc(b: ^Cmap_Builder, section: Cmap_Section, ops: []Cmap_Operand) { + switch section { + case .Codespace: + if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Str { return } + lo, hi := ops[0].str, ops[1].str + // The two ends of a codespace range are required to be the same width; + // when they are not, the narrower one is the only width both describe. + n := min(len(lo), len(hi)) + if n < 1 { return } + n = min(n, 4) + if len(b.codespace) >= MAX_CMAP_ENTRIES { return } + cs := Cmap_Codespace{lo = cmap_be_code(lo[:n]), hi = cmap_be_code(hi[:n]), nbytes = n} + // A range that runs backwards in any byte position describes nothing. + for i in 0 ..< n { + sh := uint(8 * (n - 1 - i)) + if byte((cs.lo >> sh) & 0xFF) > byte((cs.hi >> sh) & 0xFF) { return } + } + append(&b.codespace, cs) + + case .Cidrange: + if len(ops) < 3 || ops[0].kind != .Str || ops[1].kind != .Str || ops[2].kind != .Num { return } + lo := cmap_be_code(ops[0].str) + hi := cmap_be_code(ops[1].str) + if lo > hi { return } + if ops[2].num < 0 || ops[2].num > 0xFFFF_FFFF { return } + if len(b.ranges) >= MAX_CMAP_ENTRIES { return } + append(&b.ranges, Cmap_Range{lo = lo, hi = hi, dst = u32(ops[2].num)}) + + case .Cidchar: + if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Num { return } + if ops[1].num < 0 || ops[1].num > 0xFFFF_FFFF { return } + cmap_add_single(b, cmap_be_code(ops[0].str), u32(ops[1].num)) + + case .Bfrange: + if len(ops) < 3 || ops[0].kind != .Str || ops[1].kind != .Str { return } + lo := cmap_be_code(ops[0].str) + hi := cmap_be_code(ops[1].str) + if lo > hi { return } + count := u64(hi) - u64(lo) + 1 + count = min(count, MAX_CMAP_RANGE_CODES) + #partial switch ops[2].kind { + case .Str: + cmap_add_bfrange_string(b, lo, int(count), ops[2].str) + case .Arr: + n := min(int(count), len(ops[2].arr)) + for i in 0 ..< n { + cmap_add_text(b, lo + u32(i), ops[2].arr[i]) + } + } + + case .Bfchar: + if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Str { return } + cmap_add_text(b, cmap_be_code(ops[0].str), ops[1].str) + + case .None, .Skip: + // Not reachable: the caller only emits for a data section. + } +} + +// cmap_add_bfrange_string expands the ` ` form. ISO 32000-2, +// 9.10.3: the destination is incremented once per code, in its last code unit. +@(private = "file") +cmap_add_bfrange_string :: proc(b: ^Cmap_Builder, lo: u32, count: int, dst: []byte) { + runes: [MAX_CMAP_DST_RUNES]rune + n := cmap_utf16_runes(dst, runes[:]) + if n == 0 { return } + base := i64(runes[n - 1]) + + buf: [MAX_CMAP_DST_RUNES * 4]byte + for i in 0 ..< count { + r := rune(base + i64(i)) + // Past the end of Unicode, or onto a surrogate: the source range ran + // further than its destination could follow. + if r > 0x10FFFF || (r >= 0xD800 && r <= 0xDFFF) { + r = utf8.RUNE_ERROR + } + runes[n - 1] = r + k := 0 + for j in 0 ..< n { + enc, sz := utf8.encode_rune(runes[j]) + copy(buf[k:], enc[:sz]) + k += sz + } + // Stop as soon as the entry could not be stored. The caps are one + // level down, in cmap_add_text_bytes, and this loop is what has to be + // bounded by them: `<0000> <0020>` is 65536 iterations, the + // caps are full after two such lines, and nothing bounds the number of + // lines but the token fuel -- 1.4 million of them fit, which is half + // an hour of expanding codes that are all thrown away. A 30 KB file + // compresses down to that. + if !cmap_add_text_bytes(b, lo + u32(i), buf[:k]) { return } + } +} + +// cmap_add_text records a UTF-16BE destination string as UTF-8. +@(private = "file") +cmap_add_text :: proc(b: ^Cmap_Builder, code: u32, dst: []byte) { + runes: [MAX_CMAP_DST_RUNES]rune + n := cmap_utf16_runes(dst, runes[:]) + if n == 0 { return } + buf: [MAX_CMAP_DST_RUNES * 4]byte + k := 0 + for j in 0 ..< n { + enc, sz := utf8.encode_rune(runes[j]) + copy(buf[k:], enc[:sz]) + k += sz + } + cmap_add_text_bytes(b, code, buf[:k]) +} + +// cmap_add_text_bytes stores one code -> text mapping, and reports whether it +// did: a caller expanding a range needs to know that the CMap is full, or it +// goes on generating entries that are all discarded. +@(private = "file") +cmap_add_text_bytes :: proc(b: ^Cmap_Builder, code: u32, utf8_bytes: []byte) -> bool { + if len(utf8_bytes) == 0 { return false } + if len(b.texts) >= MAX_CMAP_ENTRIES { return false } + if len(b.pool) + len(utf8_bytes) > MAX_CMAP_TEXT_BYTES { return false } + off := u32(len(b.pool)) + append(&b.pool, ..utf8_bytes) + append(&b.texts, Cmap_Build_Text{code = code, off = off, len = u32(len(utf8_bytes)), seq = b.seq}) + b.seq += 1 + return true +} + +@(private = "file") +cmap_add_single :: proc(b: ^Cmap_Builder, code: u32, dst: u32) { + if len(b.singles) >= MAX_CMAP_ENTRIES { return } + append(&b.singles, Cmap_Build_Single{code = code, dst = dst, seq = b.seq}) + b.seq += 1 +} + +// cmap_utf16_runes decodes a UTF-16BE destination string. Unpaired surrogates +// and a trailing half code unit each become U+FFFD, so the result is always +// encodable as valid UTF-8 (ISO 32000-2, 9.10.3 requires the destination of a +// /ToUnicode mapping to be UTF-16BE). +@(private = "file") +cmap_utf16_runes :: proc(src: []byte, out: []rune) -> int { + n := 0 + i := 0 + for i + 1 < len(src) && n < len(out) { + u := u16(src[i]) << 8 | u16(src[i + 1]) + i += 2 + switch { + case u >= 0xD800 && u <= 0xDBFF: + if i + 1 < len(src) { + v := u16(src[i]) << 8 | u16(src[i + 1]) + if v >= 0xDC00 && v <= 0xDFFF { + i += 2 + out[n] = rune(0x10000 + (u32(u - 0xD800) << 10 | u32(v - 0xDC00))) + n += 1 + continue + } + } + out[n] = utf8.RUNE_ERROR + n += 1 + case u >= 0xDC00 && u <= 0xDFFF: + out[n] = utf8.RUNE_ERROR + n += 1 + case: + out[n] = rune(u) + n += 1 + } + } + if i < len(src) && n < len(out) { + out[n] = utf8.RUNE_ERROR + n += 1 + } + return n +} + +// --------------------------------------------------------------------------- +// Finishing +// --------------------------------------------------------------------------- + +@(private = "file") +cmap_finish :: proc(b: ^Cmap_Builder, cm: ^Cmap) { + cm.name = b.name + cm.wmode = b.wmode + if !b.has_wmode && cm.parent != nil { + cm.wmode = cm.parent.wmode + } + + if len(b.codespace) > 0 { + cs := make([]Cmap_Codespace, len(b.codespace), b.allocator) + if cs != nil { + copy(cs, b.codespace[:]) + cm.codespace = cs + } + } else if cm.parent != nil { + // ISO 32000-2, 9.7.5.3: a CMap that declares no codespace ranges takes + // the ones of the CMap it uses. Sharing the parent's slice is safe -- + // nothing here ever mutates a finished Cmap. + cm.codespace = cm.parent.codespace + } + + slice.sort_by(b.singles[:], proc(x, y: Cmap_Build_Single) -> bool { + return x.code < y.code || (x.code == y.code && x.seq < y.seq) + }) + slice.sort_by(b.texts[:], proc(x, y: Cmap_Build_Text) -> bool { + return x.code < y.code || (x.code == y.code && x.seq < y.seq) + }) + slice.sort_by(b.ranges[:], proc(x, y: Cmap_Range) -> bool { + return x.lo < y.lo + }) + + if n := len(b.singles); n > 0 { + out := make([]Cmap_Single, n, b.allocator) + if out != nil { + k := 0 + for i in 0 ..< n { + // Sorted by (code, seq), so the last of a run is the latest + // definition and the one that wins. + if i + 1 < n && b.singles[i + 1].code == b.singles[i].code { continue } + out[k] = Cmap_Single{code = b.singles[i].code, dst = b.singles[i].dst} + k += 1 + } + cm.singles = out[:k] + } + } + + if n := len(b.texts); n > 0 { + out := make([]Cmap_Text, n, b.allocator) + if out != nil { + k := 0 + for i in 0 ..< n { + if i + 1 < n && b.texts[i + 1].code == b.texts[i].code { continue } + out[k] = Cmap_Text{code = b.texts[i].code, off = b.texts[i].off, len = b.texts[i].len} + k += 1 + } + cm.texts = out[:k] + } + } + + if n := len(b.ranges); n > 0 { + out := make([]Cmap_Range, n, b.allocator) + if out != nil { + copy(out, b.ranges[:]) + cm.ranges = out + } + } + + if n := len(b.pool); n > 0 && len(cm.texts) > 0 { + pool := make([]u8, n, b.allocator) + if pool != nil { + copy(pool, b.pool[:]) + cm.text_pool = pool + } else { + cm.texts = nil + } + } +} diff --git a/cmap_test.odin b/cmap_test.odin new file mode 100644 index 0000000..5fc6fb7 --- /dev/null +++ b/cmap_test.odin @@ -0,0 +1,996 @@ +package pdf + +import "core:fmt" +import "core:mem" +import "core:mem/virtual" +import "core:testing" +import "core:time" + +// --------------------------------------------------------------------------- +// Fixture +// +// Every CMap under test is built in a local arena, so a failing assertion +// cannot leak: the arena is destroyed on the way out whatever happened. The +// context allocator is pointed at it too, because `cmap_load` with a nil +// Document has no arena of its own to use. +// --------------------------------------------------------------------------- + +@(private = "file") +Cmap_Fixture :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private = "file") +cmf_init :: proc(f: ^Cmap_Fixture) { + _ = virtual.arena_init_growing(&f.arena) + f.alloc = virtual.arena_allocator(&f.arena) +} + +@(private = "file") +cmf_destroy :: proc(f: ^Cmap_Fixture) { + virtual.arena_destroy(&f.arena) +} + +@(private = "file") +cmf_parse :: proc(f: ^Cmap_Fixture, src: string) -> ^Cmap { + cm, err := cmap_parse(nil, transmute([]byte)src, f.alloc) + if err != .None || cm == nil { + return nil + } + return cm +} + +// cmf_stream makes a filterless stream, whose payload `stream_data` hands back +// as a sub-slice of the input: no allocation, nothing to own. +@(private = "file") +cmf_stream :: proc(f: ^Cmap_Fixture, src: string) -> ^Stream { + d := new_dict(f.alloc) + return new_stream(d, transmute([]byte)src, f.alloc) +} + +// cmf_codes decodes a whole string the way a text operator would, returning +// (code, width) pairs. It also asserts the loop always advances. +@(private = "file") +cmf_codes :: proc(t: ^testing.T, cm: ^Cmap, s: []byte) -> (codes: [dynamic]u32, widths: [dynamic]int) { + for i := 0; i < len(s); { + code, n := cmap_next_code(cm, s[i:]) + if !testing.expectf(t, n > 0, "cmap_next_code stalled at offset %d", i) { + break + } + append(&codes, code) + append(&widths, n) + i += n + } + return +} + +@(private = "file") +expect_text :: proc(t: ^testing.T, cm: ^Cmap, code: u32, want: string, loc := #caller_location) { + got, ok := cmap_lookup_text(cm, code) + testing.expectf(t, ok, "no text for code 0x%X, wanted %q", code, want, loc = loc) + testing.expectf(t, got == want, "code 0x%X: got %q, wanted %q", code, got, want, loc = loc) +} + +// --------------------------------------------------------------------------- +// Variable-width codes +// --------------------------------------------------------------------------- + +@(test) +test_cmap_mixed_width_codespace :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // A Shift-JIS-shaped encoding: single bytes below 0x81, and a two-byte + // lead/trail pair above it. Both widths in one CMap is the whole reason + // this file exists. + cm := cmf_parse(&f, ` +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CMapName /Test-H def +/CMapType 1 def +2 begincodespacerange +<00> <80> +<8140> <9ffc> +endcodespacerange +endcmap +CMapName currentdict /CMap defineresource pop +end +end +`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + src := []byte{0x41, 0x81, 0x40, 0x7F, 0x9F, 0xFC, 0x00} + codes, widths := cmf_codes(t, cm, src) + defer delete(codes) + defer delete(widths) + + want_codes := []u32{0x41, 0x8140, 0x7F, 0x9FFC, 0x00} + want_widths := []int{1, 2, 1, 2, 1} + testing.expectf(t, len(codes) == len(want_codes), "segmented into %d codes: %v", len(codes), codes) + for w, i in want_codes { + if i >= len(codes) { break } + testing.expectf(t, codes[i] == w, "code %d: got 0x%X, want 0x%X", i, codes[i], w) + testing.expectf(t, widths[i] == want_widths[i], + "code %d: got %d bytes, want %d", i, widths[i], want_widths[i]) + } +} + +@(test) +test_cmap_codespace_is_bytewise :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +2 begincodespacerange +<00> <80> +<8140> <9ffc> +endcodespacerange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + // 0x8200 is inside the numeric interval [0x8140, 0x9FFC] but its trailing + // byte is outside [0x40, 0xFC], so it is NOT a code in this range + // (ISO 32000-2, 9.7.6.2) -- it is an invalid code. Its lead byte is still + // inside [0x81, 0x9F], which is a partial match, so 9.7.6.3 b) makes the + // invalid code two bytes wide all the same. + code, n := cmap_next_code(cm, []byte{0x82, 0x00}) + testing.expectf(t, n == 2, "0x8200 consumed %d bytes; a partial match consumes the range's width", n) + testing.expectf(t, code == 0x8200, "got code 0x%X, want 0x8200", code) + + // 0x8240 is inside both byte ranges, so it is a two-byte code. + code2, n2 := cmap_next_code(cm, []byte{0x82, 0x40}) + testing.expectf(t, n2 == 2 && code2 == 0x8240, "0x8240: got 0x%X in %d bytes", code2, n2) + + // Where the two readings really part company is the width. Give the same + // two-byte range a one-byte range that covers every lead byte: bytewise, + // 0x82 0x00 is a complete one-byte code and nothing longer matches, so it + // is one byte. Read as a numeric interval, 0x8200 would fall inside + // [0x8140, 0x9FFC] and swallow the byte after it. + cm2 := cmf_parse(&f, `begincmap +2 begincodespacerange +<00> +<8140> <9ffc> +endcodespacerange +endcmap`) + if !testing.expect(t, cm2 != nil, "parse failed") { return } + code3, n3 := cmap_next_code(cm2, []byte{0x82, 0x00}) + testing.expectf(t, n3 == 1 && code3 == 0x82, + "0x82 0x00 under a one-byte catch-all: got 0x%X in %d bytes, want 0x82 in 1", code3, n3) + code4, n4 := cmap_next_code(cm2, []byte{0x82, 0x40}) + testing.expectf(t, n4 == 2 && code4 == 0x8240, + "0x8240 is still the longer complete match: got 0x%X in %d bytes", code4, n4) +} + +// ISO 32000-2, 9.7.6.3 splits an unmatched code into two cases. a) A first +// byte that begins no range at all takes the width of the shortest range. +// b) A first byte that DOES begin a longer range takes that range's width, +// even though the bytes after it do not complete the code. Only b) keeps the +// rest of the string in step: one byte too few and every code after the bad +// one is read at the wrong offset, so a single stray byte in a CJK string +// turns the whole of the rest of it into wrong glyphs. +@(test) +test_cmap_next_code_partial_match :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // The codespace of the standard's own example. + cm := cmf_parse(&f, `begincmap +4 begincodespacerange +<00> <80> +<8140> <9ffc> + + +endcodespacerange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + Case :: struct { src: []byte, want_code: u32, want_n: int } + for c in ([]Case{ + // 0x81 opens the two-byte range but 0x20 does not complete it. + {[]byte{0x81, 0x20, 0x41}, 0x8120, 2}, + {[]byte{0x82, 0x39}, 0x8239, 2}, + {[]byte{0x90, 0x30, 0x41}, 0x9030, 2}, + // The same for the second two-byte range. + {[]byte{0xE0, 0x00}, 0xE000, 2}, + // 0xFC is outside [0xE0, 0xFB] and every other lead range, so this is + // rule a) and the shortest range decides: one byte. + {[]byte{0xFC, 0x41}, 0xFC, 1}, + // Valid codes are untouched. + {[]byte{0x41}, 0x41, 1}, + {[]byte{0x81, 0x40}, 0x8140, 2}, + {[]byte{0xA0, 0x41}, 0xA0, 1}, + // A partial match with the string running out still consumes what is + // left rather than stalling. + {[]byte{0x81}, 0x81, 1}, + }) { + code, n := cmap_next_code(cm, c.src) + testing.expectf(t, n == c.want_n && code == c.want_code, + "%X: got 0x%X in %d bytes, want 0x%X in %d", c.src, code, n, c.want_code, c.want_n) + } + + // The whole point: the code after a bad one lands where it should. + codes, widths := cmf_codes(t, cm, []byte{0x81, 0x20, 0x41}) + defer delete(codes) + defer delete(widths) + testing.expectf(t, len(codes) == 2, "segmented into %d codes: %v", len(codes), codes) + if len(codes) == 2 { + testing.expectf(t, codes[1] == 0x41, "the code after the invalid one is 0x%X, want 0x41", codes[1]) + } +} + +@(test) +test_cmap_next_code_always_advances :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Two-byte codespace only, driven with an odd number of bytes and with a + // byte outside every range: neither may stall the caller. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <2000> <20ff> endcodespacerange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + _, n := cmap_next_code(cm, []byte{0x99, 0x99}) + testing.expectf(t, n == 2, "unmatched bytes consumed %d, want the shortest range's 2", n) + + // A trailing half code unit still has to be consumed. + code, n1 := cmap_next_code(cm, []byte{0x20}) + testing.expectf(t, n1 == 1 && code == 0x20, "trailing byte: got 0x%X in %d bytes", code, n1) + + // No CMap and no bytes are both defined. + c0, n0 := cmap_next_code(cm, nil) + testing.expectf(t, n0 == 0 && c0 == 0, "empty string: got 0x%X in %d bytes", c0, n0) + cn, nn := cmap_next_code(nil, []byte{0xAB, 0xCD}) + testing.expectf(t, nn == 1 && cn == 0xAB, "nil CMap: got 0x%X in %d bytes", cn, nn) +} + +@(test) +test_cmap_identity :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmap_identity(false, f.alloc) + if !testing.expect(t, cm != nil, "cmap_identity returned nil") { return } + testing.expect(t, cm.name == "Identity-H", "wrong name") + testing.expect(t, cm.wmode == 0, "Identity-H is horizontal") + testing.expect(t, !cm.predefined_fallback, "Identity-H is not a fallback") + + src := []byte{0x00, 0x41, 0xFF, 0xFE} + codes, widths := cmf_codes(t, cm, src) + defer delete(codes) + defer delete(widths) + testing.expectf(t, len(codes) == 2, "identity segmented into %d codes: %v", len(codes), codes) + if len(codes) == 2 { + testing.expectf(t, codes[0] == 0x0041 && widths[0] == 2, "first code 0x%X", codes[0]) + testing.expectf(t, codes[1] == 0xFFFE && widths[1] == 2, "second code 0x%X", codes[1]) + } + + for code in ([]u32{0, 1, 0x41, 0x1234, 0xFFFF}) { + cid, ok := cmap_lookup(cm, code) + testing.expectf(t, ok && cid == code, "identity lookup 0x%X gave 0x%X, %v", code, cid, ok) + } + + v := cmap_identity(true, f.alloc) + if !testing.expect(t, v != nil, "cmap_identity(vertical) returned nil") { return } + testing.expect(t, v.name == "Identity-V" && v.wmode == 1, "Identity-V is vertical") +} + +@(test) +test_cmap_predefined_fallback :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // The CJK registries are not shipped. A name from one must still yield a + // usable two-byte CMap, and must say that it is not the real thing. + cm, err := cmap_load(nil, Name("UniJIS-UCS2-H")) + if !testing.expectf(t, err == .None && cm != nil, "load: %s", error_string(err)) { return } + testing.expect(t, cm.predefined_fallback, "a substituted CMap must be flagged") + testing.expect(t, cm.name == "UniJIS-UCS2-H", "the requested name is kept") + testing.expect(t, cm.wmode == 0, "-H is horizontal") + _, n := cmap_next_code(cm, []byte{0x30, 0x42}) + testing.expectf(t, n == 2, "the fallback is two-byte, got %d", n) + + vert, verr := cmap_load(nil, Name("UniJIS-UCS2-V")) + if !testing.expectf(t, verr == .None && vert != nil, "load: %s", error_string(verr)) { return } + testing.expect(t, vert.wmode == 1, "-V must be recognised as vertical") + + // Identity-H is real, not a fallback. + id, ierr := cmap_load(nil, Name("Identity-H")) + if !testing.expectf(t, ierr == .None && id != nil, "load: %s", error_string(ierr)) { return } + testing.expect(t, !id.predefined_fallback, "Identity-H must not be flagged as substituted") + + // Neither a name nor a stream is the one case with nothing to hand back. + bad, berr := cmap_load(nil, i64(7)) + testing.expect(t, bad == nil && berr == .Bad_Cmap, "a non-CMap object is an error") +} + +// --------------------------------------------------------------------------- +// CID mappings +// --------------------------------------------------------------------------- + +@(test) +test_cmap_cid_ranges_and_chars :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +2 begincidrange +<0000> <00ff> 100 +<0300> <03ff> 900 +endcidrange +1 begincidchar +<1000> 4242 +endcidchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + Case :: struct { code: u32, cid: u32, ok: bool } + for c in ([]Case{ + {0x0000, 100, true}, + {0x0010, 116, true}, + {0x00FF, 355, true}, + {0x0300, 900, true}, + {0x03FF, 1155, true}, + {0x1000, 4242, true}, + {0x0100, 0, false}, + {0x2000, 0, false}, + }) { + cid, ok := cmap_lookup(cm, c.code) + testing.expectf(t, ok == c.ok, "code 0x%X: found=%v, want %v", c.code, ok, c.ok) + if c.ok { + testing.expectf(t, cid == c.cid, "code 0x%X: cid %d, want %d", c.code, cid, c.cid) + } + } +} + +@(test) +test_cmap_bad_ranges_dropped :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +2 begincodespacerange +<0030> <0020> +<0000> +endcodespacerange +2 begincidrange +<0030> <0020> 7 +<0040> <0041> 9 +endcidrange +1 beginbfrange +<0060> <0050> <0041> +endbfrange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + // A range that runs backwards describes nothing and must not be stored, + // let alone answered from. + _, ok30 := cmap_lookup(cm, 0x30) + testing.expect(t, !ok30, "a lo>hi cidrange must be dropped") + _, ok20 := cmap_lookup(cm, 0x20) + testing.expect(t, !ok20, "a lo>hi cidrange must be dropped") + _, okbf := cmap_lookup_text(cm, 0x60) + testing.expect(t, !okbf, "a lo>hi bfrange must be dropped") + + cid, ok := cmap_lookup(cm, 0x41) + testing.expectf(t, ok && cid == 10, "the good range still works: %d, %v", cid, ok) + + // Dropped, not merely never matched: a backwards range that is kept would + // still be searched on every lookup, and `dst + (code - lo)` under it + // underflows. + testing.expectf(t, len(cm.ranges) == 1, "kept %d cid ranges, want 1", len(cm.ranges)) + testing.expectf(t, len(cm.codespace) == 1, "kept %d codespace ranges, want 1", len(cm.codespace)) + testing.expectf(t, len(cm.texts) == 0, "kept %d bf entries, want 0", len(cm.texts)) + + // The backwards codespace range is gone, so only the two-byte one is left. + _, n := cmap_next_code(cm, []byte{0x00, 0x41}) + testing.expectf(t, n == 2, "a lo>hi codespace range must be dropped, got width %d", n) +} + +// --------------------------------------------------------------------------- +// /ToUnicode +// --------------------------------------------------------------------------- + +@(test) +test_cmap_bfrange_numeric_destination :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +1 beginbfrange +<0020> <0022> <0041> +endbfrange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x20, "A") + expect_text(t, cm, 0x21, "B") + expect_text(t, cm, 0x22, "C") + _, ok := cmap_lookup_text(cm, 0x23) + testing.expect(t, !ok, "one past the end of a bfrange is unmapped") +} + +@(test) +test_cmap_bfrange_array_destination :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // The second bfrange form: one destination string per source code, which + // is how a subset font spells out an irregular run. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +2 beginbfrange +<0005> <0007> [<0041> <00420043> ] +<0010> <0013> [<0058> <0059>] +endbfrange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x05, "A") + expect_text(t, cm, 0x06, "BC") + expect_text(t, cm, 0x07, "\U0002003E") + + // An array shorter than the range maps only as far as it reaches. + expect_text(t, cm, 0x10, "X") + expect_text(t, cm, 0x11, "Y") + _, ok := cmap_lookup_text(cm, 0x12) + testing.expect(t, !ok, "a short destination array must not invent entries") +} + +@(test) +test_cmap_bfchar_ligature_and_surrogates :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +5 beginbfchar +<0001> <00660066006c> +<0002> +<0003> +<0004> +<0005> <004160> +endbfchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + // A single glyph standing for a ligature: three code points, one code. + expect_text(t, cm, 0x0001, "ffl") + // A surrogate pair is one character, U+2003E, not two. + expect_text(t, cm, 0x0002, "\U0002003E") + // An unpaired surrogate, high or low, is U+FFFD -- never invalid UTF-8. + expect_text(t, cm, 0x0003, "�") + expect_text(t, cm, 0x0004, "�") + // A destination that is not a whole number of UTF-16 code units -- three + // bytes -- ends in half a unit, which is U+FFFD like any other truncation. + expect_text(t, cm, 0x0005, "A�") +} + +@(test) +test_cmap_later_definition_wins :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // A broad bfrange followed by a bfchar correcting one code out of it is a + // shape real producers emit; the correction has to be the one that sticks. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +1 beginbfrange +<0041> <005a> <0061> +endbfrange +1 beginbfchar +<0042> <0030> +endbfchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x41, "a") + expect_text(t, cm, 0x42, "0") + expect_text(t, cm, 0x43, "c") +} + +@(test) +test_cmap_absurd_range_is_clamped :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Four billion codes in one line is legal syntax. Expanding it is not an + // option, so the expansion stops -- but what it did produce is correct. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <00000000> endcodespacerange +1 beginbfrange +<00000000> <0041> +endbfrange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0, "A") + testing.expectf(t, len(cm.texts) == MAX_CMAP_RANGE_CODES, + "expanded %d entries, the cap is %d", len(cm.texts), MAX_CMAP_RANGE_CODES) + _, ok := cmap_lookup_text(cm, 0x00FF_FFFF) + testing.expect(t, !ok, "the clamped tail of the range is simply unmapped") +} + +// A bfrange whose expansion is thrown away must not be expanded. One +// `<0000> ` line is half the entry cap and two fill it, so every line +// after that produces nothing at all -- while still costing 65536 iterations +// each if the loop only learns about the cap from the entries it stores being +// dropped. Nothing bounds the number of lines but the token fuel, which allows +// about 1.4 million of them, and a body that repetitive compresses to a few +// tens of KB: a 30 KB file would pin a core for half an hour. +@(test) +test_cmap_repeated_full_bfrange_terminates :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + LINES :: 20_000 + src: [dynamic]u8 + defer delete(src) + append(&src, ..transmute([]byte)string( + "begincmap\n1 begincodespacerange <0000> endcodespacerange\nbeginbfrange\n")) + for _ in 0 ..< LINES { + append(&src, ..transmute([]byte)string("<0000><0020>\n")) + } + append(&src, ..transmute([]byte)string("endbfrange\nendcmap\n")) + + started := time.now() + cm := cmf_parse(&f, string(src[:])) + ms := time.duration_milliseconds(time.since(started)) + if !testing.expect(t, cm != nil, "parse failed") { return } + + // What it does produce is still right: the destination is incremented once + // per code, and the repeated lines are all the same mapping. + expect_text(t, cm, 0x0000, " ") + expect_text(t, cm, 0x0021, "A") + testing.expectf(t, len(cm.texts) == MAX_CMAP_RANGE_CODES, + "expanded %d distinct codes, want %d", len(cm.texts), MAX_CMAP_RANGE_CODES) + + // Not a performance bound: without the early exit this same body takes + // about 25 seconds, and the limit is loose enough that a loaded machine + // cannot trip it. + testing.expectf(t, ms < 5000, "%d bfrange lines took %.0f ms", LINES, ms) +} + +// --------------------------------------------------------------------------- +// Malformed input +// --------------------------------------------------------------------------- + +@(test) +test_cmap_count_disagrees_with_contents :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // The integer before `beginbfchar` is a hint, not a contract: files get it + // wrong in both directions and the contents are what count. + cm := cmf_parse(&f, `begincmap +9 begincodespacerange <0000> endcodespacerange +99 beginbfchar +<0041> <0061> +<0042> <0062> +endbfchar +0 begincidchar +<0043> 7 +endcidchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x41, "a") + expect_text(t, cm, 0x42, "b") + testing.expectf(t, len(cm.texts) == 2, "got %d text entries, want 2", len(cm.texts)) + cid, ok := cmap_lookup(cm, 0x43) + testing.expectf(t, ok && cid == 7, "a section declared empty still maps its contents: %d %v", cid, ok) +} + +@(test) +test_cmap_truncated :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Stops in the middle of a bfchar section, with a half-written pair. What + // was collected before the cut has to survive. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +3 beginbfchar +<0041> <0061> +<0042> `) + if !testing.expect(t, cm != nil, "a truncated CMap still parses") { return } + + expect_text(t, cm, 0x41, "a") + _, ok := cmap_lookup_text(cm, 0x42) + testing.expect(t, !ok, "the half-written pair maps nothing") + _, n := cmap_next_code(cm, []byte{0x00, 0x41}) + testing.expectf(t, n == 2, "the codespace before the cut survives, got width %d", n) +} + +@(test) +test_cmap_unterminated_section :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // No `endbfchar`; the next `begin...` is where the section really ended. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> +1 beginbfchar +<0041> <0061> +1 begincidchar +<0043> 7 +endcidchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x41, "a") + cid, ok := cmap_lookup(cm, 0x43) + testing.expectf(t, ok && cid == 7, "the section after the unterminated one still parses: %d %v", cid, ok) + _, n := cmap_next_code(cm, []byte{0x00, 0x41}) + testing.expectf(t, n == 2, "the unterminated codespace range still counts, got width %d", n) +} + +@(test) +test_cmap_unknown_section_is_skipped :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // `notdefrange` and `usematrix` are sections this reader does not + // interpret. Stepping over their contents is not the same as ignoring the + // section header: what is inside must not reach the enclosing CMap, or a + // stray `/WMode 1 def` in a section that has nothing to do with the + // writing mode would turn the whole font vertical. + cm := cmf_parse(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +1 beginbfchar +<0041> <0061> +endbfchar +1 beginnotdefrange +<0041> <0062> +endnotdefrange +1 beginusematrix +/WMode 1 def +/CMapName /Wrong def +<0041> <0063> +endusematrix +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + expect_text(t, cm, 0x41, "a") + testing.expectf(t, len(cm.texts) == 1, "got %d text entries, want 1", len(cm.texts)) + testing.expectf(t, cm.wmode == 0, "a /WMode inside a skipped section must not apply, got %d", cm.wmode) + testing.expectf(t, cm.name == "", "a /CMapName inside a skipped section must not apply, got %q", cm.name) +} + +@(test) +test_cmap_longest_codespace_wins :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Overlapping widths: 0x41 is a code in the one-byte range and also the + // lead byte of the two-byte range. The wider code is the intended one, so + // declaration order must not decide it. + cm := cmf_parse(&f, `begincmap +2 begincodespacerange +<00> +<4100> <41ff> +endcodespacerange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + code, n := cmap_next_code(cm, []byte{0x41, 0x00}) + testing.expectf(t, n == 2 && code == 0x4100, "longest match: got 0x%X in %d bytes", code, n) + // Only one byte left: the wide range cannot apply and the narrow one does. + c1, n1 := cmap_next_code(cm, []byte{0x41}) + testing.expectf(t, n1 == 1 && c1 == 0x41, "one byte left: got 0x%X in %d bytes", c1, n1) + // A byte the wide range does not lead with is narrow. + c2, n2 := cmap_next_code(cm, []byte{0x42, 0x00}) + testing.expectf(t, n2 == 1 && c2 == 0x42, "narrow code: got 0x%X in %d bytes", c2, n2) +} + +@(test) +test_cmap_junk_never_hangs :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // A well-formed CMap cut at every offset, plus a few shapes chosen to make + // the tokeniser unhappy. None may crash, hang or fail to return a CMap. + full := `begincmap +/CMapName /X def +/WMode 0 def +2 begincodespacerange <00> <80> <8140> <9ffc> endcodespacerange +1 begincidrange <8140> <9ffc> 1 endcidrange +1 beginbfrange <0041> <005a> [<0061> <0062>] endbfrange +1 beginbfchar <0041> endbfchar +endcmap` + for cut in 0 ..= len(full) { + cm, err := cmap_parse(nil, transmute([]byte)full[:cut], f.alloc) + testing.expectf(t, err == .None && cm != nil, "cut at %d: %s", cut, error_string(err)) + if cm != nil { + _, _ = cmap_next_code(cm, []byte{0x81, 0x40, 0x41}) + _, _ = cmap_lookup(cm, 0x8140) + _, _ = cmap_lookup_text(cm, 0x41) + } + } + + for junk in ([]string{ + "", + "endcmap", + "beginbfchar", + "beginbfrange <0041", + "beginbfrange <0041> <0042> [", + "beginbfrange <0041> <0042> [<0061>", + "begincodespacerange <> <>", + "begincidrange /a /b /c endcidrange", + "begincidchar <41> -5 endcidchar", + "beginbfchar () () endbfchar", + "usecmap def begin end pop", + "/WMode /oops def", + ") ) ) > > >", + "begin begincmap begin", + }) { + cm, err := cmap_parse(nil, transmute([]byte)junk, f.alloc) + testing.expectf(t, err == .None && cm != nil, "%q: %s", junk, error_string(err)) + if cm != nil { + _, n := cmap_next_code(cm, []byte{0x41, 0x42}) + testing.expectf(t, n > 0, "%q: decoding stalled", junk) + } + } +} + +// --------------------------------------------------------------------------- +// /UseCMap +// --------------------------------------------------------------------------- + +@(test) +test_cmap_usecmap_inheritance :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + parent := cmf_stream(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +1 begincidrange <0000> <00ff> 500 endcidrange +/WMode 1 def +endcmap`) + child := cmf_stream(&f, `begincmap +1 begincidchar <0005> 7 endcidchar +endcmap`) + dict_set(child.dict, "UseCMap", parent) + + cm, err := cmap_load(nil, child) + if !testing.expectf(t, err == .None && cm != nil, "load: %s", error_string(err)) { return } + + // The child's own entry answers first. + own, ok_own := cmap_lookup(cm, 0x05) + testing.expectf(t, ok_own && own == 7, "own entry: %d %v", own, ok_own) + // Anything it does not define falls through to the used CMap. + inh, ok_inh := cmap_lookup(cm, 0x06) + testing.expectf(t, ok_inh && inh == 506, "inherited entry: %d %v", inh, ok_inh) + // A CMap with no codespace ranges of its own takes the used CMap's, which + // is what makes the inherited mapping addressable at all: without the + // inheritance there would be no ranges here and codes would be one byte. + code, n := cmap_next_code(cm, []byte{0x00, 0x06, 0x00, 0x07}) + testing.expectf(t, n == 2 && code == 0x0006, "inherited codespace: 0x%X in %d bytes", code, n) + testing.expectf(t, cm.wmode == 1, "writing mode is inherited too, got %d", cm.wmode) + + _, miss := cmap_lookup(cm, 0x1234) + testing.expect(t, !miss, "a code neither CMap defines is unmapped") +} + +@(test) +test_cmap_usecmap_own_codespace_wins :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + parent := cmf_stream(&f, `begincmap +1 begincodespacerange <00> endcodespacerange +endcmap`) + child := cmf_stream(&f, `begincmap +1 begincodespacerange <0000> endcodespacerange +endcmap`) + dict_set(child.dict, "UseCMap", parent) + + cm, err := cmap_load(nil, child) + if !testing.expectf(t, err == .None && cm != nil, "load: %s", error_string(err)) { return } + + _, n := cmap_next_code(cm, []byte{0x00, 0x41}) + testing.expectf(t, n == 2, "the CMap's own codespace must win, got width %d", n) +} + +@(test) +test_cmap_usecmap_cycle :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Two CMaps naming each other, and one naming itself. Both must terminate + // and still produce a usable CMap. + a := cmf_stream(&f, `begincmap +1 begincodespacerange <00> endcodespacerange +1 begincidchar <01> 11 endcidchar +endcmap`) + b := cmf_stream(&f, `begincmap +1 begincidchar <02> 22 endcidchar +endcmap`) + dict_set(a.dict, "UseCMap", b) + dict_set(b.dict, "UseCMap", a) + + cm, err := cmap_load(nil, a) + if !testing.expectf(t, err == .None && cm != nil, "load: %s", error_string(err)) { return } + v1, ok1 := cmap_lookup(cm, 0x01) + testing.expectf(t, ok1 && v1 == 11, "own entry survives the cycle: %d %v", v1, ok1) + v2, ok2 := cmap_lookup(cm, 0x02) + testing.expectf(t, ok2 && v2 == 22, "one hop through the cycle still resolves: %d %v", v2, ok2) + + self := cmf_stream(&f, `begincmap +1 begincidchar <03> 33 endcidchar +endcmap`) + dict_set(self.dict, "UseCMap", self) + scm, serr := cmap_load(nil, self) + if !testing.expectf(t, serr == .None && scm != nil, "self-reference: %s", error_string(serr)) { return } + v3, ok3 := cmap_lookup(scm, 0x03) + testing.expectf(t, ok3 && v3 == 33, "self-referencing CMap: %d %v", v3, ok3) +} + +@(test) +test_cmap_usecmap_operator :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // The `usecmap` operator names a predefined CMap. Identity-H is the only + // one worth naming that this library actually has. + cm := cmf_parse(&f, `begincmap +/Identity-H usecmap +1 begincidchar <0005> 77 endcidchar +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + + own, ok_own := cmap_lookup(cm, 0x0005) + testing.expectf(t, ok_own && own == 77, "own entry: %d %v", own, ok_own) + // Identity answers for everything else, and lends its codespace. + inh, ok_inh := cmap_lookup(cm, 0x1234) + testing.expectf(t, ok_inh && inh == 0x1234, "identity parent: 0x%X %v", inh, ok_inh) + code, n := cmap_next_code(cm, []byte{0x12, 0x34}) + testing.expectf(t, n == 2 && code == 0x1234, "inherited codespace: 0x%X in %d bytes", code, n) +} + +@(test) +test_cmap_wmode_and_name :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + cm := cmf_parse(&f, `begincmap +/CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 6 >> def +/CMapName /Test-V def +/CMapType 1 def +/WMode 1 def +1 begincodespacerange <0000> endcodespacerange +endcmap`) + if !testing.expect(t, cm != nil, "parse failed") { return } + testing.expectf(t, cm.wmode == 1, "/WMode 1 def, got %d", cm.wmode) + testing.expectf(t, cm.name == "Test-V", "/CMapName, got %q", cm.name) + + // The dictionary entries a stream may carry instead. + s := cmf_stream(&f, "begincmap\n1 begincodespacerange <0000> endcodespacerange\nendcmap") + dict_set(s.dict, "WMode", i64(1)) + dict_set(s.dict, "CMapName", Name("Dict-V")) + scm, err := cmap_load(nil, s) + if !testing.expectf(t, err == .None && scm != nil, "load: %s", error_string(err)) { return } + testing.expectf(t, scm.wmode == 1, "/WMode from the stream dictionary, got %d", scm.wmode) + testing.expectf(t, scm.name == "Dict-V", "/CMapName from the stream dictionary, got %q", scm.name) +} + +// --------------------------------------------------------------------------- +// A CMap shaped like the ones real files carry +// --------------------------------------------------------------------------- + +@(test) +test_cmap_realistic_tounicode :: proc(t: ^testing.T) { + f: Cmap_Fixture + cmf_init(&f) + defer cmf_destroy(&f) + context.allocator = f.alloc + + // Byte-for-byte the shape a Word or InDesign export writes for a subset + // TrueType font, which is 63% of the corpus. + src := `/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (Adobe) +/Ordering (UCS) +/Supplement 0 +>> def +/CMapName /Adobe-Identity-UCS def +/CMapType 2 def +1 begincodespacerange +<0000> +endcodespacerange +3 beginbfrange +<0003> <0003> <0020> +<000F> <0018> <002C> +<0024> <003D> <0041> +endbfrange +2 beginbfchar +<00AB> <00660066> +<00B6> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end` + cm, err := cmap_parse(nil, transmute([]byte)src, f.alloc) + if !testing.expectf(t, err == .None && cm != nil, "parse: %s", error_string(err)) { return } + + testing.expectf(t, cm.name == "Adobe-Identity-UCS", "name %q", cm.name) + expect_text(t, cm, 0x0003, " ") + expect_text(t, cm, 0x000F, ",") + expect_text(t, cm, 0x0018, "5") + expect_text(t, cm, 0x0024, "A") + expect_text(t, cm, 0x003D, "Z") + expect_text(t, cm, 0x00AB, "ff") + expect_text(t, cm, 0x00B6, "ffi") + + // Decoding a two-byte string end to end, the way a Tj operand arrives. + s := []byte{0x00, 0x24, 0x00, 0x25, 0x00, 0xAB} + b: [64]byte + k := 0 + for i := 0; i < len(s); { + code, n := cmap_next_code(cm, s[i:]) + if n <= 0 { break } + i += n + if txt, ok := cmap_lookup_text(cm, code); ok { + k += copy(b[k:], txt) + } + } + got := string(b[:k]) + testing.expectf(t, got == "ABff", "decoded %q, want %q", got, fmt.tprint("ABff")) +} diff --git a/colorspace.odin b/colorspace.odin new file mode 100644 index 0000000..0a651f3 --- /dev/null +++ b/colorspace.odin @@ -0,0 +1,866 @@ +package pdf + +import "core:math" + +// Colour spaces (ISO 32000-2, 8.6). +// +// Every space is reduced to RGB for output. That is not colour management and +// does not pretend to be: an ICCBased space renders through its /Alternate (or +// through the device space its /N implies), which is what every +// non-colour-managed viewer does and what the format explicitly permits. + +Colorspace_Kind :: enum u8 { + Device_Gray, + Device_RGB, + Device_CMYK, + Cal_Gray, + Cal_RGB, + Lab, + ICC_Based, + Indexed, + Separation, + Device_N, + Pattern, +} + +Colorspace :: struct { + kind: Colorspace_Kind, + // Components a colour value in this space carries. 1 for Indexed (the + // index itself), not the base space's count. + n: int, + name: string, + + // Indexed: `base` is the space the lookup table's entries live in, and + // `lookup` holds (hival+1) * base.n bytes. + base: ^Colorspace, + lookup: []byte, + hival: int, + + // Separation, DeviceN and ICCBased all render through `alt`. For the first + // two, `tint` maps the colorant values into it. + alt: ^Colorspace, + tint: ^Function, + colorant_names: []string, + // The /None colorant marks nothing at all on the page (8.6.6.4). + is_none: bool, + // The /All colorant marks every separation. + is_all: bool, + + // CalGray, CalRGB and Lab. + white_point: [3]f64, + black_point: [3]f64, + gamma: [3]f64, + // CalRGB's /Matrix. For a Lab space this holds something else the caller + // never sees: the 3x3 that takes XYZ under this space's own white point to + // linear sRGB, chromatic adaptation folded in, computed once at load time + // so that converting a colour is nine multiplies and no matrix building. + mtx: [9]f64, + // Lab's a and b component ranges, defaulting to [-100 100 -100 100]. + range: [4]f64, + + // Pattern's underlying space, present only for an uncoloured tiling + // pattern, whose colour is supplied by the content that paints it. + under: ^Colorspace, +} + +// Nesting of colour spaces: an Indexed whose base is a Separation whose +// alternate is an ICCBased, and so on. Four is the deepest anything +// meaningful goes; the limit exists because /Resources can be made to name a +// space that names itself. +@(private) +MAX_COLORSPACE_DEPTH :: 8 + +// The most entries an Indexed lookup table can address. /HiVal is capped at +// 255 by the format (8.6.6.3), which is what makes an index a single byte. +@(private) +MAX_INDEXED_HIVAL :: 255 + +// --------------------------------------------------------------------------- +// The device spaces +// +// These are shared, immutable and outlive every Document: they hold no +// document memory and nothing ever writes to them, so handing the same pointer +// to two Documents (or to two threads rendering different files) is safe. It +// also means the overwhelmingly common `/DeviceRGB cs` costs nothing at all. +// --------------------------------------------------------------------------- + +@(private) +cs_gray := Colorspace{kind = .Device_Gray, n = 1, name = "DeviceGray"} +@(private) +cs_rgb := Colorspace{kind = .Device_RGB, n = 3, name = "DeviceRGB"} +@(private) +cs_cmyk := Colorspace{kind = .Device_CMYK, n = 4, name = "DeviceCMYK"} +// A /Pattern space with no underlying space: the pattern carries its own +// colour, so a colour value in it has no components at all. `n` is therefore +// 0, not 1 -- `n` is what tells the content interpreter how many numeric +// operands `scn` takes, and a coloured pattern is set with `/P1 scn`, a name +// and nothing else. An uncoloured pattern is the case with components, and it +// gets them from its underlying space in cs_load_pattern. +@(private) +cs_pattern := Colorspace{kind = .Pattern, n = 0, name = "Pattern"} + +colorspace_device_gray :: proc(doc: ^Document) -> ^Colorspace { + return &cs_gray +} + +colorspace_device_rgb :: proc(doc: ^Document) -> ^Colorspace { + return &cs_rgb +} + +colorspace_device_cmyk :: proc(doc: ^Document) -> ^Colorspace { + return &cs_cmyk +} + +// colorspace_device_n returns the device space with `n` components, which is +// how an ICCBased space with no /Alternate and an inline image with no space +// at all are resolved (8.6.5.5). +@(private) +colorspace_device_n :: proc(n: int) -> ^Colorspace { + switch n { + case 1: return &cs_gray + case 4: return &cs_cmyk + } + return &cs_rgb +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// colorspace_load builds the colour space `obj` names or describes. +// +// `res` is the /Resources dictionary of the page or form whose content stream +// named it, and is where a bare name that is not a device space is looked up +// (through /Resources /ColorSpace); it may be nil, and a caller that has only +// the /ColorSpace sub-dictionary may pass that instead. +// +// The result is owned by the Document and must not be freed. Spaces are cached +// by the object they were built from, because a page that names the same space +// in four hundred XObjects must build it once. +colorspace_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (^Colorspace, Error) { + return cs_load(doc, obj, res, 0) +} + +@(private) +cs_load :: proc(doc: ^Document, obj: Object, res: ^Dict, depth: int) -> (^Colorspace, Error) { + if doc == nil { + return nil, .Invalid_Argument + } + if depth > MAX_COLORSPACE_DEPTH { + add_repair(doc, .Recursion_Limit_Hit, 0) + return nil, .Recursion_Limit + } + + o := resolve(doc, obj) + key := object_identity(o) + if key != 0 { + if cached, ok := doc.cs_cache[key]; ok { + return cached, .None + } + } + + cs: ^Colorspace + err := Error.Bad_Colorspace + #partial switch v in o { + case Name: + cs, err = cs_load_name(doc, string(v), res, depth) + case ^Array: + cs, err = cs_load_array(doc, v, res, depth) + } + if err != .None || cs == nil { + return nil, err == .None ? .Bad_Colorspace : err + } + + if key != 0 { + if doc.cs_cache == nil { + doc.cs_cache = make(map[uintptr]^Colorspace, 16, doc.allocator) + } + doc.cs_cache[key] = cs + } + return cs, .None +} + +@(private) +cs_load_name :: proc(doc: ^Document, name: string, res: ^Dict, depth: int) -> (^Colorspace, Error) { + // The device spaces cannot be redefined by a resource dictionary + // (8.6.3), so they are answered before it is consulted. + switch name { + case "DeviceGray": return &cs_gray, .None + case "DeviceRGB": return &cs_rgb, .None + case "DeviceCMYK": return &cs_cmyk, .None + case "Pattern": return &cs_pattern, .None + } + + if res != nil { + // `res` is the /Resources dictionary, so a name is looked up in its + // /ColorSpace sub-dictionary (8.6.3). A caller holding only that + // sub-dictionary may pass it instead: it is tried second, so it cannot + // shadow a genuine /ColorSpace entry. + if csdict := dict_dict(doc, res, "ColorSpace"); csdict != nil { + if entry := dict_get_raw(csdict, name); entry != nil { + return cs_load(doc, entry, res, depth + 1) + } + } + if entry := dict_get_raw(res, name); entry != nil { + return cs_load(doc, entry, res, depth + 1) + } + } + + // The inline-image abbreviations (Table 93). They are only legal inside an + // inline image dictionary, so they are tried after the resource lookup: a + // resource genuinely called /G has to win. + switch name { + case "G": return &cs_gray, .None + case "RGB": return &cs_rgb, .None + case "CMYK": return &cs_cmyk, .None + } + return nil, .Bad_Colorspace +} + +@(private) +cs_load_array :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + if array_len(a) == 0 { + return nil, .Bad_Colorspace + } + family := array_name(doc, a, 0) + // `[/DeviceRGB]` and `[/Pattern]` are both legal spellings. + if array_len(a) == 1 { + return cs_load_name(doc, family, res, depth) + } + + switch family { + case "DeviceGray", "G": return &cs_gray, .None + case "DeviceRGB", "RGB": return &cs_rgb, .None + case "DeviceCMYK", "CMYK": return &cs_cmyk, .None + case "CalGray": return cs_load_calgray(doc, a) + case "CalRGB": return cs_load_calrgb(doc, a) + case "Lab": return cs_load_lab(doc, a) + case "ICCBased": return cs_load_icc(doc, a, res, depth) + case "Indexed", "I": return cs_load_indexed(doc, a, res, depth) + case "Separation": return cs_load_separation(doc, a, res, depth) + case "DeviceN": return cs_load_devicen(doc, a, res, depth) + case "Pattern": return cs_load_pattern(doc, a, res, depth) + } + return nil, .Bad_Colorspace +} + +@(private) +cs_new :: proc(doc: ^Document, kind: Colorspace_Kind, n: int, name: string) -> ^Colorspace { + cs := new(Colorspace, doc.allocator) + cs.kind = kind + cs.n = n + cs.name = name + cs.white_point = {0.9642, 1.0, 0.8249} // D50, the format's own default + cs.gamma = {1, 1, 1} + cs.range = {-100, 100, -100, 100} + return cs +} + +// cs_read_white_point reads /WhitePoint, which must be a positive XYZ triple +// with Y = 1. A file that gives something else gets D50, because a zero or +// negative component divides by zero further down. +@(private) +cs_read_white_point :: proc(doc: ^Document, d: ^Dict, cs: ^Colorspace) { + wp := dict_array(doc, d, "WhitePoint") + if array_len(wp) < 3 { + return + } + x := clamp_finite(array_num(doc, wp, 0, 0), 0) + y := clamp_finite(array_num(doc, wp, 1, 0), 0) + z := clamp_finite(array_num(doc, wp, 2, 0), 0) + if x <= 0 || y <= 0 || z <= 0 { + add_repair(doc, .Bad_Resource, 0) + return + } + cs.white_point = {x, y, z} + + bp := dict_array(doc, d, "BlackPoint") + if array_len(bp) >= 3 { + cs.black_point = { + clamp_finite(array_num(doc, bp, 0, 0), 0), + clamp_finite(array_num(doc, bp, 1, 0), 0), + clamp_finite(array_num(doc, bp, 2, 0), 0), + } + } +} + +@(private) +cs_load_calgray :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { + d := array_dict(doc, a, 1) + cs := cs_new(doc, .Cal_Gray, 1, "CalGray") + cs_read_white_point(doc, d, cs) + g := clamp_finite(dict_num(doc, d, "Gamma", 1), 1) + if g <= 0 { + g = 1 + } + cs.gamma = {g, g, g} + return cs, .None +} + +@(private) +cs_load_calrgb :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { + d := array_dict(doc, a, 1) + cs := cs_new(doc, .Cal_RGB, 3, "CalRGB") + cs_read_white_point(doc, d, cs) + if ga := dict_array(doc, d, "Gamma"); array_len(ga) >= 3 { + for i in 0 ..< 3 { + g := clamp_finite(array_num(doc, ga, i, 1), 1) + cs.gamma[i] = g > 0 ? g : 1 + } + } + if ma := dict_array(doc, d, "Matrix"); array_len(ma) >= 9 { + for i in 0 ..< 9 { + cs.mtx[i] = clamp_finite(array_num(doc, ma, i, 0), 0) + } + } + return cs, .None +} + +@(private) +cs_load_lab :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { + d := array_dict(doc, a, 1) + cs := cs_new(doc, .Lab, 3, "Lab") + cs_read_white_point(doc, d, cs) + if r := dict_array(doc, d, "Range"); array_len(r) >= 4 { + for i in 0 ..< 4 { + cs.range[i] = clamp_finite(array_num(doc, r, i, 0), 0) + } + // An inverted range would clamp every value to nothing. + if cs.range[0] > cs.range[1] { + cs.range[0], cs.range[1] = cs.range[1], cs.range[0] + } + if cs.range[2] > cs.range[3] { + cs.range[2], cs.range[3] = cs.range[3], cs.range[2] + } + } + cs.mtx = lab_to_srgb_matrix(cs.white_point) + return cs, .None +} + +@(private) +cs_load_icc :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + stm := to_stream(array_get(doc, a, 1)) or_else nil + d, _ := to_dict(array_get(doc, a, 1)) + n := int(dict_int(doc, d, "N", 0)) + if n != 1 && n != 3 && n != 4 { + n = 0 + } + + alt: ^Colorspace + if entry := dict_get_raw(d, "Alternate"); entry != nil { + loaded, err := cs_load(doc, entry, res, depth + 1) + if err == .None && loaded != nil && loaded.kind != .Pattern { + // An /Alternate that disagrees with /N is the file contradicting + // itself; /N is what the sample data is laid out by. + if n == 0 || loaded.n == n { + alt = loaded + } + } + } + if alt == nil { + if n == 0 { + // Neither /N nor a usable /Alternate. The profile itself would say, + // but reading ICC profiles is exactly what this library does not + // do, so the commonest case is assumed. + add_repair(doc, .Bad_Resource, 0, stm != nil ? stm.ref : Ref{}) + n = 3 + } + alt = colorspace_device_n(n) + } + + // Rendering happens entirely through the alternate: no ICC profile is ever + // parsed and no colour management is performed. Every non-colour-managed + // viewer does the same, and 8.6.5.5 allows it. + cs := cs_new(doc, .ICC_Based, alt.n, "ICCBased") + cs.alt = alt + return cs, .None +} + +@(private) +cs_load_indexed :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + base, berr := cs_load(doc, array_get_raw(a, 1), res, depth + 1) + if berr != .None || base == nil { + return nil, .Bad_Colorspace + } + // The base of an Indexed space may not itself be Indexed or Pattern + // (8.6.6.3); either would make an index mean nothing. + if base.kind == .Indexed || base.kind == .Pattern { + return nil, .Bad_Colorspace + } + + cs := cs_new(doc, .Indexed, 1, "Indexed") + cs.base = base + cs.hival = int(clamp(array_int(doc, a, 2, 0), 0, MAX_INDEXED_HIVAL)) + + lookup := array_get(doc, a, 3) + #partial switch v in lookup { + case String: + cs.lookup = transmute([]byte)v + case ^Stream: + data, derr := stream_data(doc, v) + if derr == .None { + cs.lookup = data + } + } + if len(cs.lookup) < (cs.hival + 1) * base.n { + // A short table is common in files that were subset or edited. The + // entries past its end read as zero rather than past the buffer. + add_repair(doc, .Bad_Resource, 0) + } + return cs, .None +} + +@(private) +cs_load_separation :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + cs := cs_new(doc, .Separation, 1, "Separation") + colorant := array_name(doc, a, 1) + names := make([]string, 1, doc.allocator) + names[0] = colorant + cs.colorant_names = names + // /None marks nothing on the page at all; /All marks every separation + // (8.6.6.4). + cs.is_none = colorant == "None" + cs.is_all = colorant == "All" + + cs_load_tint(doc, cs, a, res, depth) + return cs, .None +} + +@(private) +cs_load_devicen :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + list := to_array(array_get(doc, a, 1)) or_else nil + n := array_len(list) + if n < 1 { + return nil, .Bad_Colorspace + } + // 8.6.6.5 caps a DeviceN space at 32 colorants, which is also the width of + // a Color. + n = min(n, MAX_COLOR_COMPONENTS) + + cs := cs_new(doc, .Device_N, n, "DeviceN") + names := make([]string, n, doc.allocator) + none_count := 0 + for i in 0 ..< n { + names[i] = array_name(doc, list, i) + if names[i] == "None" { + none_count += 1 + } + } + cs.colorant_names = names + // A DeviceN marks nothing only when every one of its colorants is /None. + cs.is_none = none_count == n + + cs_load_tint(doc, cs, a, res, depth) + return cs, .None +} + +// cs_load_tint fills in the alternate space and tint transform shared by +// Separation and DeviceN. Both may be missing or broken, and neither failure +// is a reason to reject the space: `colorspace_to_rgb` falls back to treating +// the colorant as ink coverage, which keeps a spot colour dark instead of +// dropping the artwork that uses it. +@(private) +cs_load_tint :: proc(doc: ^Document, cs: ^Colorspace, a: ^Array, res: ^Dict, depth: int) { + // 8.6.6.4 and 8.6.6.5: the alternate space "shall be any device or + // CIE-based colour space but shall not be another special colour space + // (Pattern, Indexed, Separation, or DeviceN)". All four are refused, and + // for the same reason -- none of them can be handed the output of a tint + // transform and give a colour back. The components of a tint transform's + // output run 0..1; an Indexed component is a palette index, so following + // one collapses the whole ramp onto two entries, and a Pattern has no + // colour value at all. pdfium refuses the whole space at this point; + // dropping to the ink-coverage fallback below is gentler and keeps the + // artwork on the page. + refused := false + if alt, err := cs_load(doc, array_get_raw(a, 2), res, depth + 1); err == .None && alt != nil { + #partial switch alt.kind { + case .Pattern, .Indexed, .Separation, .Device_N: + refused = true + case: + cs.alt = alt + } + } + if tint, err := function_load(doc, array_get_raw(a, 3)); err == .None && tint != nil && tint.n_out >= 1 { + if cs.alt == nil && !refused { + // A tint transform with no alternate space still says how many + // components it produces, and a device space with that many is a + // far better guess than giving up on the colour entirely. That + // does not extend to an alternate that was named and refused: its + // outputs mean whatever that space's components mean, which is + // exactly what cannot be read as a device colour. + cs.alt = colorspace_device_n(tint.n_out) + } + if cs.alt != nil && tint.n_out == cs.alt.n { + cs.tint = tint + } + } + if cs.tint == nil || cs.alt == nil { + add_repair(doc, .Bad_Resource, 0) + } +} + +@(private) +cs_load_pattern :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { + under, err := cs_load(doc, array_get_raw(a, 1), res, depth + 1) + if err != .None || under == nil || under.kind == .Pattern { + return &cs_pattern, .None + } + cs := cs_new(doc, .Pattern, under.n, "Pattern") + cs.under = under + return cs, .None +} + +// --------------------------------------------------------------------------- +// Conversion to RGB +// --------------------------------------------------------------------------- + +// colorspace_to_rgb converts one colour value to RGB in 0..1. It allocates +// nothing and never fails: an unconvertible colour comes back as black, since +// a page that draws the wrong colour is still a page and a page that stops +// drawing is not. +colorspace_to_rgb :: proc(cs: ^Colorspace, c: Color) -> [3]f64 { + return cs_to_rgb(cs, c, 0) +} + +@(private) +cs_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { + if cs == nil || depth > MAX_COLORSPACE_DEPTH { + return {0, 0, 0} + } + + switch cs.kind { + case .Device_Gray: + g := cs_comp(c, 0) + return {g, g, g} + + case .Device_RGB: + return {cs_comp(c, 0), cs_comp(c, 1), cs_comp(c, 2)} + + case .Device_CMYK: + // The naive conversion, subtracting each ink and the black from full + // intensity. It is not colorimetric and no non-colour-managed viewer + // does anything else; a real CMYK profile is the only way to do + // better and this library reads none. + cy, m, y, k := cs_comp(c, 0), cs_comp(c, 1), cs_comp(c, 2), cs_comp(c, 3) + return { + 1 - min(1, cy + k), + 1 - min(1, m + k), + 1 - min(1, y + k), + } + + case .Cal_Gray: + g := cs_cal_component(cs_comp(c, 0), cs.gamma[0]) + return {g, g, g} + + case .Cal_RGB: + return { + cs_cal_component(cs_comp(c, 0), cs.gamma[0]), + cs_cal_component(cs_comp(c, 1), cs.gamma[1]), + cs_cal_component(cs_comp(c, 2), cs.gamma[2]), + } + + case .Lab: + return cs_lab_to_rgb(cs, c) + + case .ICC_Based: + return cs_to_rgb(cs.alt, c, depth + 1) + + case .Indexed: + return cs_indexed_to_rgb(cs, c, depth) + + case .Separation, .Device_N: + return cs_tint_to_rgb(cs, c, depth) + + case .Pattern: + // An uncoloured pattern carries the colour it is painted in, in the + // underlying space. A coloured one supplies its own, and this value is + // never asked for. + if cs.under != nil { + return cs_to_rgb(cs.under, c, depth + 1) + } + return {0, 0, 0} + } + return {0, 0, 0} +} + +// cs_comp reads component i, clamped to 0..1. Used for the spaces whose +// components are all 0..1, which is every one of them but Lab and Indexed. +@(private) +cs_comp :: proc(c: Color, i: int) -> f64 { + if i < 0 || i >= c.n || i >= MAX_COLOR_COMPONENTS { + return 0 + } + return clamp(clamp_finite(c.v[i], 0), 0, 1) +} + +// cs_cal_component decodes one CalGray or CalRGB component. +// +// The full colorimetry -- white point, /Matrix, adaptation -- is deliberately +// not implemented: the format allows a reader to treat these as DeviceGray and +// DeviceRGB and viewers do. /Gamma is honoured, because it is one pow: the +// component is decoded to linear light and re-encoded for an sRGB output +// device. At the usual /Gamma 2.2 that round trip is within half a level of +// leaving the value alone, so the common case still matches every other +// viewer; a gamma of exactly 1 is the default and is skipped entirely. +@(private) +cs_cal_component :: proc(v: f64, gamma: f64) -> f64 { + x := clamp(clamp_finite(v, 0), 0, 1) + if gamma == 1 { + return x + } + return cs_srgb_encode(math.pow(x, gamma)) +} + +// cs_srgb_encode is the sRGB transfer function (IEC 61966-2-1), taking linear +// light to a display value. +@(private) +cs_srgb_encode :: proc(v: f64) -> f64 { + x := clamp(clamp_finite(v, 0), 0, 1) + if x <= 0.0031308 { + return x * 12.92 + } + return 1.055 * math.pow(x, 1.0 / 2.4) - 0.055 +} + +// --------------------------------------------------------------------------- +// Lab +// --------------------------------------------------------------------------- + +// lab_to_srgb_matrix builds XYZ-under-`wp` to linear sRGB. +// +// sRGB is defined against D65 and PDF's Lab spaces are almost always D50, so +// the two are joined by a Bradford chromatic adaptation. Without it a D50 +// white -- L* 100, the commonest colour in any Lab image -- renders as a +// yellow cast rather than as white. +@(private) +lab_to_srgb_matrix :: proc(wp: [3]f64) -> [9]f64 { + // Bradford cone response and its inverse. + ma := [9]f64{ + 0.8951, 0.2664, -0.1614, + -0.7502, 1.7135, 0.0367, + 0.0389, -0.0685, 1.0296, + } + ma_inv := [9]f64{ + 0.9869929, -0.1470543, 0.1599627, + 0.4323053, 0.5183603, 0.0492912, + -0.0085287, 0.0400428, 0.9684867, + } + // XYZ (D65) to linear sRGB. + to_rgb := [9]f64{ + 3.2404542, -1.5371385, -0.4985314, + -0.9692660, 1.8760108, 0.0415560, + 0.0556434, -0.2040259, 1.0572252, + } + d65 := [3]f64{0.95047, 1.0, 1.08883} + + src := mat3_mul_vec(ma, wp) + dst := mat3_mul_vec(ma, d65) + scale: [9]f64 + for i in 0 ..< 3 { + // A white point component of zero was rejected at load time; this is + // belt and braces, since a division by zero here would poison every + // colour the space produces. + scale[i * 3 + i] = src[i] != 0 ? dst[i] / src[i] : 1 + } + adapt := mat3_mul(ma_inv, mat3_mul(scale, ma)) + return mat3_mul(to_rgb, adapt) +} + +@(private) +mat3_mul :: proc(a, b: [9]f64) -> (m: [9]f64) { + for r in 0 ..< 3 { + for c in 0 ..< 3 { + sum := 0.0 + for k in 0 ..< 3 { + sum += a[r * 3 + k] * b[k * 3 + c] + } + m[r * 3 + c] = sum + } + } + return +} + +@(private) +mat3_mul_vec :: proc(a: [9]f64, v: [3]f64) -> (o: [3]f64) { + for r in 0 ..< 3 { + o[r] = a[r * 3] * v[0] + a[r * 3 + 1] * v[1] + a[r * 3 + 2] * v[2] + } + return +} + +// cs_lab_g inverts the CIE L* nonlinearity. +@(private) +cs_lab_g :: proc(x: f64) -> f64 { + // 6/29, below which the curve is linear so that its slope stays finite. + if x >= 6.0 / 29.0 { + return x * x * x + } + return (108.0 / 841.0) * (x - 4.0 / 29.0) +} + +@(private) +cs_lab_to_rgb :: proc(cs: ^Colorspace, c: Color) -> [3]f64 { + l := clamp(clamp_finite(c.n > 0 ? c.v[0] : 0, 0), 0, 100) + a := clamp(clamp_finite(c.n > 1 ? c.v[1] : 0, 0), cs.range[0], cs.range[1]) + b := clamp(clamp_finite(c.n > 2 ? c.v[2] : 0, 0), cs.range[2], cs.range[3]) + + m := (l + 16) / 116 + xw := cs.white_point[0] * cs_lab_g(m + a / 500) + yw := cs.white_point[1] * cs_lab_g(m) + zw := cs.white_point[2] * cs_lab_g(m - b / 200) + + out: [3]f64 + for i in 0 ..< 3 { + lin := cs.mtx[i * 3] * xw + cs.mtx[i * 3 + 1] * yw + cs.mtx[i * 3 + 2] * zw + // Colours outside the sRGB gamut -- most of the Lab space is -- are + // clipped per channel, which is what every viewer without a gamut + // mapping engine does. + out[i] = cs_srgb_encode(lin) + } + return out +} + +// --------------------------------------------------------------------------- +// Indexed +// --------------------------------------------------------------------------- + +@(private) +cs_indexed_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { + base := cs.base + if base == nil { + return {0, 0, 0} + } + // An index out of range is clamped, never followed: the table is exactly + // (hival+1) entries and a file is free to name entry 300 of a 4-entry one. + idx := int(clamp(clamp_finite(math.round(c.n > 0 ? c.v[0] : 0), 0), 0, f64(cs.hival))) + + comp: Color + comp.n = min(base.n, MAX_COLOR_COMPONENTS) + for j in 0 ..< comp.n { + off := idx * base.n + j + raw := off >= 0 && off < len(cs.lookup) ? f64(cs.lookup[off]) : 0 + lo, hi := cs_component_range(base, j) + comp.v[j] = lo + raw * (hi - lo) / 255 + } + return cs_to_rgb(base, comp, depth + 1) +} + +// cs_component_range is the interval component j of a colour in this space +// runs over. Everything is 0..1 except Lab, whose L is 0..100 and whose a and +// b carry their own /Range. +@(private) +cs_component_range :: proc(cs: ^Colorspace, j: int) -> (lo, hi: f64) { + if cs != nil && cs.kind == .Lab { + switch j { + case 0: return 0, 100 + case 1: return cs.range[0], cs.range[1] + case 2: return cs.range[2], cs.range[3] + } + } + return 0, 1 +} + +// --------------------------------------------------------------------------- +// Separation and DeviceN +// --------------------------------------------------------------------------- + +@(private) +cs_tint_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { + if cs.is_none { + // Nothing is painted in a /None space at all. The interpreter checks + // `is_none` before drawing; this value only decides what a caller that + // does not sees, and white is the least destructive answer. + return {1, 1, 1} + } + + if cs.tint != nil && cs.alt != nil { + in_: [MAX_COLOR_COMPONENTS]f64 + out_: [MAX_COLOR_COMPONENTS]f64 + n := min(cs.n, MAX_COLOR_COMPONENTS) + for i in 0 ..< n { + in_[i] = clamp_finite(i < c.n ? c.v[i] : 0, 0) + } + alt_n := min(cs.alt.n, MAX_COLOR_COMPONENTS) + function_eval(cs.tint, in_[:n], out_[:alt_n]) + + alt_color: Color + alt_color.n = alt_n + for i in 0 ..< alt_n { + alt_color.v[i] = out_[i] + } + return cs_to_rgb(cs.alt, alt_color, depth + 1) + } + + // No usable tint transform. Treating the darkest colorant as ink coverage + // keeps a spot colour dark and a light tint light, which is much closer + // than either black or white and never loses the artwork. + ink := 0.0 + for i in 0 ..< min(cs.n, c.n) { + ink = max(ink, clamp(clamp_finite(c.v[i], 0), 0, 1)) + } + g := 1 - ink + return {g, g, g} +} + +// --------------------------------------------------------------------------- +// Initial values and image decode arrays +// --------------------------------------------------------------------------- + +// colorspace_default_color is the colour a space starts at when it is selected +// with `cs` or `CS` (ISO 32000-2, 8.6.8): black for the device and CIE spaces, +// the first entry for Indexed, full tint for Separation and DeviceN. +colorspace_default_color :: proc(cs: ^Colorspace) -> (c: Color) { + if cs == nil { + return color_make(0) + } + n := min(cs.n, MAX_COLOR_COMPONENTS) + switch cs.kind { + case .Device_CMYK: + // 0 0 0 1: black, not the white that four zero components would be. + c.n = 4 + c.v[3] = 1 + case .Separation, .Device_N: + c.n = n + for i in 0 ..< n { + c.v[i] = 1 + } + case .Pattern: + // Until a pattern is selected there is nothing to paint with. An + // uncoloured pattern's components come from the underlying space. + if cs.under != nil { + return colorspace_default_color(cs.under) + } + c.n = 0 + case .Device_Gray, .Device_RGB, .Cal_Gray, .Cal_RGB, .Lab, .ICC_Based, .Indexed: + c.n = n + } + return +} + +// colorspace_default_decode is the /Decode array an image in this space gets +// when it does not supply one (ISO 32000-2, Table 89). `bpc` matters only for +// an Indexed space, whose default maps the raw sample straight onto the +// lookup table. +// +// The result is allocated with `allocator` and belongs to the caller. +colorspace_default_decode :: proc(cs: ^Colorspace, bpc: int, allocator := context.allocator) -> []f64 { + if cs == nil { + return nil + } + n := min(cs.n, MAX_COLOR_COMPONENTS) + if n < 1 { + return nil + } + out := make([]f64, 2 * n, allocator) + + if cs.kind == .Indexed { + bits := clamp(bpc, 1, 16) + out[0] = 0 + out[1] = f64(int(1) << uint(bits)) - 1 + return out + } + for j in 0 ..< n { + out[2 * j], out[2 * j + 1] = cs_component_range(cs, j) + } + return out +} diff --git a/colorspace_test.odin b/colorspace_test.odin new file mode 100644 index 0000000..85eab8f --- /dev/null +++ b/colorspace_test.odin @@ -0,0 +1,814 @@ +package pdf + +import "core:testing" + +// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_bytes` -- live in +// function_test.odin, because a colour space is not testable without the +// functions its tint transforms are made of. + +@(private = "file") +TOL :: 1e-9 + +@(private = "file") +expect_rgb :: proc(t: ^testing.T, got: [3]f64, r, g, b: f64, tol: f64, what: string, + loc := #caller_location) { + if !(abs(got[0] - r) <= tol && abs(got[1] - g) <= tol && abs(got[2] - b) <= tol) { + testing.expectf(t, false, "%s: got (%f %f %f), want (%f %f %f)", + what, got[0], got[1], got[2], r, g, b, loc = loc) + } +} + +@(private = "file") +load_cs :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil) -> ^Colorspace { + cs, err := colorspace_load(doc, cf_parse(doc, src), res) + testing.expect_value(t, err, Error.None) + return cs +} + +@(private = "file") +cf_array :: proc(doc: ^Document, items: ..Object) -> ^Array { + a := new_array(doc.allocator, max(len(items), 1)) + for it in items { + array_push(a, it) + } + return a +} + +// --------------------------------------------------------------------------- +// The device spaces +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_device_gray :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := colorspace_device_gray(doc) + testing.expect_value(t, cs.kind, Colorspace_Kind.Device_Gray) + testing.expect_value(t, cs.n, 1) + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "black") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.25, 0.25, 0.25, TOL, "quarter") + + // Components outside 0..1 are clamped, not wrapped. + expect_rgb(t, colorspace_to_rgb(cs, color_make(2)), 1, 1, 1, TOL, "too bright") + expect_rgb(t, colorspace_to_rgb(cs, color_make(-1)), 0, 0, 0, TOL, "negative") + + // Naming it produces the very same space, not a copy. + named := load_cs(t, doc, "/DeviceGray") + testing.expect(t, named == cs, "/DeviceGray must be the shared space") + testing.expect(t, load_cs(t, doc, "[/DeviceGray]") == cs, "and so must [/DeviceGray]") + testing.expect(t, load_cs(t, doc, "/G") == cs, "and the inline-image abbreviation") +} + +@(test) +test_colorspace_device_rgb :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := colorspace_device_rgb(doc) + testing.expect_value(t, cs.n, 3) + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0)), 0, 0, 0, TOL, "black") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 1, 1)), 1, 1, 1, TOL, "white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0.5, 0)), 1, 0.5, 0, TOL, "orange") + // A colour with too few components reads the missing ones as zero. + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "short colour") + + testing.expect(t, load_cs(t, doc, "/DeviceRGB") == cs, "/DeviceRGB") + testing.expect(t, load_cs(t, doc, "/RGB") == cs, "/RGB abbreviation") +} + +// The naive conversion: subtract each ink and the black from full intensity. +// It is what every non-colour-managed viewer does. +@(test) +test_colorspace_device_cmyk :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := colorspace_device_cmyk(doc) + testing.expect_value(t, cs.n, 4) + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0, 0)), 1, 1, 1, TOL, "no ink is white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0, 1)), 0, 0, 0, TOL, "black ink") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0, 0, 0)), 0, 1, 1, TOL, "cyan") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 1, 0, 0)), 1, 0, 1, TOL, "magenta") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 1, 0)), 1, 1, 0, TOL, "yellow") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 1, 1, 0)), 1, 0, 0, TOL, "red") + // Half a cyan under a quarter black: 1 - (0.5 + 0.25) against 1 - 0.25. + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5, 0, 0, 0.25)), + 0.25, 0.75, 0.75, TOL, "ink plus black") + // Ink and black together past 1 saturate rather than going negative. + expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 1, 1, 1)), 0, 0, 0, TOL, "everything") + + testing.expect(t, load_cs(t, doc, "/DeviceCMYK") == cs, "/DeviceCMYK") + testing.expect(t, load_cs(t, doc, "/CMYK") == cs, "/CMYK abbreviation") +} + +// --------------------------------------------------------------------------- +// CalGray and CalRGB +// --------------------------------------------------------------------------- + +// With the default /Gamma these behave exactly as the device spaces, which is +// what the format permits and what viewers do. +@(test) +test_colorspace_cal_defaults_to_device :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + gray := load_cs(t, doc, "[/CalGray << /WhitePoint [0.9505 1 1.089] >>]") + if gray == nil { return } + testing.expect_value(t, gray.kind, Colorspace_Kind.Cal_Gray) + testing.expect_value(t, gray.n, 1) + expect_rgb(t, colorspace_to_rgb(gray, color_make(0.25)), 0.25, 0.25, 0.25, TOL, "gamma 1 gray") + + rgb := load_cs(t, doc, "[/CalRGB << /WhitePoint [0.9505 1 1.089] >>]") + if rgb == nil { return } + testing.expect_value(t, rgb.kind, Colorspace_Kind.Cal_RGB) + testing.expect_value(t, rgb.n, 3) + expect_rgb(t, colorspace_to_rgb(rgb, color_make(1, 0.5, 0)), 1, 0.5, 0, TOL, "gamma 1 rgb") + + // A white point that would divide by zero is refused; the space still + // loads, with the format's default. + bad := load_cs(t, doc, "[/CalGray << /WhitePoint [0 0 0] /Gamma 1 >>]") + if bad != nil { + testing.expect(t, bad.white_point[0] > 0, "a zero white point is replaced") + } +} + +// /Gamma is honoured by decoding to linear light and re-encoding for sRGB. At +// the usual 2.2 that round trip lands within half a level of 255 of leaving +// the value alone, so the common case still agrees with every other viewer. +@(test) +test_colorspace_calgray_gamma :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := load_cs(t, doc, "[/CalGray << /WhitePoint [0.9505 1 1.089] /Gamma 2.2 >>]") + if cs == nil { return } + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "black stays black") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "white stays white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)), + 0.5038667822745281, 0.5038667822745281, 0.5038667822745281, 1e-9, + "gamma 2.2 round trip at 0.5") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), + 0.2410493652415971, 0.2410493652415971, 0.2410493652415971, 1e-9, + "gamma 2.2 round trip at 0.25") + // Which is to say: within one level of 255 of the DeviceGray answer. + got := colorspace_to_rgb(cs, color_make(0.5)) + testing.expect(t, abs(got[0] - 0.5) < 1.0 / 255, "still what a viewer shows") + + // A per-component /Gamma on CalRGB. + rgb := load_cs(t, doc, + "[/CalRGB << /WhitePoint [0.9505 1 1.089] /Gamma [2.2 1 2.2] >>]") + if rgb == nil { return } + out := colorspace_to_rgb(rgb, color_make(0.5, 0.5, 0.5)) + testing.expect(t, abs(out[0] - 0.5038667822745281) < 1e-9, "red honours its gamma") + testing.expect(t, abs(out[1] - 0.5) < 1e-12, "green is left alone at gamma 1") + testing.expect(t, abs(out[2] - 0.5038667822745281) < 1e-9, "blue honours its gamma") +} + +// --------------------------------------------------------------------------- +// Lab +// +// The reference values are the published sRGB primaries expressed in L*a*b* +// under a D50 white point, which is the white point PDF Lab spaces almost +// always carry. Getting back to within a level of 255 of pure red, green and +// blue is what says the conversion -- including the Bradford adaptation from +// D50 to sRGB's D65 -- is right. +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_lab_known_colours :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := load_cs(t, doc, + "[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-128 127 -128 127] >>]") + if cs == nil { return } + testing.expect_value(t, cs.kind, Colorspace_Kind.Lab) + testing.expect_value(t, cs.n, 3) + + expect_rgb(t, colorspace_to_rgb(cs, color_make(100, 0, 0)), 1, 1, 1, 1e-6, "L* 100 is white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0)), 0, 0, 0, 1e-9, "L* 0 is black") + // L* 53.585 is the mid grey of sRGB: 128/255. + expect_rgb(t, colorspace_to_rgb(cs, color_make(53.5850, 0, 0)), + 128.0 / 255, 128.0 / 255, 128.0 / 255, 1e-5, "L* 53.585 is sRGB 128") + + expect_rgb(t, colorspace_to_rgb(cs, color_make(54.2917, 80.8125, 69.8911)), + 1, 0, 0, 0.002, "sRGB red") + expect_rgb(t, colorspace_to_rgb(cs, color_make(87.8181, -79.2718, 80.9946)), + 0, 1, 0, 0.002, "sRGB green") + expect_rgb(t, colorspace_to_rgb(cs, color_make(29.5676, 68.2986, -112.0294)), + 0, 0, 1, 0.002, "sRGB blue") +} + +@(test) +test_colorspace_lab_d65_and_clamping :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The same conversion under a D65 white point, where sRGB red has + // different coordinates. + d65 := load_cs(t, doc, "[/Lab << /WhitePoint [0.9505 1 1.089] >>]") + if d65 == nil { return } + expect_rgb(t, colorspace_to_rgb(d65, color_make(53.2408, 80.0925, 67.2032)), + 1, 0, 0, 0.002, "sRGB red under D65") + expect_rgb(t, colorspace_to_rgb(d65, color_make(100, 0, 0)), 1, 1, 1, 1e-6, "white under D65") + + // /Range clamps a and b before anything else happens, so a wild value + // cannot walk out of the gamut and take the whole colour with it. + narrow := load_cs(t, doc, + "[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-10 10 -10 10] >>]") + if narrow == nil { return } + testing.expect_value(t, narrow.range[0], -10) + a := colorspace_to_rgb(narrow, color_make(50, 100, 0)) + b := colorspace_to_rgb(narrow, color_make(50, 10, 0)) + expect_rgb(t, a, b[0], b[1], b[2], 1e-12, "a is clamped to /Range") + + // L* outside 0..100 is clamped too. + hi := colorspace_to_rgb(narrow, color_make(500, 0, 0)) + expect_rgb(t, hi, 1, 1, 1, 1e-6, "L* above 100") + lo := colorspace_to_rgb(narrow, color_make(-500, 0, 0)) + expect_rgb(t, lo, 0, 0, 0, 1e-9, "L* below 0") +} + +// --------------------------------------------------------------------------- +// ICCBased +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_iccbased :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A colour space is a name or an array, never a bare stream: ICCBased is + // always written [/ICCBased stream] (ISO 32000-2, 8.6.5.5). Handing the + // stream over on its own must be refused rather than guessed at, or a + // stream that happens to carry an /N would be read as a colour space + // wherever one is expected. + _, bare_err := colorspace_load(doc, cf_stream(doc, "<< /N 3 >>", nil), nil) + testing.expect_value(t, bare_err, Error.Bad_Colorspace) + + // No /Alternate: the device space /N implies. + icc3_arr := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< /N 3 >>", nil)) + icc3, icc3_err := colorspace_load(doc, icc3_arr, nil) + testing.expect_value(t, icc3_err, Error.None) + if icc3 == nil { + testing.expect(t, false, "an ICCBased space with /N 3 must load") + return + } + testing.expect_value(t, icc3.n, 3) + expect_rgb(t, colorspace_to_rgb(icc3, color_make(1, 0, 0)), 1, 0, 0, TOL, + "three components render as RGB") + // The array form is what a file actually writes; the stream alone is what + // cf_stream can build, so both are exercised. + arr := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< /N 4 >>", nil)) + icc4, err4 := colorspace_load(doc, arr, nil) + testing.expect_value(t, err4, Error.None) + if icc4 == nil { return } + testing.expect_value(t, icc4.kind, Colorspace_Kind.ICC_Based) + testing.expect_value(t, icc4.n, 4) + expect_rgb(t, colorspace_to_rgb(icc4, color_make(0, 1, 1, 0)), 1, 0, 0, TOL, + "four components render as CMYK") + + // An /Alternate is used when it agrees with /N. + alt := cf_array(doc, Name("ICCBased"), + cf_stream(doc, "<< /N 3 /Alternate [/Lab << /WhitePoint [0.9642 1 0.8249] >>] >>", nil)) + lab_icc, _ := colorspace_load(doc, alt, nil) + if lab_icc == nil { return } + testing.expect_value(t, lab_icc.n, 3) + expect_rgb(t, colorspace_to_rgb(lab_icc, color_make(100, 0, 0)), 1, 1, 1, 1e-6, + "rendered through the alternate") + + // An /Alternate that contradicts /N is ignored in favour of /N. + bad := cf_array(doc, Name("ICCBased"), + cf_stream(doc, "<< /N 1 /Alternate /DeviceCMYK >>", nil)) + gray_icc, _ := colorspace_load(doc, bad, nil) + if gray_icc == nil { return } + testing.expect_value(t, gray_icc.n, 1) + expect_rgb(t, colorspace_to_rgb(gray_icc, color_make(0.5)), 0.5, 0.5, 0.5, TOL, + "one component is grey whatever the alternate said") + + // Neither /N nor /Alternate: three components assumed, and a repair. + guess := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< >>", nil)) + guessed, _ := colorspace_load(doc, guess, nil) + if guessed != nil { + testing.expect_value(t, guessed.n, 3) + testing.expect(t, doc_has_repair(doc, .Bad_Resource), "guessing /N is a repair") + } +} + +// --------------------------------------------------------------------------- +// Indexed +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_indexed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs, err := colorspace_load(doc, Ref{10, 0}, nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + testing.expect_value(t, cs.kind, Colorspace_Kind.Indexed) + // An Indexed colour has one component -- the index -- whatever the base is. + testing.expect_value(t, cs.n, 1) + testing.expect_value(t, cs.hival, 3) + + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 0, 0, TOL, "entry 0") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 1, 0, TOL, "entry 1") + expect_rgb(t, colorspace_to_rgb(cs, color_make(2)), 0, 0, 1, TOL, "entry 2") + expect_rgb(t, colorspace_to_rgb(cs, color_make(3)), 1, 1, 1, TOL, "entry 3") +} + +// An index outside 0..hival is clamped. It must never read past the table. +@(test) +test_colorspace_indexed_clamps :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs, _ := colorspace_load(doc, Ref{10, 0}, nil) + if cs == nil { return } + expect_rgb(t, colorspace_to_rgb(cs, color_make(4)), 1, 1, 1, TOL, "one past the end") + expect_rgb(t, colorspace_to_rgb(cs, color_make(9999)), 1, 1, 1, TOL, "far past the end") + expect_rgb(t, colorspace_to_rgb(cs, color_make(-1)), 1, 0, 0, TOL, "below the start") + expect_rgb(t, colorspace_to_rgb(cs, color_make(-9999)), 1, 0, 0, TOL, "far below") + + // A table shorter than /HiVal claims: the entries that are not there read + // as zero rather than off the end of the buffer. + short := load_cs(t, doc, "[/Indexed /DeviceRGB 5 ]") + if short == nil { return } + testing.expect_value(t, short.hival, 5) + expect_rgb(t, colorspace_to_rgb(short, color_make(0)), 1, 0, 0, TOL, "the entry that exists") + expect_rgb(t, colorspace_to_rgb(short, color_make(5)), 0, 0, 0, TOL, "one that does not") + + // /HiVal above 255 is not a table this format can index. + big := load_cs(t, doc, "[/Indexed /DeviceRGB 99999 ]") + if big != nil { + testing.expect_value(t, big.hival, 255) + } +} + +@(test) +test_colorspace_indexed_variants :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The lookup table may be a stream instead of a string. + stm := cf_stream(doc, "<< >>", []byte{0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF}) + cs, err := colorspace_load(doc, + cf_array(doc, Name("Indexed"), Name("DeviceRGB"), i64(1), stm), nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "stream entry 0") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "stream entry 1") + + // A base whose components are not 0..1: each table byte spans the base + // component's own range, so 255 in the L* slot is L* 100. + lab := load_cs(t, doc, + "[/Indexed [/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-128 127] >>] " + + "1 <008080 FF8080>]") + if lab == nil { return } + // 0x80 is the middle of [-128 127], which is a* = b* = -0.5: near neutral. + expect_rgb(t, colorspace_to_rgb(lab, color_make(0)), 0, 0, 0, 0.01, "L* 0 entry") + expect_rgb(t, colorspace_to_rgb(lab, color_make(1)), 1, 1, 1, 0.01, "L* 100 entry") + + // An Indexed space over another Indexed space, or over a Pattern, means + // nothing and is refused. + _, e1 := colorspace_load(doc, cf_parse(doc, "[/Indexed [/Indexed /DeviceRGB 1 <0000>] 1 <0001>]"), nil) + testing.expect_value(t, e1, Error.Bad_Colorspace) + _, e2 := colorspace_load(doc, cf_parse(doc, "[/Indexed /Pattern 1 <0001>]"), nil) + testing.expect_value(t, e2, Error.Bad_Colorspace) +} + +// --------------------------------------------------------------------------- +// Separation and DeviceN +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_separation :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A spot colour whose tint transform is a type 2 function into DeviceCMYK: + // full tint is magenta plus yellow, which is red. + cs, err := colorspace_load(doc, Ref{5, 0}, nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + testing.expect_value(t, cs.kind, Colorspace_Kind.Separation) + testing.expect_value(t, cs.n, 1) + testing.expect(t, cs.tint != nil, "the tint transform loaded") + testing.expect(t, cs.alt != nil && cs.alt.kind == .Device_CMYK, "the alternate space") + testing.expect(t, len(cs.colorant_names) == 1 && cs.colorant_names[0] == "Spot", "colorant") + + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no tint is white") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "full tint is red") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)), 1, 0.5, 0.5, TOL, "half tint") + + // The initial colour in a Separation is full tint, not zero (8.6.8). + c := colorspace_default_color(cs) + testing.expect_value(t, c.n, 1) + testing.expect_value(t, c.v[0], 1) +} + +// /None marks nothing at all on the page. +@(test) +test_colorspace_separation_none :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := load_cs(t, doc, + "[/Separation /None /DeviceCMYK << /FunctionType 2 /Domain [0 1] " + + "/C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>]") + if cs == nil { return } + testing.expect(t, cs.is_none, "/None paints nothing") + testing.expect(t, !cs.is_all, "and is not /All") + // Even at full tint, which would otherwise be solid black. + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "nothing is painted") + + all := load_cs(t, doc, + "[/Separation /All /DeviceCMYK << /FunctionType 2 /Domain [0 1] " + + "/C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>]") + if all == nil { return } + testing.expect(t, all.is_all, "/All marks every separation") + testing.expect(t, !all.is_none, "and is not /None") + expect_rgb(t, colorspace_to_rgb(all, color_make(1)), 0, 0, 0, TOL, "full tint is black") +} + +// A tint transform that is missing or unusable must not lose the artwork: the +// colorant is treated as ink coverage instead. +@(test) +test_colorspace_separation_broken_tint :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs := load_cs(t, doc, "[/Separation /Spot /DeviceCMYK << /FunctionType 9 >>]") + if cs == nil { return } + testing.expect(t, cs.tint == nil, "the tint transform did not load") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no ink") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 0, 0, TOL, "full ink") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "a quarter") + testing.expect(t, doc_has_repair(doc, .Bad_Resource), "a broken tint is a repair") + + // The alternate space missing entirely is survivable in the same way. + no_alt := load_cs(t, doc, "[/Separation /Spot]") + if no_alt != nil { + expect_rgb(t, colorspace_to_rgb(no_alt, color_make(1)), 0, 0, 0, TOL, "no alternate") + } +} + +// 8.6.6.4 and 8.6.6.5: the alternate "shall not be another special colour +// space (Pattern, Indexed, Separation, or DeviceN)", and the four are excluded +// for one reason -- a tint transform's output cannot be read as a colour in +// any of them. Indexed is the one that shows: its single component is a +// palette index, so a tint running 0..1 lands on entry 0 or entry 1 and the +// whole ramp collapses onto two colours. Refusing the alternate drops the +// space onto the ink-coverage fallback, which is monotone in the tint and +// keeps the artwork. +@(test) +test_colorspace_separation_special_alternate :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + RAMP :: " << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>" + for src in ([]string{ + "[/Separation /Spot [/Indexed /DeviceRGB 3 ]" + RAMP + "]", + "[/Separation /Spot [/Separation /Other /DeviceGray" + RAMP + "]" + RAMP + "]", + "[/Separation /Spot [/DeviceN [/A] /DeviceGray" + RAMP + "]" + RAMP + "]", + "[/Separation /Spot /Pattern" + RAMP + "]", + "[/DeviceN [/A] [/Indexed /DeviceRGB 3 ]" + RAMP + "]", + }) { + cs := load_cs(t, doc, src) + if cs == nil { continue } + testing.expectf(t, cs.alt == nil, "%s: the alternate must be refused", src) + testing.expectf(t, cs.tint == nil, "%s: and its tint transform with it", src) + // Ink coverage, not two palette entries: the ramp runs the right way + // and takes every value in between. + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no ink") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "a quarter") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)), 0.5, 0.5, 0.5, TOL, "half") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 0, 0, TOL, "full ink") + } + testing.expect(t, doc_has_repair(doc, .Bad_Resource), "an unusable alternate is a repair") + + // The control: a device alternate is what the rule allows, and it still + // goes through the tint transform. + good := load_cs(t, doc, + "[/Separation /Spot /DeviceGray << /FunctionType 2 /Domain [0 1] " + + "/C0 [1] /C1 [0] /N 1 >>]") + if good == nil { return } + testing.expect(t, good.alt != nil && good.alt.kind == .Device_Gray, "a device alternate loads") + testing.expect(t, good.tint != nil, "and keeps its tint transform") + expect_rgb(t, colorspace_to_rgb(good, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "through the tint") +} + +@(test) +test_colorspace_devicen :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Two colorants into DeviceRGB. The program throws the second away and + // copies the first into all three outputs. + tint := cf_stream(doc, + "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] >>", + cf_bytes("{ pop dup dup }")) + names := cf_array(doc, Name("Ink1"), Name("Ink2")) + cs, err := colorspace_load(doc, + cf_array(doc, Name("DeviceN"), names, Name("DeviceRGB"), tint), nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + testing.expect_value(t, cs.kind, Colorspace_Kind.Device_N) + testing.expect_value(t, cs.n, 2) + testing.expect(t, cs.tint != nil, "the tint transform loaded") + testing.expect(t, len(cs.colorant_names) == 2 && cs.colorant_names[1] == "Ink2", "colorants") + + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25, 0.9)), + 0.25, 0.25, 0.25, TOL, "the first colorant drives all three outputs") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0)), 1, 1, 1, TOL, "full first colorant") + + // The initial colour is 1.0 in every component. + c := colorspace_default_color(cs) + testing.expect_value(t, c.n, 2) + testing.expect_value(t, c.v[0], 1) + testing.expect_value(t, c.v[1], 1) +} + +@(test) +test_colorspace_devicen_degrades :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No tint transform at all: the darkest colorant becomes ink coverage. + names := cf_array(doc, Name("Ink1"), Name("Ink2")) + cs, err := colorspace_load(doc, + cf_array(doc, Name("DeviceN"), names, Name("DeviceRGB")), nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + testing.expect_value(t, cs.n, 2) + expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0)), 1, 1, 1, TOL, "no ink") + expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25, 0.75)), + 0.25, 0.25, 0.25, TOL, "the darkest colorant") + + // Every colorant /None: nothing is marked. + nones := cf_array(doc, Name("None"), Name("None")) + all_none, _ := colorspace_load(doc, + cf_array(doc, Name("DeviceN"), nones, Name("DeviceRGB")), nil) + if all_none != nil { + testing.expect(t, all_none.is_none, "a DeviceN of nothing but /None marks nothing") + } + // One real colorant among them still marks. + mixed := cf_array(doc, Name("None"), Name("Ink")) + some, _ := colorspace_load(doc, + cf_array(doc, Name("DeviceN"), mixed, Name("DeviceRGB")), nil) + if some != nil { + testing.expect(t, !some.is_none, "one real colorant is still painted") + } + + // A DeviceN with no colorants at all is not a colour space. + _, e := colorspace_load(doc, cf_parse(doc, "[/DeviceN [] /DeviceRGB]"), nil) + testing.expect_value(t, e, Error.Bad_Colorspace) +} + +// --------------------------------------------------------------------------- +// Pattern +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_pattern :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + plain := load_cs(t, doc, "/Pattern") + if plain == nil { return } + testing.expect_value(t, plain.kind, Colorspace_Kind.Pattern) + testing.expect(t, plain.under == nil, "a coloured pattern has no underlying space") + // Nothing has been selected yet, so there are no components to carry -- + // `/P1 scn` is a name and no numbers, which is what cs.n has to say. + testing.expect_value(t, plain.n, 0) + testing.expect_value(t, colorspace_default_color(plain).n, 0) + // And no image can be in a Pattern space (8.9.5.1), so it has no default + // /Decode either. + testing.expect(t, colorspace_default_decode(plain, 8, context.allocator) == nil, + "a Pattern space has no image decode array") + + // An uncoloured pattern carries the colour it is painted in. + under := load_cs(t, doc, "[/Pattern /DeviceCMYK]") + if under == nil { return } + testing.expect_value(t, under.kind, Colorspace_Kind.Pattern) + testing.expect_value(t, under.n, 4) + testing.expect(t, under.under != nil && under.under.kind == .Device_CMYK, "underlying space") + expect_rgb(t, colorspace_to_rgb(under, color_make(0, 1, 1, 0)), 1, 0, 0, TOL, + "painted through the underlying space") + // Whose initial colour is the underlying space's. + c := colorspace_default_color(under) + testing.expect_value(t, c.n, 4) + testing.expect_value(t, c.v[3], 1) +} + +// --------------------------------------------------------------------------- +// Resource lookup, caching and malformed input +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_named_through_resources :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + res, _ := to_dict(cf_parse(doc, + "<< /ColorSpace << /CS0 [/Indexed /DeviceRGB 1 ] /CS1 /DeviceCMYK >> >>")) + cs0 := load_cs(t, doc, "/CS0", res) + if cs0 == nil { return } + testing.expect_value(t, cs0.kind, Colorspace_Kind.Indexed) + expect_rgb(t, colorspace_to_rgb(cs0, color_make(1)), 0, 1, 0, TOL, "resolved through /Resources") + + cs1 := load_cs(t, doc, "/CS1", res) + testing.expect(t, cs1 == colorspace_device_cmyk(doc), "a name for a device space") + + // A caller holding only the /ColorSpace dictionary may pass that instead. + csdict, _ := to_dict(cf_parse(doc, "<< /CSX /DeviceCMYK >>")) + testing.expect(t, load_cs(t, doc, "/CSX", csdict) == colorspace_device_cmyk(doc), + "the /ColorSpace dictionary works too") + + // A name that is in no resource dictionary is not a colour space. + _, err := colorspace_load(doc, cf_parse(doc, "/Nope"), res) + testing.expect_value(t, err, Error.Bad_Colorspace) + + // A device name cannot be redefined by a resource dictionary. + shadow, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /DeviceRGB /DeviceCMYK >> >>")) + testing.expect(t, load_cs(t, doc, "/DeviceRGB", shadow) == colorspace_device_rgb(doc), + "/DeviceRGB is always /DeviceRGB") +} + +// A page that names the same space in four hundred XObjects must build it once. +@(test) +test_colorspace_cached_by_object :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + a, _ := colorspace_load(doc, Ref{10, 0}, nil) + b, _ := colorspace_load(doc, Ref{10, 0}, nil) + testing.expect(t, a != nil && a == b, "the same object must build one space") + + // Reached by name through a resource dictionary, the space behind the name + // is the same object and so is cached as well. + res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /CS0 10 0 R >> >>")) + c := load_cs(t, doc, "/CS0", res) + testing.expect(t, c == a, "the name resolves onto the cached space") + + // A Separation loaded twice reuses the loaded tint transform, too. + s1, _ := colorspace_load(doc, Ref{5, 0}, nil) + s2, _ := colorspace_load(doc, Ref{5, 0}, nil) + testing.expect(t, s1 != nil && s1 == s2, "a Separation is cached") +} + +// A colour space that names itself must stop rather than recursing forever. +@(test) +test_colorspace_self_reference :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /Self 9 0 R >> >>")) + cs, err := colorspace_load(doc, cf_parse(doc, "/Self"), res) + testing.expect(t, cs == nil, "a self-naming space cannot be built") + testing.expect_value(t, err, Error.Recursion_Limit) + testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "and it is a repair") +} + +@(test) +test_colorspace_rejects_nonsense :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + _, e1 := colorspace_load(doc, nil, nil) + testing.expect_value(t, e1, Error.Bad_Colorspace) + + _, e2 := colorspace_load(doc, cf_parse(doc, "42"), nil) + testing.expect_value(t, e2, Error.Bad_Colorspace) + + _, e3 := colorspace_load(doc, cf_parse(doc, "[]"), nil) + testing.expect_value(t, e3, Error.Bad_Colorspace) + + _, e4 := colorspace_load(doc, cf_parse(doc, "[/NotASpace 1 2 3]"), nil) + testing.expect_value(t, e4, Error.Bad_Colorspace) + + // Converting through no space at all is black, not a crash. + expect_rgb(t, colorspace_to_rgb(nil, color_make(1, 1, 1)), 0, 0, 0, TOL, "nil space") + testing.expect_value(t, colorspace_default_color(nil).n, 1) + testing.expect(t, colorspace_default_decode(nil, 8) == nil, "nil space has no decode") +} + +// --------------------------------------------------------------------------- +// Initial colours and image decode arrays +// --------------------------------------------------------------------------- + +@(test) +test_colorspace_default_color :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + gray := colorspace_default_color(colorspace_device_gray(doc)) + testing.expect_value(t, gray.n, 1) + testing.expect_value(t, gray.v[0], 0) + + rgb := colorspace_default_color(colorspace_device_rgb(doc)) + testing.expect_value(t, rgb.n, 3) + expect_rgb(t, colorspace_to_rgb(colorspace_device_rgb(doc), rgb), 0, 0, 0, TOL, "black") + + // DeviceCMYK starts at 0 0 0 1 -- black -- not at four zeros, which would + // be white (8.6.4.4). + cmyk := colorspace_default_color(colorspace_device_cmyk(doc)) + testing.expect_value(t, cmyk.n, 4) + testing.expect_value(t, cmyk.v[3], 1) + expect_rgb(t, colorspace_to_rgb(colorspace_device_cmyk(doc), cmyk), 0, 0, 0, TOL, "black") + + // Indexed starts at entry zero. + idx, _ := colorspace_load(doc, Ref{10, 0}, nil) + if idx != nil { + c := colorspace_default_color(idx) + testing.expect_value(t, c.n, 1) + testing.expect_value(t, c.v[0], 0) + } + + // Lab starts at 0 0 0, which is black. + lab := load_cs(t, doc, "[/Lab << /WhitePoint [0.9642 1 0.8249] >>]") + if lab != nil { + c := colorspace_default_color(lab) + testing.expect_value(t, c.n, 3) + expect_rgb(t, colorspace_to_rgb(lab, c), 0, 0, 0, TOL, "black") + } +} + +@(test) +test_colorspace_default_decode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + gray := colorspace_default_decode(colorspace_device_gray(doc), 8) + defer delete(gray) + testing.expect_value(t, len(gray), 2) + testing.expect_value(t, gray[0], 0) + testing.expect_value(t, gray[1], 1) + + cmyk := colorspace_default_decode(colorspace_device_cmyk(doc), 8) + defer delete(cmyk) + testing.expect_value(t, len(cmyk), 8) + testing.expect_value(t, cmyk[7], 1) + + // Indexed maps the raw sample straight onto the table, so its default + // depends on the bit depth. + idx, _ := colorspace_load(doc, Ref{10, 0}, nil) + if idx != nil { + Depth :: struct { bpc: int, want: f64 } + depths := []Depth{ + Depth{1, 1}, Depth{2, 3}, Depth{4, 15}, Depth{8, 255}, Depth{16, 65535}, + } + for c in depths { + d := colorspace_default_decode(idx, c.bpc) + defer delete(d) + testing.expect_value(t, len(d), 2) + testing.expect_value(t, d[0], 0) + testing.expectf(t, d[1] == c.want, "bpc %d: got %f, want %f", c.bpc, d[1], c.want) + } + } + + // Lab's default decode is its own component ranges. + lab := load_cs(t, doc, + "[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-90 80 -70 60] >>]") + if lab != nil { + d := colorspace_default_decode(lab, 8) + defer delete(d) + testing.expect_value(t, len(d), 6) + testing.expect_value(t, d[0], 0) + testing.expect_value(t, d[1], 100) + testing.expect_value(t, d[2], -90) + testing.expect_value(t, d[3], 80) + testing.expect_value(t, d[4], -70) + testing.expect_value(t, d[5], 60) + } + + // A Separation's decode is one 0..1 pair, whatever its alternate space is. + sep, _ := colorspace_load(doc, Ref{5, 0}, nil) + if sep != nil { + d := colorspace_default_decode(sep, 8) + defer delete(d) + testing.expect_value(t, len(d), 2) + testing.expect_value(t, d[1], 1) + } +} + +// A colour space nested inside another still converts: an Indexed table whose +// entries are spot-colour tints. +@(test) +test_colorspace_nested :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs, err := colorspace_load(doc, + cf_array(doc, Name("Indexed"), Ref{5, 0}, i64(1), String(transmute([]byte)string("\x00\xff"))), + nil) + testing.expect_value(t, err, Error.None) + if cs == nil { return } + testing.expect(t, cs.base != nil && cs.base.kind == .Separation, "the base is the spot colour") + // Entry 0 is no tint at all, entry 1 is full tint: white, then red. + expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "entry 0") + expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "entry 1") +} diff --git a/content.odin b/content.odin new file mode 100644 index 0000000..4978aa9 --- /dev/null +++ b/content.odin @@ -0,0 +1,2641 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" + +// The content-stream interpreter (ISO 32000-2, 8.2 and Annex A). +// +// A content stream is postfix: operands are pushed until an operator arrives, +// the operator consumes the last few of them, and the stack is emptied again. +// That is the whole of the syntax, and it is why an unrecognised operator is +// harmless -- it takes its operands with it and the next one starts clean. +// +// This file is where every other file in the library meets. It resolves names +// against /Resources, keeps the graphics state, turns strings into positioned +// glyphs, and hands finished drawing operations to a Device. It never +// rasterises anything and never reaches into a device: everything goes through +// the `dev_*` wrappers in device.odin, so the three devices and this +// interpreter could be, and were, written apart from one another. +// +// THE THINGS THAT ARE EASY TO GET WRONG +// +// * `W` and `W*` do not clip. They arm a clip that the NEXT painting +// operator installs, after it has painted, using the path it painted. +// `W n` is the common spelling and paints nothing at all. +// * `Q` must restore the device clip stack to the depth its `q` saw, or +// everything after it draws through the wrong clip. `Graphics_State` +// carries `clip_depth` for exactly this. +// * A `q` that overflows the depth cap must not be matched by a later `Q`, +// or the stack underflows and the rest of the page draws in the state of +// whatever enclosed it. +// * Word spacing applies to a single-byte code 32 and to nothing else -- +// never to a two-byte 0x0020 in a composite font, which is an ordinary +// CID (9.3.3). `Font_Code.is_space` is the only test. +// * Pattern space is anchored to the default space of the stream the +// pattern is a resource of, not to the CTM of the fill that paints it +// (8.7.3.1). `base_ctm` is that space; `gs.ctm` is not. +// * Inline image data is binary and is not lexed. Finding its end wrongly +// desynchronises every operator after it. +// +// TWO THINGS A DEVICE AUTHOR HAS TO KNOW +// +// * The interpreter counts the clips IT pushes, not the ones a device chose +// to honour, so a device that implements `pop_clip` must implement every +// `clip_*` hook it can be handed. Leaving `clip_stroke_path` nil while +// implementing `pop_clip` unbalances the stack: a patterned stroke pushes +// through the one and pops through the other. +// * A Type 3 glyph run reaches the device through `ignore_text` whatever the +// render mode is, because the drawing is done by running the glyph +// descriptions -- which arrive as ordinary fills and strokes. A device +// that wants the text of a Type 3 font must implement `ignore_text`. +// +// EVERYTHING IS BOUNDED +// +// A content stream is the most attacker-controlled data in a PDF. One fuel +// counter, shared by the whole run, bounds every token the interpreter reads +// anywhere -- in the page, in a form it invokes, in a Type 3 glyph, in a +// tiling pattern's cell. Recursion is depth-capped and guarded by a visited +// set, the operand stack is fixed, and every operand that reaches the geometry +// is checked for finiteness. Nothing here can fail a page: a malformed +// operator records a `Repair` and the next one runs. + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +// Tokens -- operands and operators together -- one run may consume, including +// everything it recurses into. A 5 MB content stream is about a million +// tokens, so this is several of the largest pages anyone has drawn; past it a +// stream is looping, not drawing. +@(private) +DEFAULT_CONTENT_FUEL :: 1 << 22 + +// Nested BMC/BDC sections. The format has no limit and neither do producers, +// but a page that nests marked content 256 deep is not marking anything up. +@(private) +MAX_MARKED_DEPTH :: 256 + +// Cells of one tiling pattern replayed for one fill. `pattern_tiles` caps the +// lattice itself much higher; this is the cap on running a content stream per +// cell, which is thousands of times more expensive than placing one. +@(private) +MAX_TILE_REPLAYS :: 8192 + +// Soft masks one PAGE may install, counted on the shared state rather than on +// one run: a page's content stream and every one of its annotations are +// separate `run_content` calls, and a per-run cap would let MAX_ANNOTS of them +// multiply it. `q /GS gs Q` repeated a million times must not be a million +// allocations. +@(private) +MAX_SOFT_MASKS :: 1024 + +// Inline images one PAGE may read, counted on the shared state rather than on +// one run for the same reason the soft-mask budget is. Each one keeps its +// dictionary, its payload and its Image on the Document, none of which the +// page's scratch reclaims and none of which a second render of the same page +// reuses, and `BI ID EI` costs two tokens -- so the token budget alone +// permitted two million of them from a 37 KB file, which measured 1.35 GB per +// render. Four thousand is far past what any producer emits: 8.9.7 means an +// inline image for the handful of small ones a page carries -- it recommends +// they stay under 4 KiB each -- and a page with thousands of them is using +// them as a bomb, not as pictures. +@(private) +MAX_INLINE_IMAGES :: 4096 + +// Bytes of inline image data one BI/ID/EI may carry. 8.9.7 recommends 4 KiB; +// producers exceed it, but a 16 MB inline image is a decompression bomb +// rather than a picture. +@(private) +MAX_INLINE_IMAGE_BYTES :: 1 << 24 + +// Key/value pairs read from an inline image dictionary before the scan for +// `ID` gives up. The format defines eleven. +@(private) +MAX_INLINE_KEYS :: 64 + +// Entries read from an /OCProperties list. +@(private) +MAX_OC_GROUPS :: 4096 + +// Glyphs accumulated for one text-clipping object. +@(private) +MAX_TEXT_CLIP_ITEMS :: 1 << 16 + +// --------------------------------------------------------------------------- +// Options +// --------------------------------------------------------------------------- + +// Content_Options tunes one run of the interpreter. +Content_Options :: struct { + // Scratch memory for everything the run allocates: the operand stack's + // arrays, the path under construction, glyph runs, tile lattices, and the + // string bodies the lexer materialises. + // + // It MUST be an arena or another bulk-freeing allocator: the run frees + // nothing individually, by design -- a page of text is tens of thousands + // of tiny allocations and a per-page arena reclaims them in one call. + // Leave it zero and the run makes, and destroys, a growing arena of its + // own, which is correct but costs one address-space reservation per call. + allocator: mem.Allocator, + + // Tokens the whole run may consume, nested forms, Type 3 glyphs, soft + // masks and tiling replays included. 0 selects DEFAULT_CONTENT_FUEL. + max_ops: int, + + // Draw every optional-content group, whatever the catalog's /OCProperties + // says is switched off. Text extraction usually wants this -- hidden text + // is still text -- and a renderer usually does not. + ignore_optional_content: bool, + + // State shared by every run of ONE page. A page is not one run: the + // content stream is one and each annotation's appearance stream is + // another, and the things that have to be bounded per page rather than per + // run -- the soft-mask and inline-image budgets -- live here. + // + // It MUST come from the same memory as `allocator` and live exactly as + // long, because the soft masks it remembers are allocated from that + // allocator. `page_run` puts both in the page's scratch arena, which is + // what makes that automatic. Leave it nil and the run gets one of its own, + // which is correct for a single stream and is what every caller outside + // `page.odin` wants. + shared: ^Content_Shared, +} + +// Content_Shared is the per-page state described on `Content_Options.shared`. +@(private) +Content_Shared :: struct { + n_soft_masks: int, + n_inline: int, + + // The map every BI parses its dictionary into, cleared and reused. Only a + // BI that goes on to build an Image gets a dictionary of its own, because + // only that one outlives the run. + inline_dict: ^Dict, + + // The ExtGState /SMask dictionary and CTM of the most recent install, and + // the Soft_Mask built for it. `/GS gs` repeated -- the same ExtGState + // applied to run after run of content -- is the common shape, and building + // a fresh Soft_Mask for each is an allocation and a slot of the budget + // spent on something already in hand. + last_smd: ^Dict, + last_ctm: Matrix, + last_mask: ^Soft_Mask, +} + +// --------------------------------------------------------------------------- +// Run state +// --------------------------------------------------------------------------- + +// Content_Ctx is what every stream in one run shares: the sink, the budget, +// and the guards against a stream that draws itself. +@(private) +Content_Ctx :: struct { + doc: ^Document, + dev: ^Device, + alloc: mem.Allocator, + opts: Content_Options, + + // Tokens left. Shared, so a form cannot buy itself more by recursing. + fuel: int, + depth: int, + + // Streams currently being executed, keyed by address. A form or a pattern + // cell that invokes itself is stopped here; one invoked twice in sequence + // is not, because the entry is removed on the way out. + visited: map[uintptr]bool, + + // Optional-content groups the catalog's default configuration switches + // off, keyed by the address of their dictionary. `doc.cache` gives one + // address per object number, which is what makes the key meaningful. + oc_off: map[uintptr]bool, + + // Per-page state, shared with the other runs of this page. Never nil: a + // run given none in its options gets `own_shared` below. + shared: ^Content_Shared, + own_shared: Content_Shared, + + // One set of scratch buffers per recursion level. Nesting is strictly + // stacked and depth-capped, so level N's buffers are free the moment + // level N returns -- and a tiling pattern that replays its cell eight + // thousand times must not allocate eight thousand operand stacks. The + // scratch allocator is an arena and reclaims nothing until the run ends, + // so reuse here is the difference between kilobytes and hundreds of + // megabytes. + bufs: [MAX_DEPTH]Content_Buffers, +} + +// Content_Buffers is everything a Content_Runner allocates, kept so the next +// run at the same depth can have it back with its capacity intact. +@(private) +Content_Buffers :: struct { + ready: bool, + stack: Gstate_Stack, + rect: Path, + boxes: [dynamic]Rect, + path: Path, + text: Text, + tclip: Text, + codes: [dynamic]Font_Code, + mc: [dynamic]Mc_Entry, +} + +@(private) +Content_Flag :: enum u8 { + // Colour operators do nothing: inside an uncoloured tiling pattern's cell + // and inside a Type 3 glyph that declared itself a stencil with `d1`. + Ignore_Color, + // The stream describes a Type 3 glyph, so `d0` and `d1` mean something. + Type3_Glyph, +} + +@(private) +Content_Flags :: bit_set[Content_Flag; u8] + +@(private) +Pending_Clip :: enum u8 { + None, + Nonzero, + Even_Odd, +} + +// Mc_Entry is one open marked-content section, so that the right `EMC` closes +// the right thing. +@(private) +Mc_Entry :: struct { + // dev_begin_layer was called and the matching EMC must end it. + layer: bool, + // This section is the one that switched drawing off. + hides: bool, +} + +@(private) +Content_Runner :: struct { + ctx: ^Content_Ctx, + res: ^Dict, + gs: Graphics_State, + stack: Gstate_Stack, + + // A conservative device-space bound on the clip, used to size the area a + // tiling pattern has to cover. It is saved and restored with the graphics + // state; `Graphics_State` is shared with three devices and frozen, so the + // box rides in a stack of its own pushed in lockstep with that one. + clip_box: Rect, + box_stack: [dynamic]Rect, + + // The default space of this stream: what pattern space is anchored to. + // For a page it is the CTM `run_content` was given, for a form the CTM + // including the form's /Matrix, for a Type 3 glyph the glyph's own. + base_ctm: Matrix, + + ops: [MAX_CONTENT_OPERANDS]Object, + n_ops: int, + + path: Path, + // A scratch path for the rectangles the interpreter builds itself -- a + // form's /BBox, a tiling cell's, a Type 3 glyph's. Reused rather than + // built per use: a pattern replayed four thousand times would otherwise + // leave four thousand of them in an arena that reclaims nothing. + rect: Path, + pending_clip: Pending_Clip, + + // Device clip entries this stream has pushed and not yet popped. + clip_depth: int, + // `q`s that overflowed the gstate cap. Their `Q`s must be swallowed + // rather than popping somebody else's state. + q_overflow: int, + + in_text: bool, + tm, tlm: Matrix, + text: Text, + codes: [dynamic]Font_Code, + // Glyphs accumulated for a text clip, which is the union of everything + // shown in clipping mode between BT and ET and is installed at ET. + tclip: Text, + tclip_on: bool, + + mc: [dynamic]Mc_Entry, + mc_over: int, + n_hidden: int, + + // BX/EX nesting: inside it an unknown operator is expected, not damage. + compat: int, + + flags: Content_Flags, +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +// run_content executes one content stream against a Device. +// +// `content` is the concatenated bytes of the stream -- `page_content` builds +// that for a page -- `res` is the /Resources dictionary every name in it is +// looked up in, and `ctm` maps the stream's default user space to device +// space. For a page that is the flip from PDF's y-up space to the bitmap's +// y-down one, composed with the zoom and /Rotate. +// +// Nothing here is fatal. An operator that cannot run records a `Repair` on the +// Document and the next one runs, so a page whose ninth operator is garbage +// still draws the other several thousand. .Invalid_Argument comes back only +// when there is no Document or the matrix is not a matrix; every other return +// is .None. +// +// The Device may be nil, which runs the whole interpreter and draws nothing. +// That is not a curiosity: it is how the interpreter is measured, and it is +// what a caller that only wants the repairs a page produces asks for. +@(private) +run_content :: proc(doc: ^Document, dev: ^Device, content: []byte, res: ^Dict, + ctm: Matrix, opts := Content_Options{}) -> Error { + if doc == nil { + return .Invalid_Argument + } + if !matrix_is_finite(ctm) { + return .Invalid_Argument + } + if len(content) == 0 { + return .None + } + + // A caller that supplies no scratch allocator gets one that is certainly + // an arena, because the run frees nothing individually and handing it the + // context allocator would leak every string the lexer materialises. + arena: virtual.Arena + alloc := opts.allocator + if alloc.procedure == nil { + if arena_err := virtual.arena_init_growing(&arena); arena_err != nil { + return .Out_Of_Memory + } + alloc = virtual.arena_allocator(&arena) + } + defer if opts.allocator.procedure == nil { + virtual.arena_destroy(&arena) + } + + ctx := Content_Ctx{ + doc = doc, + dev = dev, + alloc = alloc, + opts = opts, + fuel = opts.max_ops > 0 ? opts.max_ops : DEFAULT_CONTENT_FUEL, + } + ctx.shared = opts.shared != nil ? opts.shared : &ctx.own_shared + ctx.visited = make(map[uintptr]bool, 8, alloc) + ctx.oc_off = make(map[uintptr]bool, 8, alloc) + defer delete(ctx.visited) + defer delete(ctx.oc_off) + content_load_oc(&ctx) + + gs := gstate_default(ctm) + // 8.6.8: both colours start as black in DeviceGray. gstate_default cannot + // fill that in itself -- it has no Document to resolve the space with. + gs.fill.cs = colorspace_device_gray(doc) + gs.fill.color = colorspace_default_color(gs.fill.cs) + gs.stroke.cs = gs.fill.cs + gs.stroke.color = gs.fill.color + + return content_exec(&ctx, content, res, gs, ctm, RECT_INFINITE, {}) +} + +// content_exec runs one stream: the page, a form, a Type 3 glyph, a pattern +// cell or a soft mask's group. Everything that differs between them is in the +// arguments. +@(private) +content_exec :: proc(ctx: ^Content_Ctx, data: []byte, res: ^Dict, gs: Graphics_State, + base_ctm: Matrix, clip_box: Rect, flags: Content_Flags) -> Error { + if ctx == nil || ctx.doc == nil { + return .Invalid_Argument + } + if ctx.depth >= MAX_DEPTH { + add_repair(ctx.doc, .Recursion_Limit_Hit, 0) + return .Recursion_Limit + } + b := &ctx.bufs[ctx.depth] + ctx.depth += 1 + defer ctx.depth -= 1 + + r := Content_Runner{ + ctx = ctx, + res = res, + gs = gs, + base_ctm = base_ctm, + clip_box = clip_box, + flags = flags, + tm = MATRIX_IDENTITY, + tlm = MATRIX_IDENTITY, + } + content_runner_acquire(&r, b) + defer content_runner_release(&r, b) + + lex := lexer_make(data) + for { + if ctx.fuel <= 0 { + // Out of budget. Whatever has been drawn stays drawn. + add_repair(ctx.doc, .Content_Syntax, i64(lex.pos)) + break + } + ctx.fuel -= 1 + + tok := lexer_next(&lex, ctx.alloc) + switch tok.kind { + case .EOF: + content_finish(&r) + return .None + + case .Integer: + content_push(&r, tok.int_value) + case .Real: + content_push(&r, tok.real_value) + case .String: + content_push(&r, String(tok.bytes)) + case .Name: + content_push(&r, Name(tok.text)) + + case .Array_Open, .Dict_Open: + // Composite operands -- a TJ array, a `d` dash array, a BDC + // property dictionary -- go through the object parser rather than + // being rebuilt here. Depth is seeded at 1 so that `<<...>>` + // cannot be read as the head of an indirect stream: in a content + // stream it never is. + p := parser_make(ctx.doc, data, ctx.alloc) + p.lex.pos = tok.start + p.depth = 1 + obj, perr := parse_object(&p) + lex.pos = max(p.lex.pos, tok.end) + if perr == .None { + content_push(&r, obj) + } + + case .Keyword: + switch tok.text { + case "true": content_push(&r, true) + case "false": content_push(&r, false) + case "null": content_push(&r, nil) + case "BI": + content_inline_image(&r, &lex) + r.n_ops = 0 + case: + content_op(&r, tok.text) + r.n_ops = 0 + } + + case .Array_Close, .Dict_Close, .Brace_Open, .Brace_Close, .Error: + // A stray delimiter. The lexer consumed a byte, so progress is + // guaranteed; dropping it is what every viewer does. + if r.compat == 0 { + add_repair(ctx.doc, .Content_Syntax, i64(tok.start)) + } + } + } + + content_finish(&r) + return .None +} + +// content_finish closes everything the stream left open. A truncated stream is +// the normal case, not an exceptional one, and the device's clip stack has to +// balance whatever the bytes did. +@(private) +content_finish :: proc(r: ^Content_Runner) { + if r.in_text { + content_text_end(r) + } + // Layers close in the order they opened. + for len(r.mc) > 0 { + e := pop(&r.mc) + if e.layer { + dev_end_layer(r.ctx.dev) + } + } + if gstate_depth(&r.stack) > 0 || r.q_overflow > 0 { + add_repair(r.ctx.doc, .Unbalanced_Q, 0) + } + for r.clip_depth > 0 { + dev_pop_clip(r.ctx.dev) + r.clip_depth -= 1 + } +} + +// content_runner_acquire hands the runner this depth's buffers, empty but +// with whatever capacity a previous run at the same depth grew them to. +@(private) +content_runner_acquire :: proc(r: ^Content_Runner, b: ^Content_Buffers) { + if !b.ready { + b.ready = true + b.stack = gstate_stack_make(r.ctx.alloc) + b.boxes = make([dynamic]Rect, 0, 8, r.ctx.alloc) + b.path = path_make(r.ctx.alloc, 32) + b.rect = path_make(r.ctx.alloc, 8) + b.text = text_make(r.ctx.alloc) + b.tclip = text_make(r.ctx.alloc) + b.codes = make([dynamic]Font_Code, 0, 32, r.ctx.alloc) + b.mc = make([dynamic]Mc_Entry, 0, 8, r.ctx.alloc) + } else { + clear(&b.stack.items) + clear(&b.boxes) + path_clear(&b.path) + path_clear(&b.rect) + clear(&b.text.items) + clear(&b.tclip.items) + clear(&b.codes) + clear(&b.mc) + } + b.text.font = nil + b.tclip.font = nil + r.stack = b.stack + r.box_stack = b.boxes + r.path = b.path + r.rect = b.rect + r.text = b.text + r.tclip = b.tclip + r.codes = b.codes + r.mc = b.mc +} + +// content_runner_release gives them back. The arrays are copied out by value, +// so anything the run grew has to be written back or the capacity is lost -- +// and, worse, a stale pointer would be handed to the next run at this depth. +@(private) +content_runner_release :: proc(r: ^Content_Runner, b: ^Content_Buffers) { + b.stack = r.stack + b.boxes = r.box_stack + b.path = r.path + b.rect = r.rect + b.text = r.text + b.tclip = r.tclip + b.codes = r.codes + b.mc = r.mc + r^ = {} +} + +// --------------------------------------------------------------------------- +// The operand stack +// +// Operators read their arguments from the END of the stack, so a stream that +// pushes more operands than an operator takes -- which damaged files do +// constantly -- still runs it with the right ones. +// --------------------------------------------------------------------------- + +@(private) +content_push :: proc(r: ^Content_Runner, o: Object) { + if r.n_ops >= MAX_CONTENT_OPERANDS { + // Full. Drop the oldest: the operator that eventually arrives wants + // the operands nearest to it, not the ones a hostile stream led with. + copy(r.ops[0:MAX_CONTENT_OPERANDS - 1], r.ops[1:MAX_CONTENT_OPERANDS]) + r.ops[MAX_CONTENT_OPERANDS - 1] = o + return + } + r.ops[r.n_ops] = o + r.n_ops += 1 +} + +// content_arg is argument `i` of an operator taking `n` of them. +@(private) +content_arg :: proc(r: ^Content_Runner, n, i: int) -> Object { + idx := r.n_ops - n + i + if idx < 0 || idx >= r.n_ops { + return nil + } + return r.ops[idx] +} + +@(private) +content_num :: proc(r: ^Content_Runner, n, i: int) -> f64 { + v, ok := to_number(content_arg(r, n, i)) + if !ok || !is_finite(v) { + return 0 + } + return v +} + +@(private) +content_name :: proc(r: ^Content_Runner, n, i: int) -> string { + v, _ := to_name(content_arg(r, n, i)) + return v +} + +// content_need reports whether the operator has its operands. Too few is a +// malformed stream, not a fatal one: the operator is skipped and the next runs. +@(private) +content_need :: proc(r: ^Content_Runner, n: int) -> bool { + if r.n_ops >= n { + return true + } + if r.compat == 0 { + add_repair(r.ctx.doc, .Content_Syntax, 0) + } + return false +} + +// --------------------------------------------------------------------------- +// Dispatch +// +// One switch over every operator in Annex A. An operator that is not here +// falls through to the default, which records a repair (unless a BX/EX +// bracket said to expect it) and lets the stack be cleared -- which is what +// makes an unknown operator harmless rather than fatal. +// --------------------------------------------------------------------------- + +@(private) +content_op :: proc(r: ^Content_Runner, op: string) { + switch op { + + // -- graphics state (8.4.4) ------------------------------------------- + case "q": + saved := r.gs + saved.clip_depth = r.clip_depth + if !gstate_push(&r.stack, saved) { + // The cap was hit. Remember that this `q` saved nothing, so its + // `Q` pops nothing: the alternative is an underflow that leaves + // the rest of the page in an enclosing scope's state. + r.q_overflow += 1 + add_repair(r.ctx.doc, .Unbalanced_Q, 0) + return + } + append(&r.box_stack, r.clip_box) + + case "Q": + if r.q_overflow > 0 { + r.q_overflow -= 1 + return + } + saved, ok := gstate_pop(&r.stack) + if !ok { + // 8.4.4: a `Q` with nothing to restore. Ignoring it is what every + // viewer does; the alternative is discarding the page. + add_repair(r.ctx.doc, .Unbalanced_Q, 0) + return + } + for r.clip_depth > saved.clip_depth { + dev_pop_clip(r.ctx.dev) + r.clip_depth -= 1 + } + r.gs = saved + if len(r.box_stack) > 0 { + r.clip_box = pop(&r.box_stack) + } + + case "cm": + if !content_need(r, 6) { return } + m := Matrix{ + content_num(r, 6, 0), content_num(r, 6, 1), content_num(r, 6, 2), + content_num(r, 6, 3), content_num(r, 6, 4), content_num(r, 6, 5), + } + next := matrix_mul(m, r.gs.ctm) + if !matrix_is_finite(next) { + add_repair(r.ctx.doc, .Content_Syntax, 0) + return + } + r.gs.ctm = next + + case "w": + if !content_need(r, 1) { return } + r.gs.stroke_state.line_width = max(content_num(r, 1, 0), 0) + case "J": + if !content_need(r, 1) { return } + r.gs.stroke_state.line_cap = Line_Cap(u8(clamp(content_num(r, 1, 0), 0, 2))) + case "j": + if !content_need(r, 1) { return } + r.gs.stroke_state.line_join = Line_Join(u8(clamp(content_num(r, 1, 0), 0, 2))) + case "M": + if !content_need(r, 1) { return } + // A miter limit below 1 is geometrically impossible and turns every + // join into a bevel; clamping keeps the stroker's ratio test sane. + r.gs.stroke_state.miter_limit = max(content_num(r, 1, 0), 1) + case "d": + if !content_need(r, 2) { return } + content_set_dash(r, content_arg(r, 2, 0), content_num(r, 2, 1)) + case "ri": + if !content_need(r, 1) { return } + r.gs.render_intent = content_name(r, 1, 0) + case "i": + if !content_need(r, 1) { return } + r.gs.flatness = clamp(content_num(r, 1, 0), 0, 100) + case "gs": + if !content_need(r, 1) { return } + content_ext_gstate(r, content_name(r, 1, 0)) + + // -- path construction (8.5.2) ---------------------------------------- + case "m": + if !content_need(r, 2) { return } + path_move_to(&r.path, content_num(r, 2, 0), content_num(r, 2, 1)) + case "l": + if !content_need(r, 2) { return } + path_line_to(&r.path, content_num(r, 2, 0), content_num(r, 2, 1)) + case "c": + if !content_need(r, 6) { return } + path_curve_to(&r.path, + content_num(r, 6, 0), content_num(r, 6, 1), + content_num(r, 6, 2), content_num(r, 6, 3), + content_num(r, 6, 4), content_num(r, 6, 5)) + case "v": + if !content_need(r, 4) { return } + path_curve_to_v(&r.path, + content_num(r, 4, 0), content_num(r, 4, 1), + content_num(r, 4, 2), content_num(r, 4, 3)) + case "y": + if !content_need(r, 4) { return } + path_curve_to_y(&r.path, + content_num(r, 4, 0), content_num(r, 4, 1), + content_num(r, 4, 2), content_num(r, 4, 3)) + case "h": + path_close(&r.path) + case "re": + if !content_need(r, 4) { return } + path_rect(&r.path, + content_num(r, 4, 0), content_num(r, 4, 1), + content_num(r, 4, 2), content_num(r, 4, 3)) + + // -- path painting (8.5.3) -------------------------------------------- + case "S": content_paint(r, false, false, true, false) + case "s": content_paint(r, true, false, true, false) + case "f", "F": + content_paint(r, false, true, false, false) + case "f*": content_paint(r, false, true, false, true) + case "B": content_paint(r, false, true, true, false) + case "B*": content_paint(r, false, true, true, true) + case "b": content_paint(r, true, true, true, false) + case "b*": content_paint(r, true, true, true, true) + case "n": content_paint(r, false, false, false, false) + + // -- clipping (8.5.4) ------------------------------------------------- + // W and W* do not clip. They arm a clip that the next painting operator + // installs, out of the path that operator painted. + case "W": r.pending_clip = .Nonzero + case "W*": r.pending_clip = .Even_Odd + + // -- text objects and state (9.4.1, 9.3) ------------------------------ + case "BT": content_text_begin(r) + case "ET": content_text_end(r) + + case "Tc": + if !content_need(r, 1) { return } + r.gs.text.char_spacing = content_num(r, 1, 0) + case "Tw": + if !content_need(r, 1) { return } + r.gs.text.word_spacing = content_num(r, 1, 0) + case "Tz": + if !content_need(r, 1) { return } + // The operand is a percentage; Text_State holds the fraction. + r.gs.text.horiz_scale = content_num(r, 1, 0) / 100 + case "TL": + if !content_need(r, 1) { return } + r.gs.text.leading = content_num(r, 1, 0) + case "Ts": + if !content_need(r, 1) { return } + r.gs.text.rise = content_num(r, 1, 0) + case "Tr": + if !content_need(r, 1) { return } + r.gs.text.render_mode = Text_Render_Mode(u8(clamp(content_num(r, 1, 0), 0, 7))) + case "Tf": + if !content_need(r, 2) { return } + r.gs.text.size = content_num(r, 2, 1) + content_set_font(r, content_name(r, 2, 0)) + + case "Td": + if !content_need(r, 2) { return } + content_text_move(r, content_num(r, 2, 0), content_num(r, 2, 1)) + case "TD": + if !content_need(r, 2) { return } + r.gs.text.leading = -content_num(r, 2, 1) + content_text_move(r, content_num(r, 2, 0), content_num(r, 2, 1)) + case "Tm": + if !content_need(r, 6) { return } + m := Matrix{ + content_num(r, 6, 0), content_num(r, 6, 1), content_num(r, 6, 2), + content_num(r, 6, 3), content_num(r, 6, 4), content_num(r, 6, 5), + } + if !matrix_is_finite(m) { + add_repair(r.ctx.doc, .Content_Syntax, 0) + return + } + r.tm = m + r.tlm = m + case "T*": + content_text_move(r, 0, -r.gs.text.leading) + + case "Tj": + if !content_need(r, 1) { return } + if s, ok := to_bytes(content_arg(r, 1, 0)); ok { + content_show(r, s) + } + case "TJ": + if !content_need(r, 1) { return } + content_show_array(r, content_arg(r, 1, 0)) + case "'": + if !content_need(r, 1) { return } + content_text_move(r, 0, -r.gs.text.leading) + if s, ok := to_bytes(content_arg(r, 1, 0)); ok { + content_show(r, s) + } + case "\"": + if !content_need(r, 3) { return } + r.gs.text.word_spacing = content_num(r, 3, 0) + r.gs.text.char_spacing = content_num(r, 3, 1) + content_text_move(r, 0, -r.gs.text.leading) + if s, ok := to_bytes(content_arg(r, 3, 2)); ok { + content_show(r, s) + } + + // -- Type 3 glyph metrics (9.6.4) ------------------------------------- + case "d0": + // The advance is taken from /Widths, which is where every other part + // of the library reads it; d0 is accepted and adds nothing. + if !content_need(r, 2) { return } + case "d1": + if !content_need(r, 6) { return } + content_type3_d1(r) + + // -- colour (8.6.8) --------------------------------------------------- + case "CS": + if !content_need(r, 1) { return } + content_set_colorspace(r, &r.gs.stroke, content_name(r, 1, 0)) + case "cs": + if !content_need(r, 1) { return } + content_set_colorspace(r, &r.gs.fill, content_name(r, 1, 0)) + case "SC", "SCN": + content_set_color(r, &r.gs.stroke) + case "sc", "scn": + content_set_color(r, &r.gs.fill) + case "G": + if !content_need(r, 1) { return } + content_set_device_color(r, &r.gs.stroke, colorspace_device_gray(r.ctx.doc), 1) + case "g": + if !content_need(r, 1) { return } + content_set_device_color(r, &r.gs.fill, colorspace_device_gray(r.ctx.doc), 1) + case "RG": + if !content_need(r, 3) { return } + content_set_device_color(r, &r.gs.stroke, colorspace_device_rgb(r.ctx.doc), 3) + case "rg": + if !content_need(r, 3) { return } + content_set_device_color(r, &r.gs.fill, colorspace_device_rgb(r.ctx.doc), 3) + case "K": + if !content_need(r, 4) { return } + content_set_device_color(r, &r.gs.stroke, colorspace_device_cmyk(r.ctx.doc), 4) + case "k": + if !content_need(r, 4) { return } + content_set_device_color(r, &r.gs.fill, colorspace_device_cmyk(r.ctx.doc), 4) + + // -- shadings and XObjects -------------------------------------------- + case "sh": + if !content_need(r, 1) { return } + content_shading(r, content_name(r, 1, 0)) + case "Do": + if !content_need(r, 1) { return } + content_do(r, content_name(r, 1, 0)) + + // -- marked content (14.6) -------------------------------------------- + case "MP", "DP": + // A marked-content point. It opens nothing, so it closes nothing. + case "BMC": + content_mc_begin(r, "", nil) + case "BDC": + if !content_need(r, 2) { + // A short BDC still OPENS a section. Returning here would leave + // the matching EMC to close the enclosing one instead: hidden + // optional content would become visible for the rest of the outer + // section, and a device layer would end around the wrong span of + // drawing. An anonymous section is what BMC already opens, and it + // keeps the pairing right. + content_mc_begin(r, "", nil) + return + } + content_mc_begin(r, content_name(r, 2, 0), content_arg(r, 2, 1)) + case "EMC": + content_mc_end(r) + + // -- compatibility (8.2) ---------------------------------------------- + case "BX": + r.compat += 1 + case "EX": + if r.compat > 0 { + r.compat -= 1 + } + + // -- inline images ---------------------------------------------------- + case "ID", "EI": + // Reached only when a BI was lost or its data was mis-measured. The + // stack is cleared and the stream resynchronises on the next operator. + if r.compat == 0 { + add_repair(r.ctx.doc, .Content_Syntax, 0) + } + + case: + // Annex A is closed, but files invent operators and PDF 2.0 added + // some this library does not implement. Inside BX/EX that is + // explicitly allowed and is not damage. + if r.compat == 0 { + add_repair(r.ctx.doc, .Content_Syntax, 0) + } + } +} + +@(private) +content_set_dash :: proc(r: ^Content_Runner, arr: Object, phase: f64) { + a, ok := to_array(arr) + if !ok { + // `[] 0 d` is written as a bare `0 d` by some producers. Either way, + // no array means no dashing. + r.gs.stroke_state.dash_count = 0 + r.gs.stroke_state.dash_phase = 0 + return + } + n := min(array_len(a), MAX_DASH_ENTRIES) + for i in 0 ..< n { + v := array_num(r.ctx.doc, a, i, 0) + r.gs.stroke_state.dash[i] = is_finite(v) ? v : 0 + } + r.gs.stroke_state.dash_count = n + r.gs.stroke_state.dash_phase = is_finite(phase) ? phase : 0 +} + +// --------------------------------------------------------------------------- +// Colour +// --------------------------------------------------------------------------- + +// content_components reads the last `want` numbers of the `avail` operands. +// Reading from the end is what makes an operator survive a stream that pushed +// more operands than it takes, which damaged files do constantly. +@(private) +content_components :: proc(r: ^Content_Runner, avail, want: int) -> (c: Color) { + c.n = clamp(want, 0, MAX_COLOR_COMPONENTS) + for i in 0 ..< c.n { + idx := avail - c.n + i + if idx < 0 || idx >= avail || idx >= MAX_CONTENT_OPERANDS { + continue + } + if v, ok := to_number(r.ops[idx]); ok && is_finite(v) { + c.v[i] = v + } + } + return +} + +@(private) +content_set_colorspace :: proc(r: ^Content_Runner, src: ^Paint_Source, name: string) { + if .Ignore_Color in r.flags { + return + } + cs, err := colorspace_load(r.ctx.doc, Name(name), r.res) + if err != .None || cs == nil { + add_repair(r.ctx.doc, .Bad_Resource, 0) + return + } + // 8.6.8: selecting a space also resets the colour to that space's initial + // value -- black, not whatever the previous space's components were. + src.cs = cs + src.color = colorspace_default_color(cs) + src.pattern = nil +} + +@(private) +content_set_device_color :: proc(r: ^Content_Runner, src: ^Paint_Source, cs: ^Colorspace, n: int) { + if .Ignore_Color in r.flags { + return + } + src.cs = cs + src.color = content_components(r, r.n_ops, n) + src.pattern = nil +} + +@(private) +content_set_color :: proc(r: ^Content_Runner, src: ^Paint_Source) { + if .Ignore_Color in r.flags { + return + } + cs := src.cs + if cs == nil { + cs = colorspace_device_gray(r.ctx.doc) + src.cs = cs + } + + if cs.kind != .Pattern { + src.color = content_components(r, r.n_ops, cs.n) + src.pattern = nil + return + } + + // In a Pattern space the last operand names the pattern. Anything before + // it is the colour an uncoloured cell paints in, and lives in the + // underlying space (8.7.3.1). + avail := r.n_ops + name := "" + if avail > 0 { + if nm, ok := to_name(r.ops[avail - 1]); ok { + name = nm + avail -= 1 + } + } + if name == "" { + add_repair(r.ctx.doc, .Content_Syntax, 0) + return + } + if cs.under != nil { + src.color = content_components(r, avail, cs.under.n) + } + + pats := dict_dict(r.ctx.doc, r.res, "Pattern") + entry := dict_get_raw(pats, name) + if entry == nil { + add_repair(r.ctx.doc, .Bad_Resource, 0) + src.pattern = nil + return + } + pat, err := pattern_load(r.ctx.doc, entry, r.res) + if err != .None || pat == nil { + add_repair(r.ctx.doc, .Bad_Resource, 0) + src.pattern = nil + return + } + src.pattern = pat +} + +// content_paint_of resolves everything a device needs for one operation. +@(private) +content_paint_of :: proc(r: ^Content_Runner, src: ^Paint_Source, alpha: f64) -> (p: Paint) { + p.cs = src.cs + p.color = src.color + p.pattern = src.pattern + p.alpha = clamp(alpha, 0, 1) + p.blend = r.gs.blend + p.soft_mask = r.gs.soft_mask + if p.cs == nil { + p.cs = colorspace_device_gray(r.ctx.doc) + } + // An uncoloured pattern's components are in the underlying space, so that + // is what converts them; a coloured one carries no components at all. + conv := p.cs + if conv.kind == .Pattern { + conv = conv.under + } + if conv != nil && p.color.n > 0 { + p.rgb = colorspace_to_rgb(conv, p.color) + } + return +} + +// --------------------------------------------------------------------------- +// Path painting +// --------------------------------------------------------------------------- + +@(private) +content_paint :: proc(r: ^Content_Runner, close, fill, stroke, even_odd: bool) { + if close { + path_close(&r.path) + } + if r.n_hidden == 0 && !path_is_empty(&r.path) { + if fill { + content_fill(r, even_odd) + } + if stroke { + content_stroke(r) + } + } + // The clip comes last, and out of the path that was just painted. + content_apply_clip(r) + path_clear(&r.path) +} + +@(private) +content_fill :: proc(r: ^Content_Runner, even_odd: bool) { + src := &r.gs.fill + if src.cs != nil && src.cs.is_none { + // A /Separation /None colorant marks nothing at all (8.6.6.4). + return + } + if src.pattern != nil { + content_paint_pattern(r, src, even_odd, false) + return + } + if src.cs != nil && src.cs.kind == .Pattern { + // A Pattern space with no pattern selected paints nothing. + return + } + dev_fill_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm, + content_paint_of(r, src, r.gs.fill_alpha)) +} + +@(private) +content_stroke :: proc(r: ^Content_Runner) { + src := &r.gs.stroke + if src.cs != nil && src.cs.is_none { + return + } + if src.pattern != nil { + content_paint_pattern(r, src, false, true) + return + } + if src.cs != nil && src.cs.kind == .Pattern { + return + } + dev_stroke_path(r.ctx.dev, &r.path, &r.gs.stroke_state, r.gs.ctm, + content_paint_of(r, src, r.gs.stroke_alpha)) +} + +@(private) +content_apply_clip :: proc(r: ^Content_Runner) { + if r.pending_clip == .None { + return + } + even_odd := r.pending_clip == .Even_Odd + r.pending_clip = .None + dev_clip_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm) + r.clip_depth += 1 + r.clip_box = rect_intersect(r.clip_box, path_tight_bounds(&r.path, r.gs.ctm)) +} + +// content_shape_area is the device-space region the current path can mark, +// intersected with what is left of the clip. It is what bounds a tiling +// pattern: a lattice only has to cover the shape being filled. +@(private) +content_shape_area :: proc(r: ^Content_Runner, stroking: bool) -> Rect { + area := path_tight_bounds(&r.path, r.gs.ctm) + if stroking { + // Half the pen sticks out either side, and a miter further still. + w := r.gs.stroke_state.line_width * matrix_max_expansion(r.gs.ctm) + area = rect_expand(area, max(w * r.gs.stroke_state.miter_limit, 1)) + } + if !rect_is_finite(area) { + return RECT_EMPTY + } + return rect_intersect(r.clip_box, area) +} + +// --------------------------------------------------------------------------- +// Patterns (8.7.3, 8.7.4) +// +// The interpreter resolves patterns itself rather than handing them to the +// device: a shading pattern becomes a clipped `fill_shade`, and a tiling +// pattern becomes the cell's content stream replayed once per placement. That +// is why `Paint.pattern` is cleared on the way out -- by the time a device +// sees the operation there is nothing left for it to resolve. +// --------------------------------------------------------------------------- + +@(private) +content_paint_pattern :: proc(r: ^Content_Runner, src: ^Paint_Source, even_odd, stroking: bool) { + pat := src.pattern + if pat == nil { + return + } + + // Clip to the shape, so the pattern marks only inside it. + if stroking { + dev_clip_stroke_path(r.ctx.dev, &r.path, &r.gs.stroke_state, r.gs.ctm) + } else { + dev_clip_path(r.ctx.dev, &r.path, even_odd, r.gs.ctm) + } + r.clip_depth += 1 + defer { + dev_pop_clip(r.ctx.dev) + r.clip_depth -= 1 + } + + content_pattern_over(r, src, content_shape_area(r, stroking), + stroking ? r.gs.stroke_alpha : r.gs.fill_alpha) +} + +// content_pattern_over paints the selected pattern over `area`, through +// whatever clip the caller has already installed: the shape for a fill, the +// stencil for an image mask. +@(private) +content_pattern_over :: proc(r: ^Content_Runner, src: ^Paint_Source, area: Rect, alpha: f64) { + pat := src.pattern + if pat == nil { + return + } + switch pat.kind { + case .Shading: + if !dev_wants_shades(r.ctx.dev) { + return + } + // Pattern space is anchored to the stream's default space, never to + // the CTM of the operation being painted (8.7.3.1). + m := pattern_matrix(pat, r.base_ctm) + if !matrix_is_finite(m) || pat.shading == nil { + return + } + paint := content_paint_of(r, src, alpha) + paint.pattern = nil + // 8.7.4.3: a shading pattern may carry an /ExtGState whose parameters + // are put into effect while it paints. It paints no path and shows no + // text, so the compositing parameters are the whole of what can + // matter, and the format says they replace rather than compound. + if pat.ext_gstate != nil { + doc := r.ctx.doc + if v, ok := to_number(dict_get(doc, pat.ext_gstate, "ca")); ok && is_finite(v) { + paint.alpha = clamp(v, 0, 1) + } + if nm, ok := to_name(dict_get(doc, pat.ext_gstate, "BM")); ok { + paint.blend = blend_mode_from_name(nm) + } + } + dev_fill_shade(r.ctx.dev, pat.shading, m, paint) + + case .Tiling: + content_run_tiles(r, pat, src, area, alpha) + } +} + +@(private) +content_run_tiles :: proc(r: ^Content_Runner, pat: ^Pattern, src: ^Paint_Source, + area: Rect, alpha: f64) { + ctx := r.ctx + if pat.content == nil { + return + } + key := uintptr(rawptr(pat.content)) + if key in ctx.visited { + // A cell that fills with its own pattern. + add_repair(ctx.doc, .Content_Syntax, 0) + return + } + data, derr := stream_data(ctx.doc, pat.content) + if derr != .None || len(data) == 0 { + return + } + m := pattern_matrix(pat, r.base_ctm) + if !matrix_is_finite(m) { + return + } + + // The cell starts from a clean graphics state at the cell's own CTM: it + // inherits nothing from the fill but the compositing parameters, which is + // what makes a patterned fill at 50% alpha 50% transparent. + base := gstate_default(m) + base.fill_alpha = clamp(alpha, 0, 1) + base.stroke_alpha = base.fill_alpha + base.blend = r.gs.blend + base.soft_mask = r.gs.soft_mask + base.fill.cs = colorspace_device_gray(ctx.doc) + base.fill.color = colorspace_default_color(base.fill.cs) + base.stroke.cs = base.fill.cs + base.stroke.color = base.fill.color + + flags := r.flags + if pattern_needs_color(pat) { + // /PaintType 2: the cell is a stencil and every colour operator in it + // is ignored in favour of the colour that came with the pattern name. + under := src.cs != nil ? src.cs.under : nil + if under == nil { + under = colorspace_device_gray(ctx.doc) + } + base.fill.cs = under + base.fill.color = src.color + base.stroke.cs = under + base.stroke.color = src.color + flags += {.Ignore_Color} + } + + cells := dev_begin_tile(ctx.dev, area, pat.bbox, pat.xstep, pat.ystep, m) + defer dev_end_tile(ctx.dev) + + if cells <= 0 { + // The device places the copies itself and wants the cell drawn once. + content_run_cell(r, pat, data, base, m, flags) + return + } + + tiles := pattern_tiles(pat, area, r.base_ctm, ctx.alloc) + defer delete(tiles, ctx.alloc) + n := min(len(tiles), cells, MAX_TILE_REPLAYS) + for i in 0 ..< n { + content_run_cell(r, pat, data, base, tiles[i], flags) + } +} + +@(private) +content_run_cell :: proc(r: ^Content_Runner, pat: ^Pattern, data: []byte, + base: Graphics_State, ctm: Matrix, flags: Content_Flags) { + ctx := r.ctx + if !matrix_is_finite(ctm) { + return + } + gs := base + gs.ctm = ctm + + box := r.clip_box + clipped := false + if !rect_is_empty(pat.bbox) { + path_clear(&r.rect) + path_rect(&r.rect, pat.bbox.x0, pat.bbox.y0, rect_width(pat.bbox), rect_height(pat.bbox)) + dev_clip_path(ctx.dev, &r.rect, false, ctm) + r.clip_depth += 1 + clipped = true + box = rect_intersect(box, path_tight_bounds(&r.rect, ctm)) + } + + res := pat.resources + if res == nil { + res = r.res + } + key := uintptr(rawptr(pat.content)) + ctx.visited[key] = true + content_exec(ctx, data, res, gs, ctm, box, flags) + delete_key(&ctx.visited, key) + + if clipped { + dev_pop_clip(ctx.dev) + r.clip_depth -= 1 + } +} + +// --------------------------------------------------------------------------- +// Text (9.4) +// +// THE TEXT MATRIX +// +// Tm is the only operator that sets it outright. Td, TD, T*, ' and " all +// translate the LINE matrix and copy it across; every glyph shown translates +// the text matrix alone, so the next line starts from where the last one +// began rather than from where it ended. +// +// One glyph's rendering matrix is +// +// [ Tfs*Th 0 0 ] +// [ 0 Tfs 0 ] x Tm x CTM +// [ 0 Ts 1 ] +// +// with the font matrix in front of it to get out of glyph space, and the pen +// then advances by (w0*Tfs + Tc + Tw) * Th. The horizontal scale multiplies +// the advance and the glyph but never the rise; the rise moves the glyph and +// never the pen. +// --------------------------------------------------------------------------- + +@(private) +content_text_begin :: proc(r: ^Content_Runner) { + r.in_text = true + r.tm = MATRIX_IDENTITY + r.tlm = MATRIX_IDENTITY + clear(&r.tclip.items) + r.tclip.font = nil + r.tclip_on = false +} + +@(private) +content_text_end :: proc(r: ^Content_Runner) { + r.in_text = false + if r.tclip_on { + // 9.3.6: the clip is the union of every glyph shown in a clipping + // mode anywhere between BT and ET, and it takes effect here. + dev_clip_text(r.ctx.dev, &r.tclip, r.gs.ctm) + r.clip_depth += 1 + } + r.tclip_on = false + clear(&r.tclip.items) + r.tclip.font = nil +} + +// content_text_move is Td: it translates the line matrix and restarts the +// text matrix from it. +@(private) +content_text_move :: proc(r: ^Content_Runner, tx, ty: f64) { + m := matrix_mul(translate_matrix(tx, ty), r.tlm) + if !matrix_is_finite(m) { + add_repair(r.ctx.doc, .Content_Syntax, 0) + return + } + r.tlm = m + r.tm = m +} + +@(private) +content_set_font :: proc(r: ^Content_Runner, name: string) { + fonts := dict_dict(r.ctx.doc, r.res, "Font") + fd := dict_dict(r.ctx.doc, fonts, name) + if fd == nil { + // No font: the size is still set, so a later Tf that does resolve + // lands in the right state. Showing text meanwhile draws nothing. + add_repair(r.ctx.doc, .Bad_Resource, 0) + r.gs.text.font = nil + return + } + f, err := font_load(r.ctx.doc, fd) + if err != .None || f == nil { + add_repair(r.ctx.doc, .Bad_Font_Object, 0) + r.gs.text.font = nil + return + } + r.gs.text.font = f +} + +// content_show_array is TJ: strings are shown, numbers move the pen. +@(private) +content_show_array :: proc(r: ^Content_Runner, obj: Object) { + a, ok := to_array(obj) + if !ok { + return + } + for i in 0 ..< array_len(a) { + if r.ctx.fuel <= 0 { + return + } + r.ctx.fuel -= 1 + #partial switch v in array_get_raw(a, i) { + case String: + content_show(r, transmute([]byte)v) + case i64: + content_tj_adjust(r, f64(v)) + case f64: + content_tj_adjust(r, v) + } + } +} + +// content_tj_adjust applies one number from a TJ array. It is a displacement +// in thousandths of a text-space unit and it is SUBTRACTED from the current +// position, so a positive number pulls the following glyphs closer -- which is +// how every kerning pair in every PDF is written. +@(private) +content_tj_adjust :: proc(r: ^Content_Runner, amount: f64) { + if !is_finite(amount) { + return + } + ts := &r.gs.text + d := -amount / 1000 * ts.size + m: Matrix + if ts.font != nil && ts.font.vertical { + m = matrix_mul(translate_matrix(0, d), r.tm) + } else { + m = matrix_mul(translate_matrix(d * ts.horiz_scale, 0), r.tm) + } + if matrix_is_finite(m) { + r.tm = m + } +} + +// content_show is Tj: decode the string, place every glyph, advance the pen, +// and hand the run to the device. +@(private) +content_show :: proc(r: ^Content_Runner, s: []byte) { + ctx := r.ctx + f := r.gs.text.font + if f == nil || len(s) == 0 { + return + } + + font_decode(f, s, &r.codes) + if len(r.codes) == 0 { + return + } + // Glyphs cost budget too, or a stream of a few operators showing megabytes + // of text would run unbounded. + if ctx.fuel < len(r.codes) { + ctx.fuel = 0 + } else { + ctx.fuel -= len(r.codes) + } + + ts := &r.gs.text + mode := ts.render_mode + vertical := f.vertical + fm := font_matrix(f) + + clear(&r.text.items) + r.text.font = f + r.text.wmode = vertical ? 1 : 0 + r.text.render_mode = mode + + for fc in r.codes { + // Text space to device space, for this glyph, at this pen position. + tsm := Matrix{ts.size * ts.horiz_scale, 0, 0, ts.size, 0, ts.rise} + trm := matrix_mul(tsm, matrix_mul(r.tm, r.gs.ctm)) + + gm := fm + if vertical && (fc.vx != 0 || fc.vy != 0) { + // 9.7.4.3: the position vector moves the glyph off the vertical + // baseline. It is in thousandths of a text-space unit whatever + // units the font program itself uses, so it is applied after the + // font matrix rather than inside glyph space. + gm = matrix_mul(fm, translate_matrix(-fc.vx * 0.001, -fc.vy * 0.001)) + } + + // 9.3.3: word spacing applies to a single-byte code 32 and to nothing + // else. font_decode has already made that judgement. + ws := fc.is_space ? ts.word_spacing : 0 + adv: f64 + if vertical { + // fc.width is w1y, the vertical displacement, already in + // thousandths and normally negative. + adv = fc.width * 0.001 * ts.size + ts.char_spacing + ws + } else { + w0 := fc.width * 0.001 + if f.kind == .Type3 { + // A Type 3 width is in the font's own glyph space, and + // /FontMatrix -- not a fixed 1/1000 -- is what maps it. + w0 = transform_vector(fm, Point{fc.width, 0}).x + } + adv = w0 * ts.size + ts.char_spacing + ws + } + if !is_finite(adv) { + adv = 0 + } + + item := Text_Item{ + gid = fc.gid, + code = fc.code, + cid = fc.cid, + trm = matrix_mul(gm, trm), + advance = adv, + unicode = fc.unicode, + is_space = fc.is_space, + } + if matrix_is_finite(item.trm) { + append(&r.text.items, item) + } + + next: Matrix + if vertical { + next = matrix_mul(translate_matrix(0, adv), r.tm) + } else { + next = matrix_mul(translate_matrix(adv * ts.horiz_scale, 0), r.tm) + } + if matrix_is_finite(next) { + r.tm = next + } + } + + if r.n_hidden == 0 { + content_emit_text(r, mode) + } +} + +@(private) +content_emit_text :: proc(r: ^Content_Runner, mode: Text_Render_Mode) { + ctx := r.ctx + if font_is_type3(r.text.font) { + // A Type 3 glyph is drawn by running its content stream, so the + // device's text hooks cannot draw it. It still sees the run, which is + // what keeps extraction working over a Type 3 font. + dev_ignore_text(ctx.dev, &r.text, r.gs.ctm) + wants_outlines := ctx.dev == nil || .No_Glyph_Outlines not_in ctx.dev.hints + if mode != .Invisible && mode != .Clip && wants_outlines { + content_run_type3(r) + } + } else if mode == .Invisible || mode == .Clip { + // Modes 3 and 7 paint nothing at all, and are still handed over: an + // OCR layer under a scan is written in mode 3 and is the whole of + // what such a page has to extract. + dev_ignore_text(ctx.dev, &r.text, r.gs.ctm) + } else { + if text_render_mode_fills(mode) { + dev_fill_text(ctx.dev, &r.text, r.gs.ctm, + content_paint_of(r, &r.gs.fill, r.gs.fill_alpha)) + } + if text_render_mode_strokes(mode) { + dev_stroke_text(ctx.dev, &r.text, &r.gs.stroke_state, r.gs.ctm, + content_paint_of(r, &r.gs.stroke, r.gs.stroke_alpha)) + } + } + if text_render_mode_clips(mode) { + content_accumulate_clip(r) + } +} + +@(private) +content_accumulate_clip :: proc(r: ^Content_Runner) { + if r.tclip.font == nil { + r.tclip.font = r.text.font + r.tclip.wmode = r.text.wmode + r.tclip.render_mode = r.text.render_mode + } else if r.tclip.font != r.text.font { + // A Text carries one font, so the vtable cannot express the union of + // glyph runs from two of them. Keeping the first run shows more than + // was asked for; taking the intersection instead would usually show + // nothing at all, which is the worse of the two failures. + add_repair(r.ctx.doc, .Content_Syntax, 0) + r.tclip_on = true + return + } + r.tclip_on = true + for it in r.text.items { + if len(r.tclip.items) >= MAX_TEXT_CLIP_ITEMS { + break + } + append(&r.tclip.items, it) + } +} + +// --------------------------------------------------------------------------- +// Type 3 glyphs (9.6.4) +// --------------------------------------------------------------------------- + +@(private) +content_run_type3 :: proc(r: ^Content_Runner) { + f := r.text.font + for it in r.text.items { + if r.ctx.fuel <= 0 { + return + } + content_run_type3_glyph(r, f, it.code, it.trm) + } +} + +@(private) +content_run_type3_glyph :: proc(r: ^Content_Runner, f: ^Font, code: u32, trm: Matrix) { + ctx := r.ctx + if !matrix_is_finite(trm) { + return + } + stm := font_type3_proc(f, code) + if stm == nil { + return + } + key := uintptr(rawptr(stm)) + if key in ctx.visited { + // A glyph description that shows its own character. + add_repair(ctx.doc, .Content_Syntax, 0) + return + } + data, err := stream_data(ctx.doc, stm) + if err != .None || len(data) == 0 { + return + } + + gs := r.gs + gs.ctm = trm + // The glyph description starts with no text state of its own. Leaving the + // font in place would let a glyph that shows text re-enter its own font + // through a path the visited set cannot see. + gs.text = TEXT_STATE_DEFAULT + + res := f.t3_resources + if res == nil { + res = r.res + } + + ctx.visited[key] = true + // The glyph's own space is what a pattern named in it is anchored to. + content_exec(ctx, data, res, gs, trm, r.clip_box, r.flags + {.Type3_Glyph}) + delete_key(&ctx.visited, key) +} + +@(private) +content_type3_d1 :: proc(r: ^Content_Runner) { + if .Type3_Glyph not_in r.flags { + return + } + // d1 declares the glyph a shape: it is painted entirely in the colour + // current when the glyph was shown, and every colour operator inside the + // description is ignored (9.6.4). + r.flags += {.Ignore_Color} + + bbox := rect_normalize(Rect{ + content_num(r, 6, 2), content_num(r, 6, 3), + content_num(r, 6, 4), content_num(r, 6, 5), + }) + // Four zeros mean "no bounding box declared", not "an empty glyph". + if rect_is_empty(bbox) { + return + } + path_clear(&r.rect) + path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox)) + dev_clip_path(r.ctx.dev, &r.rect, false, r.gs.ctm) + r.clip_depth += 1 + r.clip_box = rect_intersect(r.clip_box, path_tight_bounds(&r.rect, r.gs.ctm)) +} + +// --------------------------------------------------------------------------- +// XObjects (8.10) +// --------------------------------------------------------------------------- + +@(private) +content_do :: proc(r: ^Content_Runner, name: string) { + ctx := r.ctx + doc := ctx.doc + if r.n_hidden > 0 { + return + } + xd := dict_dict(doc, r.res, "XObject") + stm, ok := to_stream(dict_get(doc, xd, name)) + if !ok { + add_repair(doc, .Bad_Resource, 0) + return + } + // An XObject may declare its own optional-content membership (8.10.1). + if !content_oc_visible(ctx, dict_get_raw(stm.dict, "OC")) { + return + } + + switch dict_name(doc, stm.dict, "Subtype") { + case "Image": + content_do_image(r, stm) + case "Form": + content_do_form(r, stm) + case "PS": + // A PostScript XObject is never drawn by a PDF reader (8.10.4). + case: + // /Subtype is required and is nonetheless sometimes missing. The + // other keys say unambiguously which kind it is. + if dict_has(stm.dict, "BBox") { + content_do_form(r, stm) + } else if dict_has(stm.dict, "Width") || dict_has(stm.dict, "W") { + content_do_image(r, stm) + } else { + add_repair(doc, .Bad_Resource, 0) + } + } +} + +@(private) +content_do_form :: proc(r: ^Content_Runner, stm: ^Stream) { + ctx := r.ctx + doc := ctx.doc + key := uintptr(rawptr(stm)) + if key in ctx.visited { + // A form that invokes itself, directly or through another form. + add_repair(doc, .Content_Syntax, 0) + return + } + data, derr := stream_data(doc, stm) + if derr != .None || len(data) == 0 { + return + } + + d := stm.dict + ctm := matrix_mul(dict_matrix(doc, d, "Matrix"), r.gs.ctm) + if !matrix_is_finite(ctm) { + add_repair(doc, .Content_Syntax, 0) + return + } + + gs := r.gs + gs.ctm = ctm + + res := dict_dict(doc, d, "Resources") + if res == nil { + // A form with no /Resources inherits the invoking stream's, which is + // not what the format says and is what producers rely on. + res = r.res + } + + box := r.clip_box + clips := 0 + bbox, has_bbox := object_to_rect(doc, dict_get(doc, d, "BBox")) + // A degenerate /BBox is still a clip (8.10.2), and clipping to it is what + // makes the form draw nothing. Skipping the push instead let the form paint + // its whole content over the page -- the opposite of what the box asks for. + // `object_to_rect` already normalises reversed corners, so the only boxes + // that reach here empty are genuinely zero-area ones. + if has_bbox { + path_clear(&r.rect) + path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox)) + dev_clip_path(ctx.dev, &r.rect, false, ctm) + r.clip_depth += 1 + clips += 1 + box = rect_intersect(box, path_tight_bounds(&r.rect, ctm)) + } + + grp := dict_dict(doc, d, "Group") + group := grp != nil && dict_name(doc, grp, "S") == "Transparency" + if group { + area := has_bbox ? transform_rect(ctm, bbox) : box + // All three compositing parameters go to the device, the soft mask + // included: it is the group's RESULT they apply to, and `begin_group` + // is the only channel left once they are cleared below. + dev_begin_group(ctx.dev, area, dict_bool(doc, grp, "I"), dict_bool(doc, grp, "K"), + gs.blend, gs.fill_alpha, gs.soft_mask) + // 11.6.6: the contents composite into the group with the parameters + // reset. It is the group's RESULT that the outer alpha, blend mode and + // soft mask apply to, and applying them twice is the classic way to + // render a transparency group too faint. + gs.fill_alpha = 1 + gs.stroke_alpha = 1 + gs.blend = .Normal + gs.soft_mask = nil + } + + ctx.visited[key] = true + // A form re-anchors pattern space to its own default space (8.7.3.1). + content_exec(ctx, data, res, gs, ctm, box, r.flags - {.Type3_Glyph}) + delete_key(&ctx.visited, key) + + if group { + dev_end_group(ctx.dev) + } + for _ in 0 ..< clips { + dev_pop_clip(ctx.dev) + r.clip_depth -= 1 + } +} + +@(private) +content_do_image :: proc(r: ^Content_Runner, stm: ^Stream) { + ctx := r.ctx + if !dev_wants_images(ctx.dev) { + return + } + img, err := image_load(ctx.doc, stm, r.res) + if err != .None || img == nil { + add_repair(ctx.doc, .Bad_Resource, 0) + return + } + content_draw_image(r, img) +} + +// content_draw_image paints one image over the unit square of the CTM, which +// is where the format puts every image (8.9.5.2): the `cm` before the `Do` is +// what gives it a size and a place. +@(private) +content_draw_image :: proc(r: ^Content_Runner, img: ^Image) { + src := &r.gs.fill + if !img.is_mask { + dev_fill_image(r.ctx.dev, img, r.gs.ctm, content_paint_of(r, src, r.gs.fill_alpha)) + return + } + // An /ImageMask is a stencil painted in the current fill colour, so a + // pattern fill means clipping to the stencil and painting the pattern + // through it -- which is what clip_image_mask is for. + if src.pattern != nil { + dev_clip_image_mask(r.ctx.dev, img, r.gs.ctm) + r.clip_depth += 1 + unit := Rect{0, 0, 1, 1} + area := rect_intersect(r.clip_box, transform_rect(r.gs.ctm, unit)) + content_pattern_over(r, src, area, r.gs.fill_alpha) + dev_pop_clip(r.ctx.dev) + r.clip_depth -= 1 + return + } + if src.cs != nil && src.cs.kind == .Pattern { + return + } + paint := content_paint_of(r, src, r.gs.fill_alpha) + paint.is_mask = true + dev_fill_image_mask(r.ctx.dev, img, r.gs.ctm, paint) +} + +// --------------------------------------------------------------------------- +// Shadings (8.7.4.2) +// --------------------------------------------------------------------------- + +@(private) +content_shading :: proc(r: ^Content_Runner, name: string) { + ctx := r.ctx + if r.n_hidden > 0 || !dev_wants_shades(ctx.dev) { + return + } + all := dict_dict(ctx.doc, r.res, "Shading") + entry := dict_get_raw(all, name) + if entry == nil { + add_repair(ctx.doc, .Bad_Resource, 0) + return + } + sh, err := shading_load(ctx.doc, entry, r.res) + if err != .None || sh == nil { + add_repair(ctx.doc, .Bad_Resource, 0) + return + } + // `sh` fills the current clip with the shading's own extent. Unlike a + // shading pattern it has no /Matrix and no /Background of its own, and it + // takes nothing from the current colour. + paint := content_paint_of(r, &r.gs.fill, r.gs.fill_alpha) + paint.pattern = nil + dev_fill_shade(ctx.dev, sh, r.gs.ctm, paint) +} + +// --------------------------------------------------------------------------- +// ExtGState (8.4.5, Table 57) +// --------------------------------------------------------------------------- + +@(private) +content_ext_gstate :: proc(r: ^Content_Runner, name: string) { + ctx := r.ctx + doc := ctx.doc + all := dict_dict(doc, r.res, "ExtGState") + d := dict_dict(doc, all, name) + if d == nil { + add_repair(doc, .Bad_Resource, 0) + return + } + + if v, ok := to_number(dict_get(doc, d, "LW")); ok && is_finite(v) { + r.gs.stroke_state.line_width = max(v, 0) + } + if v, ok := to_int(dict_get(doc, d, "LC")); ok { + r.gs.stroke_state.line_cap = Line_Cap(u8(clamp(v, 0, 2))) + } + if v, ok := to_int(dict_get(doc, d, "LJ")); ok { + r.gs.stroke_state.line_join = Line_Join(u8(clamp(v, 0, 2))) + } + if v, ok := to_number(dict_get(doc, d, "ML")); ok && is_finite(v) { + r.gs.stroke_state.miter_limit = max(v, 1) + } + if a, ok := to_array(dict_get(doc, d, "D")); ok && array_len(a) >= 2 { + content_set_dash(r, array_get(doc, a, 0), array_num(doc, a, 1, 0)) + } + if v, ok := to_name(dict_get(doc, d, "RI")); ok { + r.gs.render_intent = v + } + if v, ok := to_number(dict_get(doc, d, "CA")); ok && is_finite(v) { + r.gs.stroke_alpha = clamp(v, 0, 1) + } + if v, ok := to_number(dict_get(doc, d, "ca")); ok && is_finite(v) { + r.gs.fill_alpha = clamp(v, 0, 1) + } + if v, ok := to_number(dict_get(doc, d, "FL")); ok && is_finite(v) { + r.gs.flatness = clamp(v, 0, 100) + } + if v, ok := to_number(dict_get(doc, d, "SM")); ok && is_finite(v) { + r.gs.smoothness = clamp(v, 0, 1) + } + if v, ok := to_bool(dict_get(doc, d, "SA")); ok { + r.gs.stroke_adjust = v + } + if v, ok := to_bool(dict_get(doc, d, "AIS")); ok { + r.gs.alpha_is_shape = v + } + if v, ok := to_bool(dict_get(doc, d, "TK")); ok { + r.gs.text.knockout = v + } + if v, ok := to_bool(dict_get(doc, d, "UCR")); ok { + _ = v // undercolour removal is a printing control, not a rendering one + } + // /OP sets both overprints unless /op overrides the fill one (Table 57). + if v, ok := to_bool(dict_get(doc, d, "OP")); ok { + r.gs.overprint_stroke = v + r.gs.overprint_fill = v + } + if v, ok := to_bool(dict_get(doc, d, "op")); ok { + r.gs.overprint_fill = v + } + if v, ok := to_int(dict_get(doc, d, "OPM")); ok { + r.gs.overprint_mode = int(clamp(v, 0, 1)) + } + + // 11.3.5.2: /BM may be an array of names, oldest first; the first one a + // reader recognises wins, and an unrecognised name is Normal. + #partial switch v in dict_get(doc, d, "BM") { + case Name: + r.gs.blend = blend_mode_from_name(string(v)) + case ^Array: + for i in 0 ..< min(array_len(v), 8) { + if nm := array_name(doc, v, i); nm != "" { + r.gs.blend = blend_mode_from_name(nm) + break + } + } + } + + if a, ok := to_array(dict_get(doc, d, "Font")); ok && array_len(a) >= 2 { + if fd, has := to_dict(array_get(doc, a, 0)); has { + if f, ferr := font_load(doc, fd); ferr == .None { + r.gs.text.font = f + } + } + if sz, sok := to_number(array_get(doc, a, 1)); sok && is_finite(sz) { + r.gs.text.size = sz + } + } + + if sm := dict_get(doc, d, "SMask"); sm != nil { + if nm, is_name := to_name(sm); is_name { + if nm == "None" { + r.gs.soft_mask = nil + } + } else if smd, is_dict := to_dict(sm); is_dict { + content_install_soft_mask(r, smd) + } + } +} + +// --------------------------------------------------------------------------- +// Soft masks (11.6.5) +// --------------------------------------------------------------------------- + +@(private) +content_install_soft_mask :: proc(r: ^Content_Runner, smd: ^Dict) { + ctx := r.ctx + doc := ctx.doc + sh := ctx.shared + g := dict_stream(doc, smd, "G") + if g == nil { + add_repair(doc, .Bad_Resource, 0) + r.gs.soft_mask = nil + return + } + // The same ExtGState applied again under the same CTM is the same mask. + // Rebuilding it would cost an allocation, a slot of the page's budget and + // -- because the device would not recognise it -- a whole re-render. + if sh.last_mask != nil && sh.last_smd == smd && sh.last_ctm == r.gs.ctm { + r.gs.soft_mask = sh.last_mask + return + } + if sh.n_soft_masks >= MAX_SOFT_MASKS { + // A Soft_Mask outlives the operator that installed it -- the graphics + // state stack can bring it back after a `Q` -- so a stream that + // installs one per operator has to stop somewhere. + add_repair(doc, .Content_Syntax, 0) + return + } + sh.n_soft_masks += 1 + + // Interpreter scratch, not the Document arena: a Soft_Mask has to outlive + // the OPERATOR, not the page. `device_list` copies it by value for exactly + // that reason, and putting it on the Document made every render of every + // page add to a pool that only `close` reclaims. + m := new(Soft_Mask, ctx.alloc) + m.form = g + m.luminosity = dict_name(doc, smd, "S") == "Luminosity" + // The mask is positioned by the CTM in force when it was installed, not + // by the one in force when something is painted through it. + m.ctm = r.gs.ctm + + if tr := dict_get_raw(smd, "TR"); tr != nil && !name_is(resolve(doc, tr), "Identity") { + if fn, ferr := function_load(doc, tr); ferr == .None { + m.transfer = fn + } + } + + // The backdrop colour is in the group's own colour space (11.6.5.2), and + // matters only for a luminosity mask; an alpha mask has no backdrop. + grp := dict_dict(doc, g.dict, "Group") + gcs, _ := colorspace_load(doc, dict_get_raw(grp, "CS"), r.res) + if bc := dict_array(doc, smd, "BC"); bc != nil && gcs != nil { + n := min(array_len(bc), gcs.n, MAX_COLOR_COMPONENTS) + m.backdrop.n = n + for i in 0 ..< n { + m.backdrop.v[i] = array_num(doc, bc, i, 0) + } + } + + r.gs.soft_mask = m + sh.last_smd = smd + sh.last_ctm = r.gs.ctm + sh.last_mask = m + + // A device that already holds this mask's plane does not need it drawn + // again. Everything the render depends on is in the Soft_Mask itself, so + // "equal to one I have" is the whole test the device has to make. + if dev_has_soft_mask(ctx.dev, m) { + return + } + content_render_soft_mask(r, m, gcs) +} + +// content_render_soft_mask draws the mask's group between begin_mask and +// end_mask, where what is painted becomes the mask rather than page content. +@(private) +content_render_soft_mask :: proc(r: ^Content_Runner, m: ^Soft_Mask, gcs: ^Colorspace) { + ctx := r.ctx + doc := ctx.doc + stm := m.form + key := uintptr(rawptr(stm)) + if key in ctx.visited { + add_repair(doc, .Content_Syntax, 0) + return + } + data, derr := stream_data(doc, stm) + if derr != .None || len(data) == 0 { + return + } + + d := stm.dict + ctm := matrix_mul(dict_matrix(doc, d, "Matrix"), m.ctm) + if !matrix_is_finite(ctm) { + return + } + bbox, has_bbox := object_to_rect(doc, dict_get(doc, d, "BBox")) + area := has_bbox ? transform_rect(ctm, bbox) : r.clip_box + + backdrop: Paint + backdrop.cs = gcs + backdrop.color = m.backdrop + backdrop.alpha = 1 + if gcs != nil && m.backdrop.n > 0 { + backdrop.rgb = colorspace_to_rgb(gcs, m.backdrop) + } + + dev_begin_mask(ctx.dev, area, m.luminosity, backdrop) + + // The group is rendered from a clean state: nothing outside it, including + // the alpha and blend mode that will be applied to whatever the mask ends + // up modulating, may affect the mask itself. + gs := gstate_default(ctm) + gs.fill.cs = colorspace_device_gray(doc) + gs.fill.color = colorspace_default_color(gs.fill.cs) + gs.stroke.cs = gs.fill.cs + gs.stroke.color = gs.fill.color + + box := area + clips := 0 + if has_bbox { + path_clear(&r.rect) + path_rect(&r.rect, bbox.x0, bbox.y0, rect_width(bbox), rect_height(bbox)) + dev_clip_path(ctx.dev, &r.rect, false, ctm) + r.clip_depth += 1 + clips = 1 + box = rect_intersect(box, path_tight_bounds(&r.rect, ctm)) + } + + res := dict_dict(doc, d, "Resources") + if res == nil { + res = r.res + } + + ctx.visited[key] = true + content_exec(ctx, data, res, gs, ctm, box, {}) + delete_key(&ctx.visited, key) + + for _ in 0 ..< clips { + dev_pop_clip(ctx.dev) + r.clip_depth -= 1 + } + dev_end_mask(ctx.dev, m, m.transfer) +} + +// --------------------------------------------------------------------------- +// Inline images (8.9.7) +// +// BI collects a dictionary with abbreviated keys, ID is followed by exactly +// one whitespace byte and then by binary data, and EI ends it. The data is not +// lexed and cannot be: it is arbitrary bytes, and "EI" occurs inside real +// image data often enough to matter. Getting the end wrong does not lose one +// image, it desynchronises every operator on the rest of the page, so the +// length is computed whenever it can be and scanned for only as a last resort. +// --------------------------------------------------------------------------- + +@(private) +content_inline_image :: proc(r: ^Content_Runner, lex: ^Lexer) { + ctx := r.ctx + doc := ctx.doc + sh := ctx.shared + + // Every BI is counted, whether or not anything is drawn from it. + over := sh.n_inline >= MAX_INLINE_IMAGES + if over { + if sh.n_inline == MAX_INLINE_IMAGES { + // Counted one past the cap so that a stream which is nothing but + // inline images records this once rather than filling the repair + // list with the same entry. + sh.n_inline += 1 + add_repair(doc, .Content_Syntax, i64(lex.pos)) + } + } else { + sh.n_inline += 1 + } + + // The dictionary is parsed into the page's one reusable map, which is + // scratch and is cleared for the next BI; only an image that is actually + // built gets a dictionary of its own, at the end. Every bail-out below -- + // no ID, no data, oversize data, a device that does not want images -- + // used to leave one on the Document instead, reclaimable only by `close`, + // and `BI ID EI` is two tokens. + // + // The VALUES go to the Document while an image may still come of them, + // because the Image outlives this run: a display list hands one to a + // caller long after the page's scratch is gone. Past the cap they go to + // scratch too, since nothing will be built. + d := content_inline_dict(ctx) + alloc := over ? ctx.alloc : doc.allocator + got_id := false + + collect: for _ in 0 ..< MAX_INLINE_KEYS { + if ctx.fuel <= 0 { + return + } + ctx.fuel -= 1 + key_tok := lexer_next(lex, alloc) + #partial switch key_tok.kind { + case .EOF: + return + case .Keyword: + switch key_tok.text { + case "ID": + got_id = true + break collect + case "EI", "BI": + // The dictionary never closed. Nothing can be drawn, and the + // stream resynchronises here rather than eating the page. + add_repair(doc, .Content_Syntax, i64(key_tok.start)) + return + } + continue collect + case .Name: + case: + // Junk between entries. The token was consumed, so this advances. + continue collect + } + + val_tok := lexer_next(lex, alloc) + #partial switch val_tok.kind { + case .Integer: dict_set(d, key_tok.text, val_tok.int_value) + case .Real: dict_set(d, key_tok.text, val_tok.real_value) + case .Name: dict_set(d, key_tok.text, Name(val_tok.text)) + case .String: dict_set(d, key_tok.text, String(val_tok.bytes)) + case .Keyword: + switch val_tok.text { + case "true": dict_set(d, key_tok.text, true) + case "false": dict_set(d, key_tok.text, false) + case "null": + case "ID": + // A key with no value, immediately before the data. + got_id = true + break collect + } + case .Array_Open, .Dict_Open: + // Composite values -- /Filter, /Decode, /DecodeParms -- go through + // the object parser. Only these do: the parser reads ahead to tell + // "1 0 R" from "1 0", and its lookahead does not survive being + // spliced back into this lexer, whereas a bracketed object always + // ends where the parser leaves off. + p := parser_make(doc, lex.data, alloc) + p.lex.pos = val_tok.start + p.depth = 1 + val, perr := parse_object(&p) + lex.pos = max(p.lex.pos, val_tok.end) + if perr == .None && val != nil { + dict_set(d, key_tok.text, val) + } + case .EOF: + return + } + } + + if !got_id { + add_repair(doc, .Content_Syntax, i64(lex.pos)) + return + } + // A refused image still needs its exact length, or the operators after it + // are read out of its pixels (8.9.7) -- which is why the dictionary is + // read at all past the cap. The one entry that cannot be read from scratch + // is a colour space written as an inline ARRAY: `colorspace_load` caches + // by the address of the object it is handed, and an address inside a + // per-page arena is handed straight back out to the next page. A NAME has + // no address to key on and is what an inline image writes; anything else + // is dropped here, which costs this refused image the exact byte count and + // nothing else. + if over { + for key in ([2]string{"ColorSpace", "CS"}) { + if v := dict_get_raw(d, key); v != nil { + if _, is_name := v.(Name); !is_name { + delete_key(d, key) + } + } + } + } + + // Exactly one whitespace byte separates ID from the data (8.9.7). + if lex.pos < len(lex.data) && is_pdf_whitespace(lex.data[lex.pos]) { + lex.pos += 1 + } + start := lex.pos + end, after := content_inline_end(doc, d, r.res, lex.data, start) + lex.pos = clamp(after, start, len(lex.data)) + + if end <= start { + return + } + if end - start > MAX_INLINE_IMAGE_BYTES { + add_repair(doc, .Content_Syntax, i64(start)) + return + } + if over || r.n_hidden > 0 || !dev_wants_images(ctx.dev) { + return + } + img, err := image_load_inline(doc, content_inline_own(doc, d), lex.data[start:end], r.res) + if err != .None || img == nil { + add_repair(doc, .Bad_Resource, i64(start)) + return + } + content_draw_image(r, img) +} + +// content_inline_dict is the map the next BI parses into. +@(private) +content_inline_dict :: proc(ctx: ^Content_Ctx) -> ^Dict { + sh := ctx.shared + if sh.inline_dict == nil { + sh.inline_dict = new_dict(ctx.alloc, 8) + } else { + clear(sh.inline_dict) + } + return sh.inline_dict +} + +// content_inline_own copies the parsed dictionary into Document memory for an +// Image that outlives the run -- a display list hands one to a caller long +// after the page's scratch is gone. +// +// The copy is DEEP, and it has to be: the lexer does not copy a name or a +// dictionary key that needs no unescaping, it slices the bytes it read them +// from -- and those bytes are the page's content, which is scratch that +// `page_scratch_end` unmaps. Everything else in a PDF is lexed out of the +// file, which lives as long as the Document; an inline image is the one place +// where it is not, and reading `/Filter` back out of such a dictionary -- +// which `stream_data` does the first time the image is decoded, and for a +// display list that is after the caller has it -- read freed address space. +@(private) +content_inline_own :: proc(doc: ^Document, d: ^Dict) -> ^Dict { + own := new_dict(doc.allocator, max(len(d^), 1)) + for k, v in d^ { + own[content_own_bytes(doc, transmute([]byte)k)] = content_own_value(doc, v, 0) + } + return own +} + +// content_own_bytes copies a run of bytes into the Document, as a string. +@(private) +content_own_bytes :: proc(doc: ^Document, b: []byte) -> string { + if len(b) == 0 { return "" } + out, err := make([]byte, len(b), doc.allocator) + if err != nil { return "" } + copy(out, b) + return string(out) +} + +// content_own_value is the deep copy `content_inline_own` needs. Numbers and +// booleans carry no memory; a reference points at the Document already. Only +// the four that can point into the content buffer are rebuilt, and the two +// that nest are depth-capped like every other walk over file-derived data. +@(private) +content_own_value :: proc(doc: ^Document, o: Object, depth: int) -> Object { + if depth >= MAX_DEPTH { return nil } + #partial switch v in o { + case Name: + return Name(content_own_bytes(doc, transmute([]byte)string(v))) + case String: + return String(transmute([]byte)content_own_bytes(doc, ([]byte)(v))) + case ^Array: + a := new_array(doc.allocator, array_len(v)) + for i in 0 ..< array_len(v) { + append(&a.items, content_own_value(doc, array_get_raw(v, i), depth + 1)) + } + return a + case ^Dict: + nd := new_dict(doc.allocator, max(len(v^), 1)) + for k, e in v^ { + nd[content_own_bytes(doc, transmute([]byte)k)] = content_own_value(doc, e, depth + 1) + } + return nd + case ^Stream: + // An inline image's dictionary is written between BI and ID; there is + // no syntax in it for a stream. + return nil + } + return o +} + +// content_inline_end finds the end of the data and the position just past EI. +@(private) +content_inline_end :: proc(doc: ^Document, d: ^Dict, res: ^Dict, data: []byte, start: int) -> (end, after: int) { + // PDF 2.0 added /L (/Length): a producer that states the length has + // removed all the guesswork, so it is believed first -- but only if EI is + // where it says. + if n, ok := to_int(dict_get_alt(doc, d, "Length", "L")); ok && n >= 0 && + n <= i64(len(data) - start) { + e := start + int(n) + probe := Lexer{data = data, pos = e} + if lexer_accept_keyword(&probe, "EI") { + return e, probe.pos + } + } + + // Unfiltered data has exactly one possible length, and computing it is + // the only way to be right about data that contains the bytes "EI". + if !content_inline_filtered(doc, d) { + if n, ok := content_inline_raw_size(doc, d, res); ok && n <= len(data) - start { + e := start + n + probe := Lexer{data = data, pos = e} + if lexer_accept_keyword(&probe, "EI") { + return e, probe.pos + } + } + } + + // Last resort: the first EI that is bounded by whitespace on the left and + // by whitespace, a delimiter or the end of the stream on the right. + for i := start; i + 1 < len(data); i += 1 { + if data[i] != 'E' || data[i + 1] != 'I' { + continue + } + if i > start && !is_pdf_whitespace(data[i - 1]) { + continue + } + if i + 2 < len(data) && !is_pdf_whitespace(data[i + 2]) && !is_pdf_delimiter(data[i + 2]) { + continue + } + e := i + // The whitespace before EI is a separator, not a sample. + if e > start && is_pdf_whitespace(data[e - 1]) { + e -= 1 + } + return e, i + 2 + } + // No EI at all: the stream is truncated inside the image. + add_repair(doc, .Content_Syntax, i64(start)) + return len(data), len(data) +} + +@(private) +content_inline_filtered :: proc(doc: ^Document, d: ^Dict) -> bool { + f := dict_get_alt(doc, d, "Filter", "F") + #partial switch v in f { + case Name: return true + case ^Array: return array_len(v) > 0 + } + return false +} + +// content_inline_raw_size is the exact byte count of unfiltered sample data. +@(private) +content_inline_raw_size :: proc(doc: ^Document, d: ^Dict, res: ^Dict) -> (int, bool) { + w := dict_int(doc, d, "Width", dict_int(doc, d, "W", 0)) + h := dict_int(doc, d, "Height", dict_int(doc, d, "H", 0)) + if w <= 0 || h <= 0 || w > MAX_IMAGE_DIM || h > MAX_IMAGE_DIM { + return 0, false + } + + ncomp := 1 + bpc := i64(8) + if dict_bool(doc, d, "ImageMask", dict_bool(doc, d, "IM", false)) { + bpc = 1 + } else { + bpc = dict_int(doc, d, "BitsPerComponent", dict_int(doc, d, "BPC", 8)) + cs, err := colorspace_load(doc, dict_get_alt(doc, d, "ColorSpace", "CS"), res) + if err != .None || cs == nil { + return 0, false + } + ncomp = cs.n + } + switch bpc { + case 1, 2, 4, 8, 16: + case: + return 0, false + } + if ncomp < 1 || ncomp > MAX_COLOR_COMPONENTS { + return 0, false + } + + // Rows are padded to a byte boundary (8.9.5.1). The arithmetic is in i64 + // because w, h and ncomp all came out of a file. + bits := i64(w) * i64(ncomp) * bpc + row := (bits + 7) / 8 + total := row * i64(h) + if total <= 0 || total > i64(MAX_INLINE_IMAGE_BYTES) { + return 0, false + } + return int(total), true +} + +// --------------------------------------------------------------------------- +// Marked content and optional content (14.6, 8.11) +// --------------------------------------------------------------------------- + +@(private) +content_mc_begin :: proc(r: ^Content_Runner, tag: string, props: Object) { + ctx := r.ctx + e: Mc_Entry + if tag == "OC" { + oc := props + if nm, is_name := to_name(props); is_name { + // A name is looked up in the /Properties sub-dictionary of the + // resources, which is where BDC's operand lives (14.6.2). + oc = dict_get_raw(dict_dict(ctx.doc, r.res, "Properties"), nm) + } + if !content_oc_visible(ctx, oc) { + e.hides = true + } else if r.n_hidden == 0 { + dev_begin_layer(ctx.dev, content_layer_name(ctx, oc, tag)) + e.layer = true + } + } + if len(r.mc) >= MAX_MARKED_DEPTH { + // Too deep to record. The section is counted so that its EMC still + // balances; what it would have hidden is drawn instead, which shows + // too much rather than losing the rest of the page. + if e.layer { + dev_end_layer(ctx.dev) + } + r.mc_over += 1 + return + } + if e.hides { + r.n_hidden += 1 + } + append(&r.mc, e) +} + +@(private) +content_mc_end :: proc(r: ^Content_Runner) { + if r.mc_over > 0 { + r.mc_over -= 1 + return + } + if len(r.mc) == 0 { + add_repair(r.ctx.doc, .Content_Syntax, 0) + return + } + e := pop(&r.mc) + if e.hides && r.n_hidden > 0 { + r.n_hidden -= 1 + } + if e.layer { + dev_end_layer(r.ctx.dev) + } +} + +@(private) +content_layer_name :: proc(ctx: ^Content_Ctx, oc: Object, fallback: string) -> string { + d, ok := to_dict(resolve(ctx.doc, oc)) + if !ok { + return fallback + } + if b := dict_bytes(ctx.doc, d, "Name"); len(b) > 0 { + return decode_text_string(b, ctx.alloc) + } + return fallback +} + +// content_load_oc reads the catalog's default optional-content configuration +// into a set of groups that are switched off. It is read once per run: a page +// that names the same layer in four hundred BDCs must not walk the arrays +// four hundred times. +@(private) +content_load_oc :: proc(ctx: ^Content_Ctx) { + if ctx.opts.ignore_optional_content { + return + } + doc := ctx.doc + ocp := dict_dict(doc, doc.catalog, "OCProperties") + if ocp == nil { + return + } + cfg := dict_dict(doc, ocp, "D") + if cfg == nil { + return + } + + add_all :: proc(ctx: ^Content_Ctx, a: ^Array, on: bool) { + for i in 0 ..< min(array_len(a), MAX_OC_GROUPS) { + d, ok := to_dict(array_get(ctx.doc, a, i)) + if !ok { + continue + } + key := uintptr(rawptr(d)) + if on { + delete_key(&ctx.oc_off, key) + } else { + ctx.oc_off[key] = true + } + } + } + + // /BaseState OFF means everything in /OCGs starts hidden and /ON is the + // exception list (8.11.4.3). It is rare and cheap to honour. + if dict_name(doc, cfg, "BaseState") == "OFF" { + add_all(ctx, dict_array(doc, ocp, "OCGs"), false) + add_all(ctx, dict_array(doc, cfg, "ON"), true) + } + add_all(ctx, dict_array(doc, cfg, "OFF"), false) +} + +// content_oc_visible answers whether content governed by `obj` -- an OCG, an +// OCMD, or nothing at all -- should be drawn. +@(private) +content_oc_visible :: proc(ctx: ^Content_Ctx, obj: Object) -> bool { + if ctx.opts.ignore_optional_content || obj == nil { + return true + } + d, ok := to_dict(resolve(ctx.doc, obj)) + if !ok { + // A /OC that names nothing resolvable governs nothing. + return true + } + + if dict_type(ctx.doc, d) != "OCMD" { + return !(uintptr(rawptr(d)) in ctx.oc_off) + } + + // An OCMD combines several groups under a policy (8.11.2.3). Its /VE + // visibility expression is not evaluated; /P is, and is what producers + // actually write. + on, total := 0, 0 + tally :: proc(ctx: ^Content_Ctx, o: Object, on, total: ^int) { + dd, is_dict := to_dict(resolve(ctx.doc, o)) + if !is_dict { + return + } + total^ += 1 + if !(uintptr(rawptr(dd)) in ctx.oc_off) { + on^ += 1 + } + } + + ocgs := dict_get(ctx.doc, d, "OCGs") + if a, is_array := to_array(ocgs); is_array { + for i in 0 ..< min(array_len(a), MAX_OC_GROUPS) { + tally(ctx, array_get_raw(a, i), &on, &total) + } + } else { + tally(ctx, ocgs, &on, &total) + } + if total == 0 { + return true + } + + switch dict_name(ctx.doc, d, "P", "AnyOn") { + case "AllOn": return on == total + case "AnyOff": return on < total + case "AllOff": return on == 0 + } + return on > 0 // AnyOn, the default +} diff --git a/content_test.odin b/content_test.odin new file mode 100644 index 0000000..ca4db9d --- /dev/null +++ b/content_test.odin @@ -0,0 +1,2944 @@ +package pdf + +import "core:fmt" +import "core:log" +import "core:math" +import "core:mem/virtual" +import "core:os" +import "core:path/filepath" +import "core:slice" +import "core:strings" +import "core:testing" +import "core:time" + +// Content-stream interpreter tests. +// +// Everything here runs against `Recorder`, a Device that keeps every call it +// is handed. That is deliberately the whole apparatus: the interpreter's +// output IS its sequence of device calls, so a test that pins the calls pins +// the behaviour, and the same recorder is what the three real devices will be +// checked against once they exist. +// +// The fixture document comes from function_test.odin (`cf_open`, `cf_parse`, +// `cf_stream`, `cf_close`). It is opened by scanning and so already carries +// repairs; every assertion about a repair counts the GROWTH of doc.repairs +// rather than its length. + +// --------------------------------------------------------------------------- +// The recording device +// --------------------------------------------------------------------------- + +@(private = "file") +Rec_Kind :: enum u8 { + Fill_Path, + Stroke_Path, + Clip_Path, + Clip_Stroke_Path, + Clip_Text, + Clip_Image_Mask, + Pop_Clip, + Fill_Text, + Stroke_Text, + Ignore_Text, + Fill_Image, + Fill_Image_Mask, + Fill_Shade, + Begin_Mask, + End_Mask, + Begin_Group, + End_Group, + Begin_Tile, + End_Tile, + Begin_Layer, + End_Layer, + Close, +} + +@(private = "file") +Rec :: struct { + kind: Rec_Kind, + even_odd: bool, + ctm: Matrix, + paint: Paint, + stroke: Stroke_State, + verbs: []Path_Verb, + points: []Point, + items: []Text_Item, + font: ^Font, + wmode: int, + mode: Text_Render_Mode, + name: string, + area: Rect, + view: Rect, + xstep: f64, + ystep: f64, + isolated: bool, + knockout: bool, + blend: Blend_Mode, + alpha: f64, + luminosity: bool, + img: ^Image, + shade: ^Shading, + transfer: ^Function, + // A Soft_Mask lives in the run's scratch and does not outlive it, so a + // device that records has to copy it -- exactly as device_list.odin does. + smask: Soft_Mask, + has_smask: bool, +} + +@(private = "file") +Recorder :: struct { + dev: Device, + log: [dynamic]Rec, + // What begin_tile answers: 0 means "the device tiles, run the cell once". + tiles: int, +} + +@(private = "file") +rec_add :: proc(dev: ^Device, rec: Rec) { + r := (^Recorder)(dev.user) + e := rec + if e.paint.soft_mask != nil { + e.smask = e.paint.soft_mask^ + e.has_smask = true + } + append(&r.log, e) +} + +@(private = "file") +rec_path :: proc(rec: ^Rec, p: ^Path) { + if p == nil { + return + } + rec.verbs = slice.clone(p.verbs[:]) + rec.points = slice.clone(p.points[:]) +} + +@(private = "file") +rec_text :: proc(rec: ^Rec, txt: ^Text) { + if txt == nil { + return + } + rec.items = slice.clone(txt.items[:]) + rec.font = txt.font + rec.wmode = txt.wmode + rec.mode = txt.render_mode +} + +@(private = "file") +rec_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + r := Rec{kind = .Fill_Path, even_odd = even_odd, ctm = ctm, paint = paint} + rec_path(&r, path) + rec_add(dev, r) +} + +@(private = "file") +rec_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + r := Rec{kind = .Stroke_Path, ctm = ctm, paint = paint} + if stroke != nil { + r.stroke = stroke^ + } + rec_path(&r, path) + rec_add(dev, r) +} + +@(private = "file") +rec_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { + r := Rec{kind = .Clip_Path, even_odd = even_odd, ctm = ctm} + rec_path(&r, path) + rec_add(dev, r) +} + +@(private = "file") +rec_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { + r := Rec{kind = .Clip_Stroke_Path, ctm = ctm} + if stroke != nil { + r.stroke = stroke^ + } + rec_path(&r, path) + rec_add(dev, r) +} + +@(private = "file") +rec_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + r := Rec{kind = .Clip_Text, ctm = ctm} + rec_text(&r, txt) + rec_add(dev, r) +} + +@(private = "file") +rec_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + rec_add(dev, Rec{kind = .Clip_Image_Mask, ctm = ctm, img = img}) +} + +@(private = "file") +rec_pop_clip :: proc(dev: ^Device) { + rec_add(dev, Rec{kind = .Pop_Clip}) +} + +@(private = "file") +rec_fill_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { + r := Rec{kind = .Fill_Text, ctm = ctm, paint = paint} + rec_text(&r, txt) + rec_add(dev, r) +} + +@(private = "file") +rec_stroke_text :: proc(dev: ^Device, txt: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + r := Rec{kind = .Stroke_Text, ctm = ctm, paint = paint} + if stroke != nil { + r.stroke = stroke^ + } + rec_text(&r, txt) + rec_add(dev, r) +} + +@(private = "file") +rec_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + r := Rec{kind = .Ignore_Text, ctm = ctm} + rec_text(&r, txt) + rec_add(dev, r) +} + +@(private = "file") +rec_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + rec_add(dev, Rec{kind = .Fill_Image, ctm = ctm, paint = paint, img = img}) +} + +@(private = "file") +rec_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + rec_add(dev, Rec{kind = .Fill_Image_Mask, ctm = ctm, paint = paint, img = img}) +} + +@(private = "file") +rec_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { + rec_add(dev, Rec{kind = .Fill_Shade, ctm = ctm, paint = paint, shade = shade}) +} + +@(private = "file") +rec_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { + rec_add(dev, Rec{kind = .Begin_Mask, area = area, luminosity = luminosity, paint = backdrop}) +} + +@(private = "file") +rec_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { + rec_add(dev, Rec{kind = .End_Mask, transfer = transfer}) +} + +@(private = "file") +rec_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + rec_add(dev, Rec{ + kind = .Begin_Group, area = area, isolated = isolated, + knockout = knockout, blend = blend, alpha = alpha, + }) +} + +@(private = "file") +rec_end_group :: proc(dev: ^Device) { + rec_add(dev, Rec{kind = .End_Group}) +} + +@(private = "file") +rec_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { + r := (^Recorder)(dev.user) + append(&r.log, Rec{ + kind = .Begin_Tile, area = area, view = view, + xstep = xstep, ystep = ystep, ctm = ctm, + }) + return r.tiles +} + +@(private = "file") +rec_end_tile :: proc(dev: ^Device) { + rec_add(dev, Rec{kind = .End_Tile}) +} + +@(private = "file") +rec_begin_layer :: proc(dev: ^Device, name: string) { + // The name is scratch memory the run reclaims, so it has to be copied. + rec_add(dev, Rec{kind = .Begin_Layer, name = strings.clone(name)}) +} + +@(private = "file") +rec_end_layer :: proc(dev: ^Device) { + rec_add(dev, Rec{kind = .End_Layer}) +} + +@(private = "file") +rec_close :: proc(dev: ^Device) { + rec_add(dev, Rec{kind = .Close}) +} + +@(private = "file") +rec_init :: proc(rec: ^Recorder) { + rec.log = make([dynamic]Rec, 0, 32) + rec.dev = Device{ + user = rec, + fill_path = rec_fill_path, + stroke_path = rec_stroke_path, + clip_path = rec_clip_path, + clip_stroke_path = rec_clip_stroke_path, + clip_text = rec_clip_text, + clip_image_mask = rec_clip_image_mask, + pop_clip = rec_pop_clip, + fill_text = rec_fill_text, + stroke_text = rec_stroke_text, + ignore_text = rec_ignore_text, + fill_image = rec_fill_image, + fill_image_mask = rec_fill_image_mask, + fill_shade = rec_fill_shade, + begin_mask = rec_begin_mask, + end_mask = rec_end_mask, + begin_group = rec_begin_group, + end_group = rec_end_group, + begin_tile = rec_begin_tile, + end_tile = rec_end_tile, + begin_layer = rec_begin_layer, + end_layer = rec_end_layer, + close = rec_close, + } +} + +@(private = "file") +rec_destroy :: proc(rec: ^Recorder) { + for &e in rec.log { + delete(e.verbs) + delete(e.points) + delete(e.items) + delete(e.name) + } + delete(rec.log) + rec.log = nil +} + +@(private = "file") +rec_count :: proc(rec: ^Recorder, kind: Rec_Kind) -> (n: int) { + for &e in rec.log { + if e.kind == kind { + n += 1 + } + } + return +} + +// rec_nth is the (0-based) n'th call of a kind, or nil. +@(private = "file") +rec_nth :: proc(rec: ^Recorder, kind: Rec_Kind, n := 0) -> ^Rec { + seen := 0 + for &e in rec.log { + if e.kind != kind { + continue + } + if seen == n { + return &e + } + seen += 1 + } + return nil +} + +// rec_kinds is the whole call sequence, for order assertions. +@(private = "file") +rec_kinds :: proc(rec: ^Recorder, allocator := context.allocator) -> []Rec_Kind { + out := make([]Rec_Kind, len(rec.log), allocator) + for e, i in rec.log { + out[i] = e.kind + } + return out +} + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +@(private = "file") +CT_EPS :: 1e-9 + +@(private = "file") +ct_near :: proc(t: ^testing.T, got, want: f64, what: string, tol := CT_EPS, + loc := #caller_location) { + if !(abs(got - want) <= tol) { + testing.expectf(t, false, "%s: got %v, want %v", what, got, want, loc = loc) + } +} + +@(private = "file") +ct_point :: proc(t: ^testing.T, p: Point, x, y: f64, what: string, tol := CT_EPS, + loc := #caller_location) { + if !(abs(p.x - x) <= tol && abs(p.y - y) <= tol) { + testing.expectf(t, false, "%s: got (%v, %v), want (%v, %v)", + what, p.x, p.y, x, y, loc = loc) + } +} + +// ct_run executes a content stream against a fresh recorder. +@(private = "file") +ct_run :: proc(doc: ^Document, rec: ^Recorder, src: string, res: ^Dict = nil, + ctm := MATRIX_IDENTITY, opts := Content_Options{}) -> Error { + return run_content(doc, &rec.dev, transmute([]byte)src, res, ctm, opts) +} + +// A font every code of which is exactly half an em wide. /BaseFont is not one +// of the standard 14, so no AFM metric can slip in ahead of /MissingWidth and +// every advance in these tests is 0.5 * Tfs exactly. +@(private = "file") +CT_FONT :: "<< /Type /Font /Subtype /Type1 /BaseFont /Halfwidth " + + "/FontDescriptor << /Flags 32 /MissingWidth 500 >> >>" + +// The same width through a two-byte composite font, so that a code 32 that is +// half of a two-byte code can be told from a single-byte one. +@(private = "file") +CT_FONT0 :: "<< /Type /Font /Subtype /Type0 /BaseFont /Halfwide /Encoding /Identity-H " + + "/DescendantFonts [ << /Type /Font /Subtype /CIDFontType2 /BaseFont /Halfwide " + + "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> " + + "/DW 500 >> ] >>" + +@(private = "file") +CT_TYPE3 :: "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " + + "/FontBBox [0 0 100 100] /FirstChar 65 /LastChar 66 /Widths [100 100] " + + "/Encoding << /Type /Encoding /Differences [65 /square 66 /blank] >> >>" + +// ct_res builds the resource dictionary the tests share. Everything in it is +// Document memory, so it lives until cf_close. +@(private = "file") +ct_res :: proc(doc: ^Document) -> ^Dict { + res := new_dict(doc.allocator) + + fonts := new_dict(doc.allocator) + dict_set(fonts, "F1", cf_parse(doc, CT_FONT)) + dict_set(fonts, "F0", cf_parse(doc, CT_FONT0)) + t3 := cf_parse(doc, CT_TYPE3) + if t3d, ok := to_dict(t3); ok { + procs := new_dict(doc.allocator) + // A square that fills the whole glyph box, and a glyph that draws + // nothing at all. + dict_set(procs, "square", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 d0 0 0 100 100 re f"))) + dict_set(procs, "blank", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 d0"))) + dict_set(t3d, "CharProcs", procs) + } + dict_set(fonts, "T3", t3) + dict_set(res, "Font", fonts) + + xobj := new_dict(doc.allocator) + // A form that invokes itself. Its /Resources are inherited from the page, + // which is what makes /Self resolve inside it. + dict_set(xobj, "Self", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>", + transmute([]byte)string("0 0 10 10 re f /Self Do"))) + dict_set(xobj, "Box", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Matrix [2 0 0 2 5 5] >>", + transmute([]byte)string("0 0 4 4 re f"))) + dict_set(xobj, "Grp", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] " + + "/Group << /S /Transparency /I true /K true /CS /DeviceRGB >> >>", + transmute([]byte)string("0 0 4 4 re f"))) + // A 2x2 greyscale image and a 2x2 stencil. + dict_set(xobj, "Im", cf_stream(doc, + "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 " + + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", + transmute([]byte)string("\x00\x40\x80\xff"))) + dict_set(xobj, "Stencil", cf_stream(doc, + "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ImageMask true >>", + transmute([]byte)string("\x40\x80"))) + dict_set(res, "XObject", xobj) + + shadings := new_dict(doc.allocator) + dict_set(shadings, "Sh", cf_parse(doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >>")) + dict_set(res, "Shading", shadings) + + pats := new_dict(doc.allocator) + dict_set(pats, "Tile", cf_stream(doc, + "<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] " + + "/XStep 10 /YStep 10 >>", + transmute([]byte)string("0 0 5 5 re f"))) + dict_set(pats, "Uncol", cf_stream(doc, + "<< /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 10 10] " + + "/XStep 10 /YStep 10 >>", + transmute([]byte)string("1 0 0 rg 0 0 5 5 re f"))) + dict_set(pats, "ShadeGS", cf_parse(doc, + "<< /PatternType 2 /ExtGState << /ca 0.25 /BM /Screen >> /Shading " + + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>")) + dict_set(pats, "Shade", cf_parse(doc, + "<< /PatternType 2 /Matrix [1 0 0 1 3 4] /Shading " + + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>")) + dict_set(res, "Pattern", pats) + + egs := new_dict(doc.allocator) + dict_set(egs, "G1", cf_parse(doc, + "<< /Type /ExtGState /LW 4 /LC 1 /LJ 2 /ML 3 /CA 0.25 /ca 0.5 " + + "/BM /Multiply /D [[3 2] 1] >>")) + dict_set(egs, "GNone", cf_parse(doc, "<< /Type /ExtGState /SMask /None >>")) + smask_form := cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + + "/Group << /S /Transparency /CS /DeviceGray >> >>", + transmute([]byte)string("0 0 20 20 re f")) + smd := new_dict(doc.allocator) + dict_set(smd, "S", Name("Luminosity")) + dict_set(smd, "G", smask_form) + gsm := new_dict(doc.allocator) + dict_set(gsm, "SMask", smd) + dict_set(egs, "GMask", gsm) + dict_set(res, "ExtGState", egs) + + cspaces := new_dict(doc.allocator) + dict_set(cspaces, "Sep", cf_parse(doc, + "[/Separation /Spot /DeviceRGB << /FunctionType 2 /Domain [0 1] " + + "/C0 [0 0 0] /C1 [1 0 0] /N 1 >>]")) + dict_set(cspaces, "PatRGB", cf_parse(doc, "[/Pattern /DeviceRGB]")) + dict_set(res, "ColorSpace", cspaces) + + return res +} + +// --------------------------------------------------------------------------- +// Graphics state: q, Q, cm +// --------------------------------------------------------------------------- + +@(test) +test_content_q_restores_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "q 2 0 0 2 0 0 cm 0 0 1 1 re f Q 0 0 1 1 re f"), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) + inner := rec_nth(&rec, .Fill_Path, 0) + outer := rec_nth(&rec, .Fill_Path, 1) + if inner == nil || outer == nil { return } + ct_near(t, inner.ctm.a, 2, "inside q the CTM is scaled") + ct_near(t, outer.ctm.a, 1, "after Q the CTM is restored") +} + +@(test) +test_content_cm_composes_in_order :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // CTM = operand * CTM, so the second `cm` is applied FIRST to user space: + // a point at (1,0) lands at 2*1 + 10 = 12 under `2 0 0 2 0 0` then + // `1 0 0 1 10 0`... which is 2*(1+10) = 22 the other way round. + testing.expect_value(t, ct_run(doc, &rec, "2 0 0 2 0 0 cm 1 0 0 1 10 0 cm 0 0 1 1 re f"), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { return } + ct_point(t, transform_point(e.ctm, Point{1, 0}), 22, 0, "cm order") +} + +@(test) +test_content_Q_underflow_is_ignored :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "Q Q 1 0 0 1 5 0 cm 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "a stray Q must be recorded as a repair") + // And the page still draws, in the state the strays did not damage. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + e := rec_nth(&rec, .Fill_Path) + if e == nil { return } + ct_near(t, e.ctm.e, 5, "the cm after the stray Qs still applies") +} + +// A `q` past the depth cap saves nothing, so its `Q` must restore nothing. If +// the overflow were not counted, the first `Q` would pop an outer scope's +// state and everything after it would draw in the wrong one. +@(test) +test_content_q_overflow_does_not_pop :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + over :: 6 + b := strings.builder_make() + defer strings.builder_destroy(&b) + strings.write_string(&b, "1 0 0 1 5 0 cm ") + for _ in 0 ..< MAX_GSTATE_DEPTH + over { + strings.write_string(&b, "q ") + } + strings.write_string(&b, "1 0 0 1 7 0 cm ") + for _ in 0 ..< over { + strings.write_string(&b, "Q ") + } + strings.write_string(&b, "0 0 1 1 re f ") + for _ in 0 ..< MAX_GSTATE_DEPTH { + strings.write_string(&b, "Q ") + } + strings.write_string(&b, "0 0 1 1 re f") + + testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) + first := rec_nth(&rec, .Fill_Path, 0) + second := rec_nth(&rec, .Fill_Path, 1) + if first == nil || second == nil { return } + ct_near(t, first.ctm.e, 12, "the Qs matching the overflowed qs restore nothing") + ct_near(t, second.ctm.e, 5, "the real Qs restore the state their qs saved") +} + +@(test) +test_content_stroke_state_operators :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "5 w 1 J 2 j 7 M [3 2] 1 d 0 0 10 10 re S"), Error.None) + e := rec_nth(&rec, .Stroke_Path) + if e == nil { + testing.expect(t, false, "S did not reach the device") + return + } + ct_near(t, e.stroke.line_width, 5, "w") + testing.expect_value(t, e.stroke.line_cap, Line_Cap.Round) + testing.expect_value(t, e.stroke.line_join, Line_Join.Bevel) + ct_near(t, e.stroke.miter_limit, 7, "M") + testing.expect_value(t, e.stroke.dash_count, 2) + ct_near(t, e.stroke.dash[0], 3, "dash[0]") + ct_near(t, e.stroke.dash[1], 2, "dash[1]") + ct_near(t, e.stroke.dash_phase, 1, "dash phase") +} + +@(test) +test_content_non_finite_cm_is_rejected :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // The product overflows to infinity, which must never reach the geometry. + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, + "1e300 0 0 1e300 0 0 cm 1e300 0 0 1e300 0 0 cm 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "an unusable cm must be recorded") + e := rec_nth(&rec, .Fill_Path) + if e == nil { return } + testing.expect(t, matrix_is_finite(e.ctm), "the CTM must stay finite") + ct_near(t, e.ctm.a, 1e300, "an unusable cm leaves the CTM as it was", 1e290) +} + +// --------------------------------------------------------------------------- +// Path construction and painting +// --------------------------------------------------------------------------- + +@(test) +test_content_path_operators :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "1 2 m 3 4 l 5 6 7 8 9 10 c 11 12 13 14 v 15 16 17 18 y h f"), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expect(t, false, "f did not reach the device") + return + } + want := []Path_Verb{.Move, .Line, .Curve, .Curve, .Curve, .Close} + testing.expect(t, slice.equal(e.verbs, want), "verb sequence") + if len(e.points) < 11 { return } + ct_point(t, e.points[0], 1, 2, "m") + ct_point(t, e.points[1], 3, 4, "l") + ct_point(t, e.points[4], 9, 10, "c end point") + // `v` takes the current point as its first control point. + ct_point(t, e.points[5], 9, 10, "v implied control point") + ct_point(t, e.points[7], 13, 14, "v end point") + // `y` repeats its end point as the second control point. + ct_point(t, e.points[9], 17, 18, "y second control point") + ct_point(t, e.points[10], 17, 18, "y end point") +} + +@(test) +test_content_re_winds_counter_clockwise :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "10 20 30 40 re f"), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil || len(e.points) < 4 { return } + ct_point(t, e.points[0], 10, 20, "re origin") + ct_point(t, e.points[1], 40, 20, "re second corner") + ct_point(t, e.points[2], 40, 60, "re third corner") + ct_point(t, e.points[3], 10, 60, "re fourth corner") +} + +@(test) +test_content_painting_operators :: proc(t: ^testing.T) { + Case :: struct { + src: string, + fills: int, + strokes: int, + even_odd: bool, + closed: bool, + } + cases := []Case{ + {"0 0 1 1 re S", 0, 1, false, false}, + {"0 0 m 1 1 l s", 0, 1, false, true}, + {"0 0 1 1 re f", 1, 0, false, false}, + {"0 0 1 1 re F", 1, 0, false, false}, + {"0 0 1 1 re f*", 1, 0, true, false}, + {"0 0 1 1 re B", 1, 1, false, false}, + {"0 0 1 1 re B*", 1, 1, true, false}, + {"0 0 m 1 1 l b", 1, 1, false, true}, + {"0 0 m 1 1 l b*",1, 1, true, true}, + {"0 0 1 1 re n", 0, 0, false, false}, + } + for c in cases { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, c.src), Error.None) + testing.expectf(t, rec_count(&rec, .Fill_Path) == c.fills, + "%s: %d fills, want %d", c.src, rec_count(&rec, .Fill_Path), c.fills) + testing.expectf(t, rec_count(&rec, .Stroke_Path) == c.strokes, + "%s: %d strokes, want %d", c.src, rec_count(&rec, .Stroke_Path), c.strokes) + if c.fills > 0 { + e := rec_nth(&rec, .Fill_Path) + if e != nil { + testing.expectf(t, e.even_odd == c.even_odd, "%s: fill rule", c.src) + if c.closed && len(e.verbs) > 0 { + testing.expectf(t, e.verbs[len(e.verbs) - 1] == .Close, + "%s: the path must be closed before painting", c.src) + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Clipping: W and W* arm a clip that the NEXT painting operator installs +// --------------------------------------------------------------------------- + +@(test) +test_content_clip_takes_effect_after_the_paint :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // `f` paints first and only then installs the clip. + testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W f"), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + // The trailing pop is the interpreter balancing the device's clip stack + // at the end of the stream, which it owes whether or not a Q ever came. + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Fill_Path, .Clip_Path, .Pop_Clip}), + "the fill must reach the device before the clip it arms") +} + +@(test) +test_content_clip_is_not_installed_at_W :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // `sh` is not a path-painting operator, so the armed clip is still + // pending when it runs: the shading must be painted unclipped and the + // clip must arrive only with the `n`. + testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W /Sh sh n", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Fill_Shade, .Clip_Path, .Pop_Clip}), + "W must not clip until a painting operator runs") +} + +@(test) +test_content_clip_rule_and_pop_on_Q :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "q 0 0 10 10 re W* n Q 0 0 1 1 re f"), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Pop_Clip, .Fill_Path}), + "Q must pop exactly the clips its q was holding") + e := rec_nth(&rec, .Clip_Path) + if e == nil { return } + testing.expect(t, e.even_odd, "W* clips with the even-odd rule") +} + +@(test) +test_content_clip_survives_to_end_of_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // No Q at all: the interpreter still has to leave the device's clip stack + // as it found it. + testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W n 0 0 1 1 re f"), Error.None) + testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) + testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, kinds[len(kinds) - 1] == .Pop_Clip, "the clip is popped last, not early") +} + +// --------------------------------------------------------------------------- +// Colour +// --------------------------------------------------------------------------- + +@(test) +test_content_device_colour_operators :: proc(t: ^testing.T) { + Case :: struct { + src: string, + rgb: [3]f64, + n: int, + } + cases := []Case{ + {"0.5 g 0 0 1 1 re f", {0.5, 0.5, 0.5}, 1}, + {"1 0 0 rg 0 0 1 1 re f", {1, 0, 0}, 3}, + {"0 1 1 0 k 0 0 1 1 re f", {1, 0, 0}, 4}, + } + for c in cases { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, c.src), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expectf(t, false, "%s did not paint", c.src) + continue + } + testing.expectf(t, e.paint.color.n == c.n, "%s: %d components", c.src, e.paint.color.n) + for i in 0 ..< 3 { + ct_near(t, e.paint.rgb[i], c.rgb[i], c.src, 1e-6) + } + } +} + +@(test) +test_content_stroke_colour_is_separate :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg 0 0 1 RG 0 0 1 1 re B"), Error.None) + f := rec_nth(&rec, .Fill_Path) + s := rec_nth(&rec, .Stroke_Path) + if f == nil || s == nil { return } + ct_near(t, f.paint.rgb[0], 1, "fill red", 1e-6) + ct_near(t, s.paint.rgb[2], 1, "stroke blue", 1e-6) + ct_near(t, s.paint.rgb[0], 0, "stroke has no red", 1e-6) +} + +@(test) +test_content_named_colourspace_and_scn :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // A Separation whose tint transform takes 1 to pure red. + testing.expect_value(t, ct_run(doc, &rec, "/Sep cs 1 scn 0 0 1 1 re f", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expect(t, false, "the separation fill did not paint") + return + } + testing.expect_value(t, e.paint.color.n, 1) + ct_near(t, e.paint.rgb[0], 1, "separation red", 1e-6) + ct_near(t, e.paint.rgb[1], 0, "separation green", 1e-6) +} + +// Selecting a colour space resets the colour to that space's initial value, +// so a `cs` after a red `rg` paints black, not red (8.6.8). +@(test) +test_content_cs_resets_the_colour :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg /DeviceRGB cs 0 0 1 1 re f"), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { return } + ct_near(t, e.paint.rgb[0], 0, "cs must reset the colour to black", 1e-6) +} + +@(test) +test_content_alpha_and_blend_from_extgstate :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "/G1 gs 0 0 10 10 re B", ct_res(doc)), Error.None) + f := rec_nth(&rec, .Fill_Path) + s := rec_nth(&rec, .Stroke_Path) + if f == nil || s == nil { + testing.expect(t, false, "the ExtGState fill/stroke did not paint") + return + } + ct_near(t, f.paint.alpha, 0.5, "ca is the fill alpha") + ct_near(t, s.paint.alpha, 0.25, "CA is the stroke alpha") + testing.expect_value(t, f.paint.blend, Blend_Mode.Multiply) + ct_near(t, s.stroke.line_width, 4, "LW") + testing.expect_value(t, s.stroke.line_cap, Line_Cap.Round) + testing.expect_value(t, s.stroke.line_join, Line_Join.Bevel) + testing.expect_value(t, s.stroke.dash_count, 2) + ct_near(t, s.stroke.dash_phase, 1, "the /D phase") +} + +// --------------------------------------------------------------------------- +// Text +// +// The fixture font is exactly half an em wide for every code, so at 10 point +// every advance below is 5.0 and the expected positions are arithmetic. +// --------------------------------------------------------------------------- + +@(private = "file") +ct_origin :: proc(items: []Text_Item, i: int) -> Point { + if i < 0 || i >= len(items) { + return Point{-1e30, -1e30} + } + return transform_point(items[i].trm, Point{0, 0}) +} + +@(test) +test_content_text_matrix_sequence :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // Td sets the line matrix; each glyph advances the text matrix alone, so + // T* returns to where the LINE started rather than to where it ended. + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 20 TL 100 200 Td (AB) Tj T* (C) Tj ET", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Text), 2) + + first := rec_nth(&rec, .Fill_Text, 0) + second := rec_nth(&rec, .Fill_Text, 1) + if first == nil || second == nil { return } + testing.expect_value(t, len(first.items), 2) + ct_point(t, ct_origin(first.items, 0), 100, 200, "first glyph", 1e-9) + ct_point(t, ct_origin(first.items, 1), 105, 200, "second glyph advances by 0.5 em") + testing.expect_value(t, len(second.items), 1) + ct_point(t, ct_origin(second.items, 0), 100, 180, "T* moves the line, not the pen") +} + +@(test) +test_content_TD_sets_leading :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // TD is Td with TL set to -ty, so the following T* moves by the same + // amount again. + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 10 -20 TD (A) Tj T* (B) Tj ET", ct_res(doc)), Error.None) + a := rec_nth(&rec, .Fill_Text, 0) + b := rec_nth(&rec, .Fill_Text, 1) + if a == nil || b == nil { return } + ct_point(t, ct_origin(a.items, 0), 10, -20, "TD") + ct_point(t, ct_origin(b.items, 0), 10, -40, "T* after TD") +} + +@(test) +test_content_Tm_scales_glyph_and_advance :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 2 0 0 2 30 40 Tm (AB) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil || len(e.items) < 2 { return } + ct_point(t, ct_origin(e.items, 0), 30, 40, "Tm places the first glyph") + // The advance is 5 in text space, and Tm doubles everything. + ct_point(t, ct_origin(e.items, 1), 40, 40, "Tm scales the advance") + // The rendering matrix folds in the font matrix (1/1000), the size and Tm. + ct_near(t, e.items[0].trm.a, 0.02, "trm scale") + ct_near(t, e.items[0].advance, 5, "advance is unscaled text space") +} + +@(test) +test_content_Tz_scales_horizontally_only :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 200 Tz (AB) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil || len(e.items) < 2 { return } + // Th multiplies the horizontal scale of the glyph and the advance, and + // nothing else: putting it in `d` as well is the classic Tz bug. + ct_near(t, e.items[0].trm.a, 0.02, "Tz scales the glyph horizontally") + ct_near(t, e.items[0].trm.d, 0.01, "Tz must not scale the glyph vertically") + ct_point(t, ct_origin(e.items, 1), 10, 0, "Tz scales the advance") + ct_near(t, e.items[0].advance, 5, "the recorded advance is before Th") +} + +@(test) +test_content_Ts_raises_the_glyph_not_the_pen :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 7 Ts (AB) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil || len(e.items) < 2 { return } + ct_point(t, ct_origin(e.items, 0), 0, 7, "Ts lifts the glyph") + ct_point(t, ct_origin(e.items, 1), 5, 7, "the rise does not accumulate along the line") +} + +// 9.3.3: word spacing applies to a single-byte code 32 and to nothing else. +@(test) +test_content_word_spacing_single_byte :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 3 Tw 1 Tc ( A) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil || len(e.items) < 2 { return } + testing.expect(t, e.items[0].is_space, "a one-byte 32 is a space") + // 0.5 em + Tc + Tw = 5 + 1 + 3. + ct_point(t, ct_origin(e.items, 1), 9, 0, "word spacing applies to a one-byte space") +} + +@(test) +test_content_word_spacing_not_on_two_byte_code :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // <0020> in an Identity-H font is CID 32, an ordinary glyph, and stretching + // it would space CJK text at random. + testing.expect_value(t, ct_run(doc, &rec, + "BT /F0 10 Tf 3 Tw 1 Tc <00200041> Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil { + testing.expect(t, false, "the composite font did not draw") + return + } + testing.expect_value(t, len(e.items), 2) + if len(e.items) < 2 { return } + testing.expect(t, !e.items[0].is_space, "a two-byte 0x0020 is not a space") + // 0.5 em + Tc only. + ct_point(t, ct_origin(e.items, 1), 6, 0, "no word spacing on a two-byte code") +} + +// A positive TJ number pulls the following glyphs CLOSER; a negative one +// pushes them apart. Getting the sign wrong reverses every kern in the file. +@(test) +test_content_TJ_number_direction :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf [(A) -1000 (B) 200 (C)] TJ ET", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Text), 3) + a := rec_nth(&rec, .Fill_Text, 0) + b := rec_nth(&rec, .Fill_Text, 1) + c := rec_nth(&rec, .Fill_Text, 2) + if a == nil || b == nil || c == nil { return } + ct_point(t, ct_origin(a.items, 0), 0, 0, "first glyph") + // 5 (advance) + 1000/1000 * 10 (the negative number moves right). + ct_point(t, ct_origin(b.items, 0), 15, 0, "a negative TJ number opens a gap") + // 15 + 5 - 200/1000 * 10. + ct_point(t, ct_origin(c.items, 0), 18, 0, "a positive TJ number closes one") +} + +@(test) +test_content_TJ_adjustment_obeys_Tz :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 200 Tz [(A) -1000 (B)] TJ ET", ct_res(doc)), Error.None) + b := rec_nth(&rec, .Fill_Text, 1) + if b == nil { return } + // (5 + 10) * 2. + ct_point(t, ct_origin(b.items, 0), 30, 0, "the horizontal scale applies to TJ too") +} + +@(test) +test_content_quote_operators :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // ' is T* then Tj. " additionally sets word and character spacing. + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 12 TL 5 100 Td (A) ' 3 1 (X A) \" ET", ct_res(doc)), Error.None) + a := rec_nth(&rec, .Fill_Text, 0) + b := rec_nth(&rec, .Fill_Text, 1) + if a == nil || b == nil { return } + ct_point(t, ct_origin(a.items, 0), 5, 88, "' moves down one line first") + ct_point(t, ct_origin(b.items, 0), 5, 76, "\" moves down another line") + if len(b.items) < 3 { return } + // X: 5 + 1 (Tc). space: 5 + 1 + 3 (Tw). + ct_point(t, ct_origin(b.items, 1), 11, 76, "\" sets the character spacing") + ct_point(t, ct_origin(b.items, 2), 20, 76, "\" sets the word spacing") +} + +@(test) +test_content_render_modes :: proc(t: ^testing.T) { + Case :: struct { + mode: string, + fill: int, + stroke: int, + ignore: int, + clip: int, + } + cases := []Case{ + {"0", 1, 0, 0, 0}, + {"1", 0, 1, 0, 0}, + {"2", 1, 1, 0, 0}, + {"3", 0, 0, 1, 0}, + {"4", 1, 0, 0, 1}, + {"5", 0, 1, 0, 1}, + {"6", 1, 1, 0, 1}, + {"7", 0, 0, 1, 1}, + } + for c in cases { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + src := strings.concatenate({"BT /F1 10 Tf ", c.mode, " Tr (A) Tj ET"}) + defer delete(src) + testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) + testing.expectf(t, rec_count(&rec, .Fill_Text) == c.fill, "Tr %s: fills", c.mode) + testing.expectf(t, rec_count(&rec, .Stroke_Text) == c.stroke, "Tr %s: strokes", c.mode) + testing.expectf(t, rec_count(&rec, .Ignore_Text) == c.ignore, "Tr %s: ignores", c.mode) + testing.expectf(t, rec_count(&rec, .Clip_Text) == c.clip, "Tr %s: clips", c.mode) + } +} + +// Mode 3 draws nothing and must still be handed over: an OCR layer under a +// scan is written that way and is the whole of what such a page extracts. +@(test) +test_content_invisible_text_still_reaches_the_device :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /F1 10 Tf 3 Tr 40 50 Td (AB) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Ignore_Text) + if e == nil { + testing.expect(t, false, "invisible text must still reach ignore_text") + return + } + testing.expect_value(t, len(e.items), 2) + testing.expect_value(t, e.mode, Text_Render_Mode.Invisible) + ct_point(t, ct_origin(e.items, 0), 40, 50, "invisible text is still positioned") +} + +@(test) +test_content_text_clip_is_installed_at_ET :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // The clip is the union of everything shown in a clipping mode between BT + // and ET, and only arrives at ET -- after both showing operators. + testing.expect_value(t, ct_run(doc, &rec, + "q BT /F1 10 Tf 7 Tr (AB) Tj (CD) Tj ET Q 0 0 1 1 re f", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, + []Rec_Kind{.Ignore_Text, .Ignore_Text, .Clip_Text, .Pop_Clip, .Fill_Path}), + "the text clip is installed once, at ET, and popped by Q") + e := rec_nth(&rec, .Clip_Text) + if e == nil { return } + testing.expect_value(t, len(e.items), 4) +} + +@(test) +test_content_missing_font_draws_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, + "BT /Nope 10 Tf (A) Tj ET 0 0 1 1 re f", ct_res(doc)), Error.None) + testing.expect(t, len(doc.repairs) > before, "a missing font is a repair") + testing.expect_value(t, rec_count(&rec, .Fill_Text), 0) + // And the rest of the page still draws. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// --------------------------------------------------------------------------- +// Type 3 fonts: a glyph is a content stream +// --------------------------------------------------------------------------- + +@(test) +test_content_type3_glyph_runs_its_content_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // /FontMatrix is 1/100, so a 100-unit glyph box is one em, and at 10 point + // the glyph's content runs under a CTM of 0.1. + testing.expect_value(t, ct_run(doc, &rec, + "BT /T3 10 Tf (A) Tj ET", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Ignore_Text), 1) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expect(t, false, "the Type 3 glyph description did not run") + return + } + ct_near(t, e.ctm.a, 0.1, "the glyph runs under font matrix x size") + // The glyph's own square is 100 units, which is 10 device units here. + ct_point(t, transform_point(e.ctm, Point{100, 100}), 10, 10, "glyph box") +} + +@(test) +test_content_type3_advance_uses_the_font_matrix :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // A Type 3 width is in the font's own glyph space: 100 units through a + // 1/100 matrix is one em, so at 10 point the advance is 10, not 1. + testing.expect_value(t, ct_run(doc, &rec, + "BT /T3 10 Tf (AB) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Ignore_Text) + if e == nil || len(e.items) < 2 { return } + ct_near(t, e.items[0].advance, 10, "Type 3 advance") + ct_point(t, ct_origin(e.items, 1), 10, 0, "the second glyph") + // The second glyph draws nothing, so only the first left a mark. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_type3_invisible_mode_draws_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "BT /T3 10 Tf 3 Tr (A) Tj ET", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Ignore_Text), 1) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) +} + +// --------------------------------------------------------------------------- +// Form XObjects +// --------------------------------------------------------------------------- + +@(test) +test_content_form_that_invokes_itself_terminates :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "/Self Do", ct_res(doc)), Error.None) + testing.expect(t, len(doc.repairs) > before, "the recursion must be recorded") + // Exactly one pass through the form's body. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Fill_Path, .Pop_Clip}), + "the form's BBox clip must be pushed and popped exactly once") +} + +@(test) +test_content_form_matrix_and_bbox :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "/Box Do", ct_res(doc)), Error.None) + clip := rec_nth(&rec, .Clip_Path) + fill := rec_nth(&rec, .Fill_Path) + if clip == nil || fill == nil { + testing.expect(t, false, "the form did not run") + return + } + // /Matrix [2 0 0 2 5 5] composes in front of the invoking CTM. + ct_near(t, fill.ctm.a, 2, "the form matrix scales the contents") + ct_near(t, fill.ctm.e, 5, "the form matrix translates the contents") + // The BBox is clipped in the form's own space, under the same matrix. + if len(clip.points) >= 3 { + ct_point(t, transform_point(clip.ctm, clip.points[2]), 25, 25, "BBox corner") + } +} + +@(test) +test_content_form_inherits_resources :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // /Self names itself out of the page's resources, which is only possible + // because a form with no /Resources inherits the invoking stream's. + testing.expect_value(t, ct_run(doc, &rec, "/Self Do", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_transparency_group :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "/G1 gs /Grp Do", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, + []Rec_Kind{.Clip_Path, .Begin_Group, .Fill_Path, .End_Group, .Pop_Clip}), + "the group brackets the form's contents inside its BBox clip") + + g := rec_nth(&rec, .Begin_Group) + inner := rec_nth(&rec, .Fill_Path) + if g == nil || inner == nil { return } + testing.expect(t, g.isolated, "/I") + testing.expect(t, g.knockout, "/K") + ct_near(t, g.alpha, 0.5, "the group takes the current fill alpha") + testing.expect_value(t, g.blend, Blend_Mode.Multiply) + ct_point(t, Point{g.area.x0, g.area.y0}, 0, 0, "group area") + ct_point(t, Point{g.area.x1, g.area.y1}, 10, 10, "group area") + // 11.6.6: the contents composite into the group with the parameters reset. + ct_near(t, inner.paint.alpha, 1, "the group's contents are opaque inside it") + testing.expect_value(t, inner.paint.blend, Blend_Mode.Normal) +} + +// --------------------------------------------------------------------------- +// Images +// --------------------------------------------------------------------------- + +@(test) +test_content_image_xobject :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "q 8 0 0 8 3 4 cm /Im Do Q", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Image) + if e == nil || e.img == nil { + testing.expect(t, false, "the image did not reach the device") + return + } + testing.expect_value(t, e.img.width, 2) + testing.expect_value(t, e.img.height, 2) + // An image always covers the unit square of the CTM (8.9.5.2). + ct_point(t, transform_point(e.ctm, Point{1, 1}), 11, 12, "the image's far corner") +} + +@(test) +test_content_image_mask_takes_the_fill_colour :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg /Stencil Do", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) + e := rec_nth(&rec, .Fill_Image_Mask) + if e == nil { + testing.expect(t, false, "the stencil did not reach fill_image_mask") + return + } + testing.expect(t, e.paint.is_mask, "a stencil paints a shape, not a colour") + ct_near(t, e.paint.rgb[0], 1, "the stencil takes the current fill colour", 1e-6) +} + +@(test) +test_content_ignore_image_hint :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.dev.hints = {.Ignore_Image} + + // Decoding a photograph to extract the text off the page it sits on is + // pure waste, and the hint is what lets the interpreter skip it. + testing.expect_value(t, ct_run(doc, &rec, "/Im Do 0 0 1 1 re f", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// --------------------------------------------------------------------------- +// Inline images +// --------------------------------------------------------------------------- + +@(test) +test_content_inline_image_unfiltered :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + src := "BI /W 2 /H 2 /CS /G /BPC 8 ID \x00\x40\x80\xff EI 0 0 1 1 re f" + testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Image) + if e == nil || e.img == nil { + testing.expect(t, false, "the inline image did not reach the device") + return + } + testing.expect_value(t, e.img.width, 2) + testing.expect_value(t, e.img.height, 2) + // The operator after EI must still be read: a mis-measured image + // desynchronises the whole rest of the page. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// The bytes "EI" occur inside real image data. Unfiltered data has exactly +// one possible length, and computing it is the only way to be right. +@(test) +test_content_inline_image_data_containing_EI :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + // Four grey samples that spell " EI ", followed by the real EI. + src := "BI /W 4 /H 1 /CS /G /BPC 8 ID EI EI 0 0 1 1 re f" + testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Image) + if e == nil || e.img == nil { + testing.expect(t, false, "the image ended at the EI inside its own data") + return + } + testing.expect_value(t, e.img.width, 4) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + testing.expect_value(t, len(doc.repairs), before) +} + +// Exactly one whitespace byte separates ID from the data (8.9.7). Consuming +// none, or two, shifts every sample by a byte -- which the length arithmetic +// then papers over, so only the pixels show it. +@(test) +test_content_inline_image_ID_separator :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // One mid-grey sample. A shift by one takes the separator space (0x20) + // for the pixel instead. + testing.expect_value(t, ct_run(doc, &rec, + "BI /W 1 /H 1 /CS /G /BPC 8 ID \x80 EI", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Image) + if e == nil || e.img == nil { + testing.expect(t, false, "the inline image did not load") + return + } + bmp, derr := image_decode(doc, e.img) + testing.expect_value(t, derr, Error.None) + if bmp == nil || len(bmp.pixels) < 1 { + testing.expect(t, false, "the inline image did not decode") + return + } + testing.expect_value(t, bmp.pixels[0], u8(0x80)) +} + +@(test) +test_content_inline_image_filtered :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // Filtered data has no computable length, so the end is scanned for: the + // EI must be preceded by whitespace and followed by whitespace or the end. + src := "BI /W 2 /H 2 /CS /G /BPC 8 /F /AHx ID 004080ff> EI 0 0 1 1 re f" + testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Image) + if e == nil || e.img == nil { + testing.expect(t, false, "the filtered inline image did not load") + return + } + testing.expect_value(t, e.img.width, 2) + bmp, derr := image_decode(doc, e.img) + testing.expect_value(t, derr, Error.None) + testing.expect(t, bmp != nil, "the hex data decoded") + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_inline_image_explicit_length :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // PDF 2.0's /L removes the guesswork even for filtered data. + src := "BI /W 2 /H 2 /CS /G /BPC 8 /F /AHx /L 9 ID 004080ff> EI 0 0 1 1 re f" + testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Image), 1) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_inline_image_truncated :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // No EI at all. The run has to end rather than spin, and the damage has + // to be recorded. + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, + "0 0 1 1 re f BI /W 2 /H 2 /CS /G /BPC 8 ID \x00\x40", ct_res(doc)), Error.None) + testing.expect(t, len(doc.repairs) > before, "a truncated inline image is a repair") + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_inline_image_no_ID :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // A BI whose dictionary never reaches ID must not eat the rest of the page. + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "BI /W 2 /H 2 EI 0 0 1 1 re f", ct_res(doc)), Error.None) + testing.expect(t, len(doc.repairs) > before, "") + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// --------------------------------------------------------------------------- +// Shadings and patterns +// --------------------------------------------------------------------------- + +@(test) +test_content_sh_paints_through_the_current_clip :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // `sh` has no bounding box and no matrix of its own: it paints under the + // live CTM, over whatever the clip allows. + testing.expect_value(t, ct_run(doc, &rec, "2 0 0 2 9 9 cm /Sh sh", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Shade) + if e == nil || e.shade == nil { + testing.expect(t, false, "sh did not reach the device") + return + } + testing.expect_value(t, e.shade.kind, Shading_Kind.Axial) + ct_near(t, e.ctm.a, 2, "sh uses the current CTM") + ct_near(t, e.ctm.e, 9, "sh uses the current CTM") +} + +@(test) +test_content_ignore_shade_hint :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.dev.hints = {.Ignore_Shade} + + testing.expect_value(t, ct_run(doc, &rec, "/Sh sh 0 0 1 1 re f", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Shade), 0) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// A shading pattern paints through the shape as a clip, with the pattern +// matrix anchored to the stream's default space -- NOT to the CTM of the fill. +@(test) +test_content_shading_pattern_fill :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/Pattern cs /Shade scn 0 0 10 10 re f", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Fill_Shade, .Pop_Clip}), + "a shading pattern is a shading clipped to the shape") + e := rec_nth(&rec, .Fill_Shade) + if e == nil { return } + ct_point(t, transform_point(e.ctm, Point{0, 0}), 3, 4, "the pattern matrix") +} + +// Pattern space is anchored to the page, not to the fill: the same pattern +// under a different CTM must land on exactly the same lattice, or the texture +// slides around under a transformed shape. +@(test) +test_content_pattern_space_ignores_the_fill_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/Pattern cs /Shade scn 0 0 10 10 re f " + + "q 5 0 0 5 100 100 cm /Pattern cs /Shade scn 0 0 10 10 re f Q", ct_res(doc)), Error.None) + a := rec_nth(&rec, .Fill_Shade, 0) + b := rec_nth(&rec, .Fill_Shade, 1) + if a == nil || b == nil { + testing.expect(t, false, "both patterned fills must paint") + return + } + testing.expect(t, a.ctm == b.ctm, "the pattern matrix must not follow the CTM of the fill") +} + +@(test) +test_content_tiling_pattern_device_tiles :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.tiles = 0 // "I will place the copies; run the cell once" + + testing.expect_value(t, ct_run(doc, &rec, + "/Pattern cs /Tile scn 0 0 20 20 re f", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{ + .Clip_Path, // the shape + .Begin_Tile, + .Clip_Path, // the cell's BBox + .Fill_Path, // the cell's content + .Pop_Clip, + .End_Tile, + .Pop_Clip, + }), "a device that tiles gets the cell exactly once") + + bt := rec_nth(&rec, .Begin_Tile) + if bt == nil { return } + ct_near(t, bt.xstep, 10, "xstep") + ct_near(t, bt.ystep, 10, "ystep") + ct_point(t, Point{bt.view.x1, bt.view.y1}, 10, 10, "the cell's bbox") + ct_point(t, Point{bt.area.x1, bt.area.y1}, 20, 20, "the area to cover") +} + +@(test) +test_content_tiling_pattern_interpreter_places_cells :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.tiles = 4 // "I cannot tile; place four copies for me" + + testing.expect_value(t, ct_run(doc, &rec, + "/Pattern cs /Tile scn 0 0 20 20 re f", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Begin_Tile), 1) + testing.expect_value(t, rec_count(&rec, .End_Tile), 1) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 4) + // Every cell is placed by a translation of the same matrix, so no two + // land on top of one another. + a := rec_nth(&rec, .Fill_Path, 0) + b := rec_nth(&rec, .Fill_Path, 1) + if a == nil || b == nil { return } + ct_near(t, a.ctm.a, 1, "cells share the pattern's scale") + ct_near(t, b.ctm.a, 1, "cells share the pattern's scale") + testing.expect(t, a.ctm.e != b.ctm.e || a.ctm.f != b.ctm.f, "cells must not coincide") +} + +// /PaintType 2: the cell is a stencil, and every colour operator inside it is +// ignored in favour of the colour given with the pattern name. +@(test) +test_content_uncoloured_tiling_pattern_takes_the_callers_colour :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.tiles = 1 + + testing.expect_value(t, ct_run(doc, &rec, + "/PatRGB cs 0 1 0 /Uncol scn 0 0 20 20 re f", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expect(t, false, "the uncoloured cell did not paint") + return + } + // The cell says `1 0 0 rg`; the caller said green. + ct_near(t, e.paint.rgb[0], 0, "the cell's own red must be ignored", 1e-6) + ct_near(t, e.paint.rgb[1], 1, "the cell paints in the caller's colour", 1e-6) +} + +@(test) +test_content_pattern_without_a_name_paints_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // A Pattern colour space with no pattern selected has nothing to paint + // with, and must not fall back to black. + testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs 0 0 10 10 re f", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) + testing.expect_value(t, rec_count(&rec, .Fill_Shade), 0) +} + +// --------------------------------------------------------------------------- +// Soft masks +// --------------------------------------------------------------------------- + +@(test) +test_content_soft_mask_is_rendered_at_install_time :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, "/GMask gs 0 0 1 1 re f", ct_res(doc)), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{ + .Begin_Mask, .Clip_Path, .Fill_Path, .Pop_Clip, .End_Mask, .Fill_Path, + }), "the mask's group is drawn between begin_mask and end_mask") + + bm := rec_nth(&rec, .Begin_Mask) + painted := rec_nth(&rec, .Fill_Path, 1) + if bm == nil || painted == nil { return } + testing.expect(t, bm.luminosity, "/S /Luminosity") + ct_point(t, Point{bm.area.x1, bm.area.y1}, 20, 20, "the mask's area is its BBox") + testing.expect(t, painted.has_smask, "the paint carries the installed mask") + if painted.has_smask { + testing.expect(t, painted.smask.luminosity, "") + } +} + +@(test) +test_content_smask_none_clears_it :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/GMask gs /GNone gs 0 0 1 1 re f", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Path, 1) + if e == nil { return } + testing.expect(t, e.paint.soft_mask == nil, "/SMask /None must clear the mask") +} + +@(test) +test_content_soft_mask_is_restored_by_Q :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "q /GMask gs Q 0 0 1 1 re f", ct_res(doc)), Error.None) + // The last fill is the page's, after the mask went out of scope. + e := rec_nth(&rec, .Fill_Path, 1) + if e == nil { return } + testing.expect(t, e.paint.soft_mask == nil, "Q must take the soft mask with it") +} + +// --------------------------------------------------------------------------- +// Marked content and optional content +// --------------------------------------------------------------------------- + +// ct_optional_content adds an /OCProperties to the catalog with `off` switched +// off, and returns resources whose /Properties name both groups. +@(private = "file") +ct_optional_content :: proc(doc: ^Document, res: ^Dict) -> (on_ocg, off_ocg: ^Dict) { + on_ocg, _ = to_dict(cf_parse(doc, "<< /Type /OCG /Name (Visible) >>")) + off_ocg, _ = to_dict(cf_parse(doc, "<< /Type /OCG /Name (Hidden) >>")) + + props := new_dict(doc.allocator) + dict_set(props, "L1", on_ocg) + dict_set(props, "L2", off_ocg) + dict_set(res, "Properties", props) + + off_list := new_array(doc.allocator) + array_push(off_list, off_ocg) + cfg := new_dict(doc.allocator) + dict_set(cfg, "OFF", off_list) + ocp := new_dict(doc.allocator) + dict_set(ocp, "D", cfg) + dict_set(doc.catalog, "OCProperties", ocp) + return +} + +@(test) +test_content_bmc_emc_do_not_disturb_drawing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/Span BMC 0 0 1 1 re f EMC /P << /MCID 0 >> BDC 0 0 1 1 re f EMC"), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) + testing.expect_value(t, rec_count(&rec, .Begin_Layer), 0) +} + +@(test) +test_content_visible_layer_brackets_its_content :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC 0 0 1 1 re f EMC", res), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), "") + e := rec_nth(&rec, .Begin_Layer) + if e == nil { return } + testing.expect_value(t, e.name, "Visible") +} + +@(test) +test_content_hidden_layer_is_skipped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L2 BDC 0 0 1 1 re f /Im Do BT /F1 10 Tf (A) Tj ET EMC 0 0 1 1 re f", res), Error.None) + // Only the fill after EMC survives. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) + testing.expect_value(t, rec_count(&rec, .Fill_Text), 0) + testing.expect_value(t, rec_count(&rec, .Begin_Layer), 0) +} + +// The hidden region ends at the MATCHING EMC, so a nested section inside it +// must not turn drawing back on. +@(test) +test_content_hidden_layer_nesting :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L2 BDC /Span BMC 0 0 1 1 re f EMC 0 0 1 1 re f EMC 0 0 1 1 re f", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_optional_content_can_be_ignored :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + // Hidden text is still text: an extractor asks for all of it. + err := ct_run(doc, &rec, "/OC /L2 BDC 0 0 1 1 re f EMC", res, MATRIX_IDENTITY, + Content_Options{ignore_optional_content = true}) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_stray_EMC_is_ignored :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "EMC EMC 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "a stray EMC is a repair") + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// A layer left open at the end of the stream still has to be closed, or the +// device's layer stack never unwinds. +@(test) +test_content_unclosed_layer_is_closed_at_the_end :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC 0 0 1 1 re f", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Begin_Layer), 1) + testing.expect_value(t, rec_count(&rec, .End_Layer), 1) +} + +// --------------------------------------------------------------------------- +// Robustness +// +// Content streams are the most attacker-controlled data in a PDF and the most +// malformed. Nothing below may abort a page. +// --------------------------------------------------------------------------- + +@(test) +test_content_unknown_operator_is_skipped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, + "0 0 1 1 re f /Foo 1 2 (x) frobnicate 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "an unknown operator is a repair") + // It takes its operands with it, so the next operator starts clean. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) +} + +@(test) +test_content_BX_EX_permits_unknown_operators :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, + "BX 1 2 frobnicate ] >> EX 0 0 1 1 re f"), Error.None) + testing.expect_value(t, len(doc.repairs), before) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + + // The same operator outside the bracket is damage again. + testing.expect_value(t, ct_run(doc, &rec, "frobnicate"), Error.None) + testing.expect(t, len(doc.repairs) > before, "EX must end the compatibility section") +} + +@(test) +test_content_truncated_stream_ends_cleanly :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // Ends inside a text object, inside a q, with a clip open and an operator + // half-written. + testing.expect_value(t, ct_run(doc, &rec, + "q 0 0 10 10 re W n 0 0 1 1 re f BT /F1 10 Tf 7 Tr (A) T", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + // Every clip pushed is popped, including the text clip the missing ET owed. + pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Text) + testing.expect_value(t, rec_count(&rec, .Pop_Clip), pushes) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, kinds[len(kinds) - 1] == .Pop_Clip, "the clip stack unwinds last") +} + +@(test) +test_content_unclosed_q_unwinds :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "q q q 0 0 10 10 re W n 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "an unclosed q is a repair") + testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) + testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) +} + +// The operand stack is fixed, and an operator reads its arguments from the +// END of it, so a stream that pushes hundreds still runs with the right ones. +@(test) +test_content_operand_stack_keeps_the_newest :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + b := strings.builder_make() + defer strings.builder_destroy(&b) + for i in 1 ..= MAX_CONTENT_OPERANDS + 8 { + strings.write_int(&b, i) + strings.write_byte(&b, ' ') + } + strings.write_string(&b, "re f") + + testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b)), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil || len(e.points) < 1 { + testing.expect(t, false, "the overloaded operator did not run") + return + } + last :: MAX_CONTENT_OPERANDS + 8 + ct_point(t, e.points[0], f64(last - 3), f64(last - 2), "the last four operands are the ones used") +} + +@(test) +test_content_too_few_operands_is_skipped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + before := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, "1 2 re f 0 0 1 1 re f"), Error.None) + testing.expect(t, len(doc.repairs) > before, "a short operator is a repair") + // The `f` still runs, on an empty path, and the next rectangle is intact. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + e := rec_nth(&rec, .Fill_Path) + if e == nil || len(e.points) < 1 { return } + ct_point(t, e.points[0], 0, 0, "the following operator is unaffected") +} + +@(test) +test_content_fuel_bounds_the_run :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + b := strings.builder_make() + defer strings.builder_destroy(&b) + for _ in 0 ..< 200 { + strings.write_string(&b, "0 0 1 1 re f ") + } + // Five rectangles is 30 tokens; a budget of 40 must stop well short of 200. + err := ct_run(doc, &rec, strings.to_string(b), nil, MATRIX_IDENTITY, + Content_Options{max_ops = 40}) + testing.expect_value(t, err, Error.None) + n := rec_count(&rec, .Fill_Path) + testing.expect(t, n > 0 && n < 20, "the budget must bound the run without emptying it") +} + +@(test) +test_content_nil_device_and_empty_input :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A run with no sink executes everything and draws nothing. It is how the + // interpreter is measured, and it must not crash on a nil device. + testing.expect_value(t, run_content(doc, nil, + transmute([]byte)string("q 1 0 0 1 2 2 cm 0 0 1 1 re W n /Im Do Q BT /F1 9 Tf (hi) Tj ET"), + ct_res(doc), MATRIX_IDENTITY), Error.None) + + testing.expect_value(t, run_content(doc, nil, nil, nil, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, run_content(nil, nil, transmute([]byte)string("f"), nil, MATRIX_IDENTITY), + Error.Invalid_Argument) + + bad := Matrix{1, 0, 0, 1, 0, math.INF_F64} + testing.expect_value(t, run_content(doc, nil, transmute([]byte)string("f"), nil, bad), + Error.Invalid_Argument) +} + +@(test) +test_content_mutually_recursive_forms_terminate :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + xobj := dict_dict(doc, res, "XObject") + dict_set(xobj, "Ping", cf_stream(doc, + "<< /Subtype /Form /BBox [0 0 10 10] >>", transmute([]byte)string("/Pong Do"))) + dict_set(xobj, "Pong", cf_stream(doc, + "<< /Subtype /Form /BBox [0 0 10 10] >>", transmute([]byte)string("/Ping Do 0 0 1 1 re f"))) + + testing.expect_value(t, ct_run(doc, &rec, "/Ping Do", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + testing.expect_value(t, rec_count(&rec, .Clip_Path), rec_count(&rec, .Pop_Clip)) +} + +@(test) +test_content_type3_glyph_that_shows_itself_terminates :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + // A glyph description that shows its own character through its own font. + t3, _ := to_dict(dict_get(doc, dict_dict(doc, res, "Font"), "T3")) + procs := dict_dict(doc, t3, "CharProcs") + dict_set(procs, "square", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 d0 0 0 100 100 re f BT /T3 10 Tf (A) Tj ET"))) + t3res := new_dict(doc.allocator) + dict_set(t3res, "Font", dict_get_raw(res, "Font")) + dict_set(t3, "Resources", t3res) + + testing.expect_value(t, ct_run(doc, &rec, "BT /T3 10 Tf (A) Tj ET", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +@(test) +test_content_d0_d1_outside_a_glyph_are_harmless :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // d1 declares a stencil, and outside a Type 3 glyph description that + // would silently swallow every colour operator on the page. + testing.expect_value(t, ct_run(doc, &rec, + "100 0 0 0 100 100 d1 1 0 0 rg 0 0 1 1 re f"), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { return } + ct_near(t, e.paint.rgb[0], 1, "colour still works after a stray d1", 1e-6) + testing.expect_value(t, rec_count(&rec, .Clip_Path), 0) +} + +// Inside a glyph description d1 does take effect: the whole glyph paints in +// the colour that was current when it was shown. +@(test) +test_content_d1_ignores_colour_inside_a_glyph :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + t3, _ := to_dict(dict_get(doc, dict_dict(doc, res, "Font"), "T3")) + procs := dict_dict(doc, t3, "CharProcs") + dict_set(procs, "square", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 0 0 100 100 d1 0 0 1 rg 0 0 100 100 re f"))) + + testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg BT /T3 10 Tf (A) Tj ET", res), Error.None) + e := rec_nth(&rec, .Fill_Path) + if e == nil { + testing.expect(t, false, "the glyph did not draw") + return + } + ct_near(t, e.paint.rgb[0], 1, "d1 keeps the caller's colour", 1e-6) + ct_near(t, e.paint.rgb[2], 0, "the glyph's own blue must be ignored", 1e-6) + // The declared bounding box clips the glyph. + testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) + testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) +} + +@(test) +test_content_text_operators_outside_BT :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + // Producers omit BT. The text matrix starts as the identity either way, + // so showing text still works rather than being dropped. + testing.expect_value(t, ct_run(doc, &rec, + "/F1 10 Tf 5 5 Td (A) Tj ET", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Text) + if e == nil { + testing.expect(t, false, "text without BT must still draw") + return + } + ct_point(t, ct_origin(e.items, 0), 5, 5, "") +} + +@(test) +test_content_junk_bytes_do_not_stop_the_page :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "0 0 1 1 re f ] } ) >> << /A 1 >> [1 2 3] % a comment\n 0 0 1 1 re f"), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) +} + +// Truncating a rich stream at every offset must never crash, hang or leave the +// device's clip stack unbalanced. Reaching the end of this test is the property. +@(test) +test_content_truncation_fuzz :: proc(t: ^testing.T) { + SRC :: "q 2 0 0 2 1 1 cm /G1 gs 0 0 10 10 re W n 1 0 0 rg /Pattern cs /Tile scn " + + "0 0 20 20 re f BT /F1 10 Tf 1 Tc 2 Tw 120 Tz 3 Ts 4 Tr [(AB) -300 (C)] TJ ET " + + "/Sh sh /Box Do /Im Do BI /W 2 /H 2 /CS /G /BPC 8 ID \x01\x02\x03\x04 EI " + + "/OC /L1 BDC 0 0 1 1 re f EMC Q" + + src := SRC + for cut in 0 ..= len(src) { + doc := cf_open(t) + rec: Recorder + rec_init(&rec) + + err := ct_run(doc, &rec, src[:cut], ct_res(doc)) + testing.expectf(t, err == .None, "truncation at %d returned %v", cut, err) + pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Stroke_Path) + + rec_count(&rec, .Clip_Text) + rec_count(&rec, .Clip_Image_Mask) + testing.expectf(t, pushes == rec_count(&rec, .Pop_Clip), + "truncation at %d left the clip stack unbalanced", cut) + testing.expectf(t, rec_count(&rec, .Begin_Layer) == rec_count(&rec, .End_Layer), + "truncation at %d left a layer open", cut) + testing.expectf(t, rec_count(&rec, .Begin_Tile) == rec_count(&rec, .End_Tile), + "truncation at %d left a tile open", cut) + testing.expectf(t, rec_count(&rec, .Begin_Group) == rec_count(&rec, .End_Group), + "truncation at %d left a group open", cut) + + rec_destroy(&rec) + cf_close(t, doc) + } + testing.expect(t, true, "every truncation was survived") +} + +// The same, byte by byte: one flipped byte anywhere must not change that. +@(test) +test_content_bit_flip_fuzz :: proc(t: ^testing.T) { + SRC :: "q 1 0 0 1 2 2 cm 0 0 10 10 re W n BT /F1 12 Tf (hello) Tj ET " + + "/Box Do BI /W 2 /H 2 /CS /G /BPC 8 ID \x01\x02\x03\x04 EI Q" + + buf := make([]byte, len(SRC)) + defer delete(buf) + for i in 0 ..< len(SRC) { + for bit in 0 ..< 3 { + copy(buf, transmute([]byte)string(SRC)) + buf[i] ~= 1 << u8(bit * 3) + + doc := cf_open(t) + rec: Recorder + rec_init(&rec) + + err := run_content(doc, &rec.dev, buf, ct_res(doc), MATRIX_IDENTITY) + testing.expectf(t, err == .None, "flip at %d/%d returned %v", i, bit, err) + pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Stroke_Path) + + rec_count(&rec, .Clip_Text) + rec_count(&rec, .Clip_Image_Mask) + testing.expectf(t, pushes == rec_count(&rec, .Pop_Clip), + "flip at %d/%d left the clip stack unbalanced", i, bit) + + rec_destroy(&rec) + cf_close(t, doc) + } + } + testing.expect(t, true, "every single-bit flip was survived") +} + +// A tiling pattern replays its cell thousands of times, and every replay is a +// fresh run of the interpreter. Scratch memory has to be reused between them: +// the run's allocator is an arena and reclaims nothing until the end, so a +// runner that allocated its own operand stack per cell would cost tens of +// megabytes for one fill. +@(test) +test_content_scratch_memory_is_reused_between_runs :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + rec.tiles = 4096 + + arena: virtual.Arena + if aerr := virtual.arena_init_growing(&arena); aerr != nil { + testing.expect(t, false, "could not make an arena") + return + } + defer virtual.arena_destroy(&arena) + + err := ct_run(doc, &rec, "/Pattern cs /Tile scn 0 0 2000 2000 re f", ct_res(doc), + MATRIX_IDENTITY, Content_Options{allocator = virtual.arena_allocator(&arena)}) + testing.expect_value(t, err, Error.None) + cells := rec_count(&rec, .Fill_Path) + testing.expect(t, cells > 1000, "the cell must actually have been replayed") + used := int(arena.total_used) + testing.expectf(t, used < 4 << 20, "%d bytes of scratch for %d cells", used, cells) +} + +// --------------------------------------------------------------------------- +// Corpus +// +// The interpreter driven over every page of every real PDF in the directory +// named by PDF_ODIN_CORPUS, and skipped silently when that is unset. There is +// no correct answer to compare a real page against -- that is what the tests +// above are for -- so what is asserted here is the contract the three devices +// will be written against: the run terminates, it never returns an error, and +// the clip, layer, group and tile stacks it hands the device are balanced when +// it does. That is exactly the class of bug a corpus finds and a unit test +// cannot. +// --------------------------------------------------------------------------- + +@(private = "file") +CC_MAX_FILE :: 64 << 20 + +@(private = "file") +CC_MAX_DEPTH :: 8 + +@(private = "file") +Cc_Dev :: struct { + dev: Device, + // Live nesting, so an unbalanced pop is caught where it happens. + clip: int, + layer: int, + group: int, + tile: int, + mask: int, + // The worst each reached, and whether anything ever went negative. + negative: bool, + // Totals, reported at the end so a silent no-op run cannot pass for a + // successful one. + fills, strokes, texts, glyphs, images, shades: int, +} + +@(private = "file") +cc :: proc(dev: ^Device) -> ^Cc_Dev { return (^Cc_Dev)(dev.user) } + +@(private = "file") +cc_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + cc(dev).fills += 1 +} +@(private = "file") +cc_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + cc(dev).strokes += 1 +} +@(private = "file") +cc_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { + cc(dev).clip += 1 +} +@(private = "file") +cc_clip_stroke :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { + cc(dev).clip += 1 +} +@(private = "file") +cc_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + cc(dev).clip += 1 +} +@(private = "file") +cc_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + cc(dev).clip += 1 +} +@(private = "file") +cc_pop_clip :: proc(dev: ^Device) { + d := cc(dev) + d.clip -= 1 + if d.clip < 0 { d.negative = true } +} +@(private = "file") +cc_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { + d := cc(dev) + d.texts += 1 + d.glyphs += len(txt.items) +} +@(private = "file") +cc_stroke_text :: proc(dev: ^Device, txt: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + cc(dev).texts += 1 +} +@(private = "file") +cc_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { + d := cc(dev) + d.texts += 1 + d.glyphs += len(txt.items) +} +@(private = "file") +cc_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + cc(dev).images += 1 +} +@(private = "file") +cc_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { + cc(dev).shades += 1 +} +@(private = "file") +cc_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { + cc(dev).mask += 1 +} +@(private = "file") +cc_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { + d := cc(dev) + d.mask -= 1 + if d.mask < 0 { d.negative = true } +} +@(private = "file") +cc_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + cc(dev).group += 1 +} +@(private = "file") +cc_end_group :: proc(dev: ^Device) { + d := cc(dev) + d.group -= 1 + if d.group < 0 { d.negative = true } +} +@(private = "file") +cc_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { + cc(dev).tile += 1 + return 0 +} +@(private = "file") +cc_end_tile :: proc(dev: ^Device) { + d := cc(dev) + d.tile -= 1 + if d.tile < 0 { d.negative = true } +} +@(private = "file") +cc_begin_layer :: proc(dev: ^Device, name: string) { + cc(dev).layer += 1 +} +@(private = "file") +cc_end_layer :: proc(dev: ^Device) { + d := cc(dev) + d.layer -= 1 + if d.layer < 0 { d.negative = true } +} + +@(private = "file") +cc_init :: proc(d: ^Cc_Dev) { + d.dev = Device{ + user = d, + fill_path = cc_fill_path, + stroke_path = cc_stroke_path, + clip_path = cc_clip_path, + clip_stroke_path = cc_clip_stroke, + clip_text = cc_clip_text, + clip_image_mask = cc_clip_image_mask, + pop_clip = cc_pop_clip, + fill_text = cc_text, + stroke_text = cc_stroke_text, + ignore_text = cc_ignore_text, + fill_image = cc_image, + fill_image_mask = cc_image, + fill_shade = cc_shade, + begin_mask = cc_begin_mask, + end_mask = cc_end_mask, + begin_group = cc_begin_group, + end_group = cc_end_group, + begin_tile = cc_begin_tile, + end_tile = cc_end_tile, + begin_layer = cc_begin_layer, + end_layer = cc_end_layer, + } +} + +@(private = "file") +cc_corpus_dir :: proc(allocator := context.allocator) -> string { + raw := os.get_env("PDF_ODIN_CORPUS", allocator) + trimmed := strings.trim_space(raw) + out := strings.clone(trimmed, allocator) + delete(raw, allocator) + return out +} + +@(private = "file") +cc_walk :: proc(dir: string, out: ^[dynamic]string, depth := 0) { + if depth > CC_MAX_DEPTH { return } + entries, err := os.read_directory_by_path(dir, -1, context.allocator) + if err != nil { return } + defer os.file_info_slice_delete(entries, context.allocator) + slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { return a.name < b.name }) + for e in entries { + if e.type == .Directory { + cc_walk(e.fullpath, out, depth + 1) + continue + } + if e.type != .Regular || e.size > CC_MAX_FILE { continue } + lower := strings.to_lower(e.name, context.temp_allocator) + if !strings.has_suffix(lower, ".pdf") { continue } + append(out, strings.clone(e.fullpath)) + } +} + +@(test) +test_content_corpus :: proc(t: ^testing.T) { + dir := cc_corpus_dir() + defer delete(dir) + if dir == "" { + log.info("PDF_ODIN_CORPUS is unset; skipping the content corpus test") + return + } + if !os.is_dir(dir) { + log.infof("PDF_ODIN_CORPUS is %q, which is not a directory; skipping", dir) + return + } + + paths := make([dynamic]string) + defer { + for path in paths { delete(path) } + delete(paths) + } + cc_walk(dir, &paths) + if len(paths) == 0 { + log.infof("no PDFs under %q; skipping", dir) + return + } + + // One arena for the whole sweep, reset between pages. That is how a + // renderer is meant to drive the interpreter, and it is the difference + // between a 900-page document costing one page's scratch and 900. + arena: virtual.Arena + if aerr := virtual.arena_init_growing(&arena); aerr != nil { + testing.expect(t, false, "could not make a scratch arena") + return + } + defer virtual.arena_destroy(&arena) + opts := Content_Options{allocator = virtual.arena_allocator(&arena)} + + total: Cc_Dev + files, pages, peak := 0, 0, 0 + for path in paths { + doc, err := open_file(path) + if err != .None || doc == nil { continue } + defer close(doc) + files += 1 + + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if perr != .None { continue } + data, cerr := page_content(&page, context.allocator) + defer delete(data) + if cerr != .None || len(data) == 0 { continue } + + dev: Cc_Dev + cc_init(&dev) + // The page flip a renderer would use, so the geometry is real. + flip := Matrix{1, 0, 0, -1, -page.media_box.x0, page.media_box.y1} + rerr := run_content(doc, &dev.dev, data, page.resources, flip, opts) + + name := filepath.base(path) + testing.expectf(t, rerr == .None, "%s page %d: run_content returned %v", name, i, rerr) + testing.expectf(t, dev.clip == 0, "%s page %d: %d clips left open", name, i, dev.clip) + testing.expectf(t, !dev.negative, "%s page %d: a device stack went negative", name, i) + testing.expectf(t, dev.layer == 0, "%s page %d: %d layers left open", name, i, dev.layer) + testing.expectf(t, dev.group == 0, "%s page %d: %d groups left open", name, i, dev.group) + testing.expectf(t, dev.tile == 0, "%s page %d: %d tiles left open", name, i, dev.tile) + testing.expectf(t, dev.mask == 0, "%s page %d: %d masks left open", name, i, dev.mask) + + total.fills += dev.fills + total.strokes += dev.strokes + total.texts += dev.texts + total.glyphs += dev.glyphs + total.images += dev.images + total.shades += dev.shades + pages += 1 + + peak = max(peak, int(arena.total_used)) + free_all(virtual.arena_allocator(&arena)) + } + } + + log.infof("content corpus: %d files, %d pages, %d fills, %d strokes, %d text runs, " + + "%d glyphs, %d images, %d shadings, peak scratch %d KiB", + files, pages, total.fills, total.strokes, total.texts, + total.glyphs, total.images, total.shades, peak / 1024) + // A sweep that drew nothing would pass every assertion above. + testing.expect(t, total.glyphs > 0, "the corpus produced no glyphs at all") + testing.expect(t, total.fills + total.strokes > 0, "the corpus produced no path painting") +} + +// 8.7.4.3: a shading pattern's own /ExtGState is put into effect while it +// paints, and replaces the compositing parameters rather than compounding +// with them. +@(test) +test_content_shading_pattern_extgstate :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/G1 gs /Pattern cs /ShadeGS scn 0 0 10 10 re f", ct_res(doc)), Error.None) + e := rec_nth(&rec, .Fill_Shade) + if e == nil { + testing.expect(t, false, "the shading pattern did not paint") + return + } + // /G1 set ca 0.5 and Multiply; the pattern's own state says 0.25 and Screen. + ct_near(t, e.paint.alpha, 0.25, "the pattern's ExtGState alpha wins") + testing.expect_value(t, e.paint.blend, Blend_Mode.Screen) +} + +// A BDC that arrives without its two operands still OPENS a section. If it did +// not, the matching EMC would close the ENCLOSING one: the /OC section here +// would end at the first EMC, the fill would stop being hidden, and the rest of +// the outer section would draw. `content_mc_begin` has the machinery for a BDC +// it cannot record -- the `mc_over` counter -- and the short-operand path used +// to return before reaching it. +@(test) +test_content_short_bdc_still_pairs_with_its_emc :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + // The inner BDC is one operand short, so the first EMC must close IT. + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L2 BDC /Foo BDC EMC 0 0 1 1 re f EMC", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) +} + +// The same stream over a VISIBLE layer: the fill belongs inside the layer, so +// end_layer must not arrive before it. +@(test) +test_content_short_bdc_does_not_close_a_layer_early :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L1 BDC /Foo BDC EMC 0 0 1 1 re f EMC", res), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), + "the fill is inside the layer it was written inside") +} + +// The same nesting with a WELL-FORMED inner BDC, which is the behaviour the +// anonymous section above has to imitate: the first EMC closes the INNER +// section and the second the outer one, so the fill between them is still +// inside the hidden section and the one after both of them is not. +@(test) +test_content_nested_bdc_pairs_innermost_first :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L2 BDC /P << /MCID 0 >> BDC EMC 0 0 1 1 re f EMC 0 0 1 1 re f", res), Error.None) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} + +// And over a VISIBLE layer, where the device layer's span is what is at stake: +// the inner EMC must not end it early. +@(test) +test_content_nested_bdc_brackets_a_visible_layer :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := ct_res(doc) + ct_optional_content(doc, res) + testing.expect_value(t, ct_run(doc, &rec, + "/OC /L1 BDC /P << /MCID 0 >> BDC EMC 0 0 1 1 re f EMC", res), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), + "the inner EMC closes the inner section, not the layer") +} + +// 8.10.2 makes /BBox a clip on the form's contents, so a degenerate box means +// nothing is drawn. Skipping the clip because the box has no area meant the +// opposite: the form painted its whole content, unclipped, over the page. +@(test) +test_content_form_with_a_zero_area_bbox_is_clipped_away :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + res := new_dict(doc.allocator) + xo := new_dict(doc.allocator) + dict_set(xo, "Zero", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] >>", + transmute([]byte)string("0 0 20 20 re f"))) + dict_set(xo, "Flat", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 100 0] >>", + transmute([]byte)string("0 0 20 20 re f"))) + dict_set(res, "XObject", xo) + + testing.expect_value(t, ct_run(doc, &rec, "/Zero Do /Flat Do", res), Error.None) + kinds := rec_kinds(&rec) + defer delete(kinds) + testing.expect(t, slice.equal(kinds, []Rec_Kind{ + .Clip_Path, .Fill_Path, .Pop_Clip, + .Clip_Path, .Fill_Path, .Pop_Clip, + }), "a degenerate /BBox is pushed as a clip like any other") +} + +// --------------------------------------------------------------------------- +// What one page may spend +// +// A page is not one run -- the content stream is one and every annotation's +// appearance stream is another -- and the things below are bounded per PAGE +// on the shared state, because a per-run cap would let MAX_ANNOTS of them +// multiply it. Each of these is a small file that used to cost minutes or +// gigabytes. +// --------------------------------------------------------------------------- + +// The same ExtGState applied again under the same CTM is the same mask. +// Rebuilding it costs a slot of the page's budget, an allocation, and -- since +// the device would not recognise the new one -- a whole re-render of the +// mask's group: 3000 `/S gs` measured 57 seconds at 1x. +@(test) +test_content_repeated_gs_renders_one_soft_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + N :: 3000 + b: strings.Builder + strings.builder_init(&b) + defer strings.builder_destroy(&b) + for _ in 0 ..< N { + strings.write_string(&b, "/GMask gs ") + } + strings.write_string(&b, "0 0 1 1 re f") + + start := time.now() + testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) + ms := time.duration_milliseconds(time.since(start)) + + testing.expectf(t, rec_count(&rec, .Begin_Mask) == 1, + "%d gs installed the same mask %d times", N, rec_count(&rec, .Begin_Mask)) + // The first fill is the mask group's own; the second is the page's. + e := rec_nth(&rec, .Fill_Path, 1) + testing.expect(t, e != nil && e.has_smask, "and the mask is still in force") + // Not a performance bound: one re-render per `gs` is 57 seconds. + testing.expectf(t, ms < 5000, "%d gs took %.0f ms", N, ms) +} + +// The cache is keyed by the CTM as well, because the CTM in force when a mask +// is installed is where the mask lands (11.6.5.2). The same ExtGState under a +// different CTM is a different mask and has to be drawn again. +@(test) +test_content_soft_mask_cache_respects_the_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + testing.expect_value(t, ct_run(doc, &rec, + "/GMask gs 1 0 0 1 30 0 cm /GMask gs 0 0 1 1 re f", ct_res(doc)), Error.None) + testing.expect_value(t, rec_count(&rec, .Begin_Mask), 2) + a := rec_nth(&rec, .Begin_Mask, 0) + c := rec_nth(&rec, .Begin_Mask, 1) + if a == nil || c == nil { return } + testing.expectf(t, c.area.x0 - a.area.x0 == 30, + "the second mask is drawn where the second CTM puts it: %v then %v", a.area, c.area) +} + +// A Soft_Mask outlives the operator that installed it -- a `Q` can bring it +// back -- but not the run: it belongs to the interpreter's scratch, which the +// page reclaims. On the Document it would be reclaimed only by `close`, so a +// viewer re-rendering the same page at every zoom step would add a thousand +// of them per render for as long as the file was open. +@(test) +test_content_soft_masks_do_not_grow_the_document :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + b: strings.Builder + strings.builder_init(&b) + defer strings.builder_destroy(&b) + // A different CTM every time, so every one of these is a mask the cache + // above cannot answer and the interpreter has to build. + for _ in 0 ..< MAX_SOFT_MASKS { + strings.write_string(&b, "1 0 0 1 0.1 0 cm /GMask gs ") + } + src := strings.to_string(b) + + run :: proc(t: ^testing.T, doc: ^Document, res: ^Dict, src: string) -> int { + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + testing.expect_value(t, ct_run(doc, &rec, src, res), Error.None) + return rec_count(&rec, .Begin_Mask) + } + + // ct_res builds its dictionaries in the Document arena, so it is built + // once, here, rather than inside what is being measured. The first run + // warms everything the Document legitimately caches with it: the mask + // form's decoded stream, its colour space, its group dictionary. + res := ct_res(doc) + testing.expect_value(t, run(t, doc, res, src), MAX_SOFT_MASKS) + before := document_memory_used(doc) + testing.expect_value(t, run(t, doc, res, src), MAX_SOFT_MASKS) + growth := document_memory_used(doc) - before + + testing.expectf(t, growth < 8192, + "rendering the same content twice added %d bytes of Document memory", growth) +} + +// `BI ID EI` is two tokens, so the token budget alone paid for two million +// inline images from a 37 KB file -- each with a dictionary in the Document +// arena, which only `close` reclaims. Nothing drawable comes out of any of +// them. +@(test) +test_content_empty_inline_images_cost_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + N :: 200_000 + b: strings.Builder + strings.builder_init(&b) + defer strings.builder_destroy(&b) + for _ in 0 ..< N { + strings.write_string(&b, "BI ID EI ") + } + // The stream must still be in step after all of them. + strings.write_string(&b, "0 0 1 1 re f") + + before := document_memory_used(doc) + start := time.now() + testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) + ms := time.duration_milliseconds(time.since(start)) + growth := document_memory_used(doc) - before + + testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) + testing.expectf(t, growth < 1 << 20, "%d inline images added %d bytes", N, growth) + testing.expectf(t, ms < 5000, "%d inline images took %.0f ms", N, ms) +} + +// And the ones that ARE images are counted, because what they cost the +// Document is the same whether they draw or not. A refused one still has to +// consume exactly its own bytes: these carry `EI` inside their sample data -- +// which is why the length is computed and not scanned for -- and drawing +// operators after it, so a resynchronisation that lands anywhere but the end +// paints something the file never asked for. +@(test) +test_content_inline_images_are_capped_per_page :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + rec: Recorder + rec_init(&rec) + defer rec_destroy(&rec) + + PAYLOAD :: " EI 1 0 0 rg 5 5 10 10 re f " + one := fmt.tprintf("BI /W %d /H 1 /BPC 8 /CS /G ID %sEI ", len(PAYLOAD), PAYLOAD) + + N :: MAX_INLINE_IMAGES + 100 + b: strings.Builder + strings.builder_init(&b) + defer strings.builder_destroy(&b) + for _ in 0 ..< N { + strings.write_string(&b, one) + } + strings.write_string(&b, "0 0 1 1 re f") + + repairs := len(doc.repairs) + testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) + + testing.expect_value(t, rec_count(&rec, .Fill_Image), MAX_INLINE_IMAGES) + testing.expect(t, len(doc.repairs) > repairs, "hitting the cap is recorded as a Repair") + // One fill: the marker at the end. Any more came out of an image's pixels. + testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) +} diff --git a/corpus_test.odin b/corpus_test.odin new file mode 100644 index 0000000..c32420b --- /dev/null +++ b/corpus_test.odin @@ -0,0 +1,1190 @@ +package pdf + +import "core:fmt" +import "core:log" +import "core:mem" +import "core:os" +import "core:path/filepath" +import "core:slice" +import "core:strings" +import "core:testing" +import "core:time" +import "core:unicode/utf8" + +// --------------------------------------------------------------------------- +// Corpus tests +// +// These run over whatever PDFs are in the directory named by PDF_ODIN_CORPUS +// and skip silently when it is unset, so the suite never depends on files that +// are not in the repository. +// +// They assert invariants, not results. There is no correct answer to check a +// real-world file against -- that is what the golden files in testdata/ are for +// -- so what is checked here is the contract every file must satisfy: opening +// terminates, whatever comes back is self-consistent, and nothing crashes, +// hangs or leaks. That is precisely the class of bug a corpus finds and a unit +// test cannot. +// --------------------------------------------------------------------------- + +// A file that takes longer than this is logged. It is not a hard timeout -- +// the test runner cannot interrupt a running test -- but a file that trips it +// has found a loop bound that is too generous, and that is worth seeing. +@(private = "file") +CORPUS_SLOW_MS :: 5000 + +// Files larger than this are skipped. The corpus directory is whatever the +// developer happened to point at, and a 500 MB scanned document exercises +// nothing a 5 MB one does not. +@(private = "file") +CORPUS_MAX_FILE :: 64 << 20 + +@(private = "file") +CORPUS_MAX_DEPTH :: 8 + +@(private = "file") +Corpus_Stats :: struct { + files: int, + opened: int, + failed: int, + damaged: int, + pages: int, + skipped: int, +} + +// corpus_dir returns the corpus directory, or "" when the variable is unset. +@(private = "file") +corpus_dir :: proc(allocator := context.allocator) -> string { + raw := os.get_env("PDF_ODIN_CORPUS", allocator) + trimmed := strings.trim_space(raw) + // trim_space returns a slice of `raw`, so the original allocation is still + // the one to free; hand back a clone and release it. + out := strings.clone(trimmed, allocator) + delete(raw, allocator) + return out +} + +// corpus_files lists the PDFs under `dir`, recursively and in a stable order, +// so that two runs report the same files in the same sequence. +@(private = "file") +corpus_files :: proc(dir: string, out: ^[dynamic]string, depth := 0) { + if depth > CORPUS_MAX_DEPTH { return } + + entries, err := os.read_directory_by_path(dir, -1, context.allocator) + if err != nil { return } + defer os.file_info_slice_delete(entries, context.allocator) + + slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { return a.name < b.name }) + + for e in entries { + if e.type == .Directory { + corpus_files(e.fullpath, out, depth + 1) + continue + } + if e.type != .Regular { continue } + + lower := strings.to_lower(e.name, context.temp_allocator) + if !strings.has_suffix(lower, ".pdf") { continue } + append(out, strings.clone(e.fullpath)) + } +} + +@(private = "file") +corpus_collect :: proc(dir: string) -> [dynamic]string { + paths := make([dynamic]string) + corpus_files(dir, &paths) + return paths +} + +@(private = "file") +corpus_release :: proc(paths: ^[dynamic]string) { + for p in paths { delete(p) } + delete(paths^) +} + +@(private = "file") +corpus_size :: proc(path: string) -> i64 { + info, err := os.stat(path, context.allocator) + if err != nil { return 0 } + defer os.file_info_delete(info, context.allocator) + return info.size +} + +// corpus_check_one opens one file and asserts the invariants on it. +// +// Every failure names the file: a corpus run that says only "3 files failed" +// costs more time than it saves. +@(private = "file") +corpus_check_one :: proc(t: ^testing.T, path: string, stats: ^Corpus_Stats) { + if corpus_size(path) > CORPUS_MAX_FILE { + stats.skipped += 1 + return + } + + name := filepath.base(path) + stats.files += 1 + started := time.now() + + doc, err := open_file(path) + defer close(doc) + + if err != .None { + // A file that will not open is not automatically a bug: a corpus + // collected off a disk contains genuinely broken files, and .No_Pages + // on a zero-page document is the right answer. What must never happen + // is a crash or a hang, and reaching this line proves neither did. + stats.failed += 1 + log.infof("corpus: %s: open failed: %s", name, error_string(err)) + return + } + if !testing.expectf(t, doc != nil, "%s: open reported success but gave no document", name) { + return + } + + stats.opened += 1 + if doc.is_damaged { stats.damaged += 1 } + + n := page_count(doc) + testing.expectf(t, n > 0, "%s: opened with %v pages", name, n) + stats.pages += n + + // page_get reads doc.pages and doc.page_refs in lockstep, so a length + // mismatch is an out-of-range read waiting to happen. + testing.expectf(t, len(doc.pages) == len(doc.page_refs), + "%s: %v page dicts against %v page refs", name, len(doc.pages), len(doc.page_refs)) + + for i in 0 ..< n { + page, perr := page_get(doc, i) + if !testing.expectf(t, perr == .None, "%s: page %v: %s", name, i, error_string(perr)) { + continue + } + testing.expectf(t, page.dict != nil, "%s: page %v has no dictionary", name, i) + + // No NaN or infinity may reach the device layer, and a zero-area page + // divides to one. page_get is the boundary that has to guarantee it. + testing.expectf(t, rect_is_finite(page.media_box), + "%s: page %v media box is not finite: %v", name, i, page.media_box) + testing.expectf(t, !rect_is_empty(page.media_box), + "%s: page %v media box is empty: %v", name, i, page.media_box) + testing.expectf(t, rect_is_finite(page.crop_box), + "%s: page %v crop box is not finite: %v", name, i, page.crop_box) + testing.expectf(t, !rect_is_empty(page.crop_box), + "%s: page %v crop box is empty: %v", name, i, page.crop_box) + testing.expectf(t, page.rotate >= 0 && page.rotate < 360 && page.rotate % 90 == 0, + "%s: page %v rotate is %v", name, i, page.rotate) + + corpus_check_contents(t, doc, page, name) + } + + testing.expectf(t, len(doc.repairs) <= MAX_REPAIRS, + "%s: repair list grew past its cap: %v", name, len(doc.repairs)) + + ms := time.duration_milliseconds(time.since(started)) + if ms > CORPUS_SLOW_MS { + log.infof("corpus: %s: slow: %.0f ms for %v pages", name, ms, n) + } +} + +// corpus_check_contents decodes a page's content streams. A stream that will +// not decode is not a test failure -- plenty of real files hold one, and the +// reader is required to degrade rather than refuse -- so what is checked is +// the contract `stream_data` states, which no file is allowed to break. +@(private = "file") +corpus_check_contents :: proc(t: ^testing.T, doc: ^Document, page: Page, name: string) { + contents := dict_get(doc, page.dict, "Contents") + + #partial switch v in contents { + case ^Stream: + corpus_check_stream(t, doc, v, name) + case ^Array: + // A /Contents array in a real file has a handful of entries; one with + // thousands is a file trying to make this loop expensive. + limit := min(array_len(v), 4096) + for i in 0 ..< limit { + s, ok := to_stream(array_get(doc, v, i)) + if !ok { continue } + corpus_check_stream(t, doc, s, name) + } + } +} + +// corpus_check_stream holds `stream_data` to its documented contract over real +// data. +// +// Three properties, all of which a malformed file could plausibly break: +// decoding a stream never reports an error (the error return is reserved for a +// caller mistake -- a broken filter chain degrades to empty plus a repair); a +// call always leaves the stream decided, never still .Pending; and the result +// is cached, so the second call hands back the identical bytes rather than +// decoding again. The last one is the reason nothing in the library frees what +// this returns, and it is checked by pointer, not by length. +@(private = "file") +corpus_check_stream :: proc(t: ^testing.T, doc: ^Document, s: ^Stream, name: string) { + if s == nil { return } + + data, err := stream_data(doc, s) + testing.expectf(t, err == .None, "%s: stream_data returned %s", name, error_string(err)) + testing.expectf(t, s.state != .Pending, "%s: stream_data left the stream undecoded", name) + if s.state == .Failed { + testing.expectf(t, data == nil, + "%s: a stream that failed to decode returned %v bytes", name, len(data)) + testing.expectf(t, doc.is_damaged, + "%s: a stream failed to decode without recording a repair", name) + } + + again, aerr := stream_data(doc, s) + testing.expectf(t, aerr == .None, "%s: second stream_data returned %s", name, error_string(aerr)) + testing.expectf(t, raw_data(again) == raw_data(data) && len(again) == len(data), + "%s: stream_data decoded the same stream twice: %v bytes, then %v bytes", + name, len(data), len(again)) +} + +// --------------------------------------------------------------------------- + +@(test) +test_corpus :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" { + log.info("PDF_ODIN_CORPUS is unset; skipping the corpus test") + return + } + if !os.is_dir(dir) { + log.infof("PDF_ODIN_CORPUS is %q, which is not a directory; skipping", dir) + return + } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + + if len(paths) == 0 { + log.infof("no PDFs under %q; skipping", dir) + return + } + + stats: Corpus_Stats + for p in paths { + corpus_check_one(t, p, &stats) + free_all(context.temp_allocator) + } + + log.infof("corpus %q: %v files, %v opened, %v failed, %v damaged, %v skipped, %v pages", + dir, stats.files, stats.opened, stats.failed, stats.damaged, stats.skipped, stats.pages) + + // The corpus is allowed to contain broken files, but a run in which most of + // them fail to open means the library regressed, not that the disk filled + // up with rubbish overnight. + if stats.files > 0 { + ratio := f64(stats.opened) / f64(stats.files) + testing.expectf(t, ratio >= 0.5, "only %v of %v corpus files opened (%.0f%%)", + stats.opened, stats.files, ratio * 100) + } +} + +// A second pass that re-opens every file with the page tree skipped and +// resolves every object in it. This is the broadest reachability test +// available: it drives the parser, the object streams, the decryption and the +// codecs over data no unit test invents. +@(test) +test_corpus_objects :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" || !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + + checked, streams := 0, 0 + for p in paths { + if corpus_size(p) > CORPUS_MAX_FILE { continue } + + doc, err := open_file(p, Open_Options{skip_page_tree = true}) + defer close(doc) + if err != .None || doc == nil { continue } + checked += 1 + + // The cross-reference table is what `get_object` indexes and what + // `objstm_load` trusts, so its shape is a memory-safety property + // rather than a cosmetic one, and every field in it came out of the + // file. `count >= 0` stood here and could not fail; these are the + // bounds the loader actually enforces against an attacker-controlled + // /Size and attacker-controlled offsets. + name := filepath.base(p) + count := object_count(doc) + testing.expectf(t, count <= xref_max_objects(doc), + "%s: %v xref entries for a %v byte file", name, count, len(doc.data)) + + for num in 1 ..< count { + e := doc.xref[num] + #partial switch e.kind { + case .Normal: + testing.expectf(t, e.offset > 0 && e.offset < i64(len(doc.data)), + "%s: object %v is at offset %v of a %v byte file", + name, num, e.offset, len(doc.data)) + case .Compressed: + testing.expectf(t, e.container != 0 && e.container <= MAX_OBJECT_NUMBER, + "%s: object %v is compressed into container %v", name, num, e.container) + } + + obj := get_object(doc, Ref{u32(num), e.gen}) + if s, ok := to_stream(obj); ok { + corpus_check_stream(t, doc, s, name) + streams += 1 + } + } + free_all(context.temp_allocator) + } + + if checked > 0 { + log.infof("corpus objects: walked %v files, decoded %v streams", checked, streams) + } +} + +// --------------------------------------------------------------------------- +// Fuzz-shaped tests over the corpus +// +// Truncating or corrupting a real file is the cheapest way to produce a +// malformed one that is still realistic enough to reach code a hand-written +// case never does. +// --------------------------------------------------------------------------- + +@(test) +test_corpus_truncated :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" || !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + + // A handful of files is enough: this looks for crashes in the recovery + // path, and every file exercises the same path. + limit := min(len(paths), 12) + for i in 0 ..< limit { + data, rerr := os.read_entire_file(paths[i], context.allocator) + if rerr != nil { continue } + defer delete(data) + if len(data) > 8 << 20 { continue } + + // Cuts at a spread of fractions, so the file ends inside the trailer, + // inside an object, and inside a stream in turn. + for num in 1 ..< 8 { + cut := len(data) * num / 8 + if cut <= 0 { continue } + doc, _ := open_bytes(data[:cut]) + // No assertion on the outcome: opening a truncated file may + // legitimately succeed -- the xref rebuild is good at it -- or + // fail. The test is that control reaches the next line at all. + if doc != nil { + for pi in 0 ..< min(page_count(doc), 4) { + page, perr := page_get(doc, pi) + if perr != .None { continue } + testing.expectf(t, rect_is_finite(page.media_box), + "%s: truncated at %v gave a non-finite media box %v", + filepath.base(paths[i]), cut, page.media_box) + } + } + close(doc) + } + free_all(context.temp_allocator) + } +} + +// A byte flipped in a real file reaches paths truncation does not: a corrupted +// /Length, a broken filter header, a mangled xref offset. +@(test) +test_corpus_bitflips :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" || !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + + limit := min(len(paths), 6) + for i in 0 ..< limit { + data, rerr := os.read_entire_file(paths[i], context.allocator) + if rerr != nil { continue } + defer delete(data) + if len(data) > 4 << 20 || len(data) < 64 { continue } + + scratch := make([]byte, len(data)) + defer delete(scratch) + + FLIPS :: 32 + for k in 0 ..< FLIPS { + copy(scratch, data) + // The flips are spread evenly rather than randomly: a reproducible + // failure is worth more here than a broader search. + at := len(scratch) * k / FLIPS + scratch[at] ~= 1 << u8(k % 8) + + doc, _ := open_bytes(scratch) + if doc != nil { + for pi in 0 ..< min(page_count(doc), 8) { + page, perr := page_get(doc, pi) + if perr != .None { continue } + testing.expectf(t, rect_is_finite(page.media_box), + "%s: flip at %v gave a non-finite media box %v", + filepath.base(paths[i]), at, page.media_box) + } + } + close(doc) + } + free_all(context.temp_allocator) + } +} + +// --------------------------------------------------------------------------- +// The resource sweep +// +// Every font, image, shading and pattern named by every page of every corpus +// file, loaded and driven. This is the first thing in the library that touches +// real *subsetted* font programs at scale, and a subsetter mangles a font more +// than anything else in the format mangles anything: it keeps `maxp.numGlyphs` +// at the original count while emptying `loca`, it strips a Type1C down to +// .notdef and space, it leaves a `cmap` naming glyphs that are no longer there. +// None of that is invalid and none of it may crash, hang or produce garbage. +// +// What is asserted is what the layer promises and no more: +// +// * `font_load` always hands back a Font. A font whose program cannot be +// read still has to lay text out, so failure is a flag on the result, not +// an error return. +// * A font with an EMBEDDED program either has glyphs or has `program_failed` +// set. A font with no embedded program has neither, and that is not a +// defect -- it is a standard-14 or a system font, and the widths are the +// whole of what the file gave. +// * Every outline that comes back is finite and inside a sane box. This is +// the "no NaN below the Device line" rule at the one boundary where the +// numbers come from a charstring interpreter rather than from the file. +// * `font_decode` consumes exactly the bytes it is given, one code at a +// time, never zero. The interpreter advances the pen by `nbytes`, so a +// zero there is an infinite loop on a real page. +// * An image either decodes to a bitmap of exactly its own dimensions -- one +// channel for a stencil, four otherwise -- or degrades to an error and no +// bitmap. A short buffer is the failure that matters: it is an +// out-of-bounds read in whatever composites it. +// --------------------------------------------------------------------------- + +// Glyphs probed per font. A CJK font declares 20000 glyphs of which a subset +// keeps twenty, so the probe is spread across the whole range rather than +// taken from the front; the cap is what keeps the sweep to seconds. +@(private = "file") +CORPUS_GLYPH_PROBES :: 256 + +// A glyph-space coordinate larger than this is not a glyph. Real outlines live +// within a few thousand units of the origin whatever the em square is. +@(private = "file") +CORPUS_MAX_GLYPH_COORD :: 1e6 + +@(private = "file") +CORPUS_RES_DEPTH :: 6 + +// Decoding every image in a corpus of scanned documents is where this test +// spends its time -- a full-page scan is nine megapixels and there are +// thousands of them -- and the corpus directory is whatever the developer +// happened to point at. Past this many decoded pixels images are still loaded +// but no longer decoded, so a disk full of 600 dpi TIFFs cannot turn the +// suite into a coffee break. Neither corpus this was written against comes +// close to it. +@(private = "file") +CORPUS_PIXEL_BUDGET :: 1 << 30 + +@(private = "file") +Corpus_Res_Stats :: struct { + fonts, fonts_with_glyphs, fonts_failed, fonts_unembedded: int, + glyphs_probed, glyphs_drawn: int, + codes_decoded: int, + images, images_decoded, images_degraded, images_skipped: int, + pixels: i64, + shadings, patterns: int, +} + +// corpus_glyph_count asks whichever program the Font ended up with. There is +// no `font_glyph_count`: nothing above this line needs one, because the +// interpreter asks for a glyph by id and takes "nothing to draw" for an answer. +@(private = "file") +corpus_glyph_count :: proc(f: ^Font) -> int { + if f == nil { return 0 } + // The order matters for an OpenType font whose outlines are CFF: both + // readers parsed it and only the CFF can draw, which is the same order + // `font_glyph_path` dispatches in. + if f.ttf != nil && tt_has_glyf(f.ttf) { return tt_glyph_count(f.ttf) } + if f.cff != nil { return cff_glyph_count(f.cff) } + if f.t1 != nil { return t1_glyph_count(f.t1) } + if f.ttf != nil { return tt_glyph_count(f.ttf) } + return 0 +} + +@(private = "file") +corpus_check_font :: proc(t: ^testing.T, doc: ^Document, fd: ^Dict, name, fname: string, + st: ^Corpus_Res_Stats) { + st.fonts += 1 + + f, err := font_load(doc, fd) + // Not an expectf on the error alone: a Font is what the caller is promised, + // and a nil one with .None would pass that check and crash the interpreter. + if !testing.expectf(t, err == .None && f != nil, "%s: font /%s: font_load -> %s", + name, fname, error_string(err)) { + return + } + + n := corpus_glyph_count(f) + switch { + case f.kind == .Type3: + // Type3 glyphs are content streams, so there is no program to count. + // What must hold is the matrix: it is the font's own declaration of + // its glyph space, every glyph in it is drawn through it, and a + // singular or non-finite one makes every one of them infinite. + m := font_matrix(f) + testing.expectf(t, matrix_is_finite(m) && matrix_determinant(m) != 0, + "%s: font /%s: Type3 /FontMatrix is %v", name, fname, m) + case n > 0: + st.fonts_with_glyphs += 1 + case f.embedded: + st.fonts_failed += 1 + testing.expectf(t, f.program_failed, + "%s: font /%s (%s, %v): an embedded program yielded no glyphs "+ + "and program_failed is not set", name, fname, f.base_font, f.kind) + case: + st.fonts_unembedded += 1 + } + + // Widths must work whether or not the program does: that is the whole + // reason a failed font still comes back. + for c in 0 ..< FONT_CODES { + w := f.code_width[c] + if !is_finite(w) || abs(w) > MAX_GLYPH_WIDTH { + testing.expectf(t, false, "%s: font /%s: code %v is %v wide", name, fname, c, w) + break + } + } + + // Outlines. A glyph the subsetter dropped reports .No_Glyph or comes back + // empty, both of which are correct; anything else is not. + if n > 0 { + step := max(n / CORPUS_GLYPH_PROBES, 1) + p := path_make(context.temp_allocator) + for g := 0; g < n; g += step { + clear(&p.verbs) + clear(&p.points) + st.glyphs_probed += 1 + gerr := font_glyph_path(f, u32(g), &p) + if gerr != .None && gerr != .No_Glyph { + testing.expectf(t, false, "%s: font /%s (%s): glyph %v -> %s", + name, fname, f.base_font, g, error_string(gerr)) + break + } + if len(p.verbs) == 0 { continue } + st.glyphs_drawn += 1 + bad := false + for pt in p.points { + if !is_finite(pt.x) || !is_finite(pt.y) || + abs(pt.x) > CORPUS_MAX_GLYPH_COORD || abs(pt.y) > CORPUS_MAX_GLYPH_COORD { + testing.expectf(t, false, "%s: font /%s (%s): glyph %v has point %v", + name, fname, f.base_font, g, pt) + bad = true + break + } + } + if bad { break } + } + } + + // Decoding. The probe is every byte value twice over, so a two-byte CMap + // sees each of 256 leading bytes and a one-byte font sees each code. + probe: [512]byte + for i in 0 ..< len(probe) { probe[i] = u8(i) } + codes := make([dynamic]Font_Code, 0, 64, context.temp_allocator) + font_decode(f, probe[:], &codes) + consumed := 0 + for c in codes { + if c.nbytes <= 0 { + testing.expectf(t, false, "%s: font /%s: code %v consumed %v bytes", + name, fname, c.code, c.nbytes) + return + } + consumed += c.nbytes + if !is_finite(c.width) || abs(c.width) > MAX_GLYPH_WIDTH { + testing.expectf(t, false, "%s: font /%s: code %v is %v wide", + name, fname, c.code, c.width) + return + } + } + testing.expectf(t, consumed == len(probe), + "%s: font /%s (%s, %v): %v bytes in, %v consumed by %v codes", + name, fname, f.base_font, f.kind, len(probe), consumed, len(codes)) + st.codes_decoded += len(codes) +} + +@(private = "file") +corpus_check_image :: proc(t: ^testing.T, doc: ^Document, stm: ^Stream, res: ^Dict, + name, xname: string, st: ^Corpus_Res_Stats) { + st.images += 1 + + img, err := image_load(doc, stm, res) + if err != .None || img == nil { + // An image the loader refuses is skipped by the interpreter; what + // matters is that it said so instead of handing back a half-built one. + st.images_degraded += 1 + testing.expectf(t, img == nil, "%s: image /%s: %s with an image anyway", + name, xname, error_string(err)) + return + } + + // Loading is cheap and is checked above for every image; decoding is not. + if st.pixels > CORPUS_PIXEL_BUDGET { + st.images_skipped += 1 + return + } + st.pixels += i64(img.width) * i64(img.height) + + bmp, derr := image_decode(doc, img) + if derr != .None || bmp == nil { + st.images_degraded += 1 + testing.expectf(t, bmp == nil, "%s: image /%s: %s with a bitmap anyway", + name, xname, error_string(derr)) + return + } + st.images_decoded += 1 + + want_n := img.is_mask ? 1 : 4 + testing.expectf(t, bmp.width == img.width && bmp.height == img.height && bmp.n == want_n, + "%s: image /%s: decoded %vx%vx%v for a %vx%v image (mask=%v)", + name, xname, bmp.width, bmp.height, bmp.n, img.width, img.height, img.is_mask) + // The one failure that is an out-of-bounds read rather than a wrong pixel. + testing.expectf(t, len(bmp.pixels) >= bmp.height * bmp.stride && bmp.stride >= bmp.width * bmp.n, + "%s: image /%s: %v bytes for %v rows of %v", name, xname, + len(bmp.pixels), bmp.height, bmp.stride) + testing.expectf(t, !bmp.premultiplied, + "%s: image /%s: image_decode must hand back straight alpha", name, xname) +} + +// corpus_check_resources walks one /Resources dictionary, recursing into form +// XObjects and tiling patterns, which is where most of a real file's fonts +// actually live. `seen` keys on the dictionary pointer, so a resource shared +// by 900 pages is done once and a cycle terminates. +@(private = "file") +corpus_check_resources :: proc(t: ^testing.T, doc: ^Document, res: ^Dict, name: string, + st: ^Corpus_Res_Stats, seen: ^map[rawptr]bool, depth: int) { + if res == nil || depth > CORPUS_RES_DEPTH { return } + if seen[rawptr(res)] { return } + seen[rawptr(res)] = true + + if fonts := dict_dict(doc, res, "Font"); fonts != nil { + for fname, fobj in fonts { + fd, ok := to_dict(resolve(doc, fobj)) + if !ok { continue } + if seen[rawptr(fd)] { continue } + seen[rawptr(fd)] = true + corpus_check_font(t, doc, fd, name, fname, st) + } + } + + if xo := dict_dict(doc, res, "XObject"); xo != nil { + for xname, xobj in xo { + stm, ok := to_stream(resolve(doc, xobj)) + if !ok { continue } + if seen[rawptr(stm)] { continue } + seen[rawptr(stm)] = true + if dict_name(doc, stm.dict, "Subtype") == "Form" { + corpus_check_resources(t, doc, dict_dict(doc, stm.dict, "Resources"), + name, st, seen, depth + 1) + continue + } + corpus_check_image(t, doc, stm, res, name, xname, st) + } + } + + if shs := dict_dict(doc, res, "Shading"); shs != nil { + for sname, sobj in shs { + sh, serr := shading_load(doc, sobj, res) + if serr != .None || sh == nil { continue } + st.shadings += 1 + bmp, berr := bitmap_make(16, 16, 4, context.temp_allocator) + if berr != .None { continue } + perr := shading_paint(doc, sh, &bmp, Matrix{1, 0, 0, 1, 8, 8}, + Raster_Clip{0, 0, 16, 16}, 1, .Normal, nil) + testing.expectf(t, perr == .None, "%s: shading /%s (%v): %s", + name, sname, sh.kind, error_string(perr)) + } + } + + if pats := dict_dict(doc, res, "Pattern"); pats != nil { + for pname, pobj in pats { + pat, perr := pattern_load(doc, pobj, res) + if perr != .None || pat == nil { continue } + st.patterns += 1 + if pat.kind == .Shading && pat.shading != nil { + bmp, berr := bitmap_make(16, 16, 4, context.temp_allocator) + if berr != .None { continue } + e := pattern_paint_shading(doc, pat, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 16, 16}, 1, .Normal, nil) + testing.expectf(t, e == .None, "%s: shading pattern /%s: %s", + name, pname, error_string(e)) + } + if pat.kind == .Tiling { + corpus_check_resources(t, doc, pat.resources, name, st, seen, depth + 1) + } + } + } +} + +@(test) +test_corpus_resources :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" || !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + + st: Corpus_Res_Stats + files := 0 + for p in paths { + if corpus_size(p) > CORPUS_MAX_FILE { continue } + + doc, err := open_file(p) + defer close(doc) + if err != .None || doc == nil { continue } + files += 1 + + name := filepath.base(p) + seen := make(map[rawptr]bool) + defer delete(seen) + + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if perr != .None { continue } + corpus_check_resources(t, doc, page.resources, name, &st, &seen, 0) + free_all(context.temp_allocator) + } + } + + if files == 0 { return } + log.infof("corpus resources: %v files | fonts %v (%v with glyphs, %v failed, %v unembedded), "+ + "%v glyphs probed, %v drawn, %v codes decoded | images %v (%v decoded, %v degraded, "+ + "%v not decoded, %v megapixels) | "+ + "%v shadings, %v patterns", + files, st.fonts, st.fonts_with_glyphs, st.fonts_failed, st.fonts_unembedded, + st.glyphs_probed, st.glyphs_drawn, st.codes_decoded, + st.images, st.images_decoded, st.images_degraded, st.images_skipped, + st.pixels / (1 << 20), st.shadings, st.patterns) + + // A corpus that reached no font at all means the walk broke, not that + // seventy documents contain no text. + testing.expectf(t, st.fonts > 0, "the sweep found no fonts in %v files", files) +} + +// --------------------------------------------------------------------------- +// The rendering sweep +// +// Everything above this point checks that the library survives the corpus. +// This checks that it *draws* it, which is a different and much stronger +// claim: a page can parse, load every font, decode every image and still +// rasterise to nothing at all, because the text never drew, a clip swallowed +// the page, or the base matrix put the content off the canvas. None of those +// show up as an error -- they show up as a white rectangle. +// +// So the invariant here is measured in ink. Every page is rendered and the +// fraction of pixels that are not the untouched background is recorded; the +// distribution is logged, and a page that comes back blank is named. That +// number is not a correctness proof -- a page can be inked and wrong -- but +// blankness is a one-sided test that is never a false alarm on a page with +// content on it, and it is the only automatic check there is that the +// interpreter and the devices are connected to each other at all. +// --------------------------------------------------------------------------- + +// 72 dpi. Big enough that a 6-point glyph still deposits ink, small enough +// that three thousand pages finish in a minute. +@(private = "file") +CORPUS_RENDER_SCALE :: 1.0 + +// A document with fewer pages than this renders all of them; a longer one +// renders only its first. A 900-page standard would otherwise dominate the +// sweep while telling it nothing its first fifty pages did not. +@(private = "file") +CORPUS_RENDER_ALL_UNDER :: 20 + +// A page that takes longer than this has found a loop bound that is too +// generous. It is checked after the fact -- the runner cannot interrupt a +// running test -- which is enough to catch a regression from milliseconds to +// minutes. +@(private = "file") +CORPUS_RENDER_MAX_MS :: 30_000 + +// Under one pixel in ten thousand is blank: an accidental hairline or a single +// anti-aliased dot is not a rendered page. +@(private = "file") +CORPUS_BLANK :: 0.0001 + +// Under a tenth of a percent is near-blank -- a page number and nothing else. +@(private = "file") +CORPUS_NEAR_BLANK :: 0.001 + +// A page carrying this much text has a paragraph or more on it, and a page +// with a paragraph on it covers well over a percent of itself in ink. Below +// that the page drew a fraction of what the interpreter read, which is the +// partial version of the blank-page bug and just as much a defect. +@(private = "file") +CORPUS_DENSE_CHARS :: 1000 +@(private = "file") +CORPUS_STARVED_INK :: 0.01 + +@(private = "file") +Corpus_Render_Stats :: struct { + files: int, + pages: int, + errors: int, + blank: int, + near_blank: int, + first_blank: int, // files whose FIRST page is blank -- the loud case + // A blank page that extracts text is the signature this sweep exists to + // find: the interpreter reached the glyphs, so the page is not empty -- + // something between there and the pixels dropped them. + blank_with_text: int, + empty: int, // blank and extracts nothing: genuinely a blank sheet + starved: int, // a paragraph of text and almost no ink + ink_sum: f64, + hist: [10]int, + slowest_ms: f64, + slowest_name: string, + total_ms: f64, + max_pixels: int, +} + +// The bucket boundaries the histogram reports, as fractions of the page. +@(private = "file") +CORPUS_INK_BUCKETS := [9]f64{0.0001, 0.001, 0.005, 0.01, 0.02, 0.05, 0.10, 0.25, 0.50} + +@(private = "file") +corpus_ink_bucket :: proc(f: f64) -> int { + for edge, i in CORPUS_INK_BUCKETS { + if f < edge { return i } + } + return len(CORPUS_INK_BUCKETS) +} + +// corpus_ink is the share of pixels that are not the background the render +// started from. `render_page` leaves straight alpha and an opaque white page, +// so anything that is not pure white was painted. +@(private = "file") +corpus_ink :: proc(bmp: ^Bitmap) -> f64 { + if bmp.width <= 0 || bmp.height <= 0 || bmp.n < 3 { return 0 } + inked := 0 + for y in 0 ..< bmp.height { + row := bitmap_row(bmp, y) + for x in 0 ..< bmp.width { + p := row[x * bmp.n:] + if p[0] != 255 || p[1] != 255 || p[2] != 255 { + inked += 1 + } + } + } + return f64(inked) / f64(bmp.width * bmp.height) +} + +// corpus_render_page renders one page and holds it to the contract: a bitmap +// of exactly the size `page_size` promised, no error, and no runaway time. +@(private = "file") +corpus_render_page :: proc(t: ^testing.T, page: ^Page, name: string, index: int, + st: ^Corpus_Render_Stats) -> (ink: f64, ok: bool) { + want_w, want_h := page_size(page, CORPUS_RENDER_SCALE) + + start := time.now() + bmp, err := render_page(page, Render_Options{ + ctm = scale_matrix(CORPUS_RENDER_SCALE, CORPUS_RENDER_SCALE), + }) + ms := time.duration_milliseconds(time.since(start)) + defer bitmap_destroy(&bmp) + + st.total_ms += ms + if ms > st.slowest_ms { + st.slowest_ms = ms + st.slowest_name = name + } + testing.expectf(t, ms < CORPUS_RENDER_MAX_MS, "%s page %d took %.0f ms to render", + name, index + 1, ms) + + if err != .None { + st.errors += 1 + testing.expectf(t, false, "%s page %d: render_page returned %s", + name, index + 1, error_string(err)) + return 0, false + } + + // The size is part of the contract: `page_size` is what a caller allocates + // a window for before it ever calls render_page, and a bitmap that does + // not match it means one of the two derived the base matrix differently. + testing.expectf(t, bmp.width == want_w && bmp.height == want_h, + "%s page %d: rendered %dx%d, page_size said %dx%d", + name, index + 1, bmp.width, bmp.height, want_w, want_h) + testing.expectf(t, bmp.n == 4 && len(bmp.pixels) >= bmp.stride * bmp.height, + "%s page %d: %d components, %d bytes for %d rows of %d", + name, index + 1, bmp.n, len(bmp.pixels), bmp.height, bmp.stride) + testing.expectf(t, !bmp.premultiplied, + "%s page %d: the bitmap left the library premultiplied", name, index + 1) + + st.max_pixels = max(st.max_pixels, bmp.width * bmp.height) + return corpus_ink(&bmp), true +} + +@(test) +test_corpus_render :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" { + log.info("PDF_ODIN_CORPUS is unset; skipping the render sweep") + return + } + if !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + if len(paths) == 0 { return } + + // Nothing a render allocates may outlive it. The Document arena is + // excluded on purpose -- that is freed by `close` and is not what this is + // watching -- so what is left is the bitmap, the devices and the scratch, + // and all three are supposed to be gone by the time the call returns. + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + defer mem.tracking_allocator_destroy(&track) + + st: Corpus_Render_Stats + blanks := make([dynamic]string) + defer { + for b in blanks { delete(b) } + delete(blanks) + } + + for p in paths { + if corpus_size(p) > CORPUS_MAX_FILE { continue } + + doc, err := open_file(p) + defer close(doc) + if err != .None || doc == nil { continue } + st.files += 1 + + name := filepath.base(p) + n := page_count(doc) + last := n <= CORPUS_RENDER_ALL_UNDER ? n : 1 + + for i in 0 ..< last { + page, perr := page_get(doc, i) + if perr != .None { continue } + + ink, ok := 0.0, false + { + context.allocator = mem.tracking_allocator(&track) + ink, ok = corpus_render_page(t, &page, name, i, &st) + } + if !ok { continue } + + st.pages += 1 + st.ink_sum += ink + st.hist[corpus_ink_bucket(ink)] += 1 + + // Ask the text device what the interpreter saw on any page that + // came out light. A page that draws nothing and extracts nothing + // is a blank sheet, which every corpus has; a page that draws + // nothing and extracts four hundred characters is a defect, and + // the two must never be reported as the same thing. + if ink < CORPUS_STARVED_INK { + text, _ := extract_text(&page) + defer delete(text) + chars := len(text) + + if ink < CORPUS_BLANK { + st.blank += 1 + if i == 0 { st.first_blank += 1 } + if chars > 0 { st.blank_with_text += 1 } else { st.empty += 1 } + append(&blanks, corpus_blank_line(name, i, ink, chars)) + } else if ink < CORPUS_NEAR_BLANK { + st.near_blank += 1 + if chars > 0 { st.blank_with_text += 1 } else { st.empty += 1 } + append(&blanks, corpus_blank_line(name, i, ink, chars)) + } else if chars >= CORPUS_DENSE_CHARS { + st.starved += 1 + append(&blanks, corpus_blank_line(name, i, ink, chars)) + } + } + } + free_all(context.temp_allocator) + } + + if st.files == 0 { return } + + log.infof("render sweep: %d files, %d pages rendered at %gx, %d errors, "+ + "%.1f%% mean ink, largest %d px, %.0f ms total, slowest %.0f ms (%s)", + st.files, st.pages, f64(CORPUS_RENDER_SCALE), st.errors, + st.pages > 0 ? st.ink_sum / f64(st.pages) * 100 : 0, + st.max_pixels, st.total_ms, st.slowest_ms, st.slowest_name) + + labels := [10]string{ + " 0 .. 0.01%", " 0.01 .. 0.1%", " 0.1 .. 0.5%", + " 0.5 .. 1%", " 1 .. 2%", " 2 .. 5%", + " 5 .. 10%", " 10 .. 25%", " 25 .. 50%", " 50 .. 100%", + } + for count, i in st.hist { + if count == 0 { continue } + log.infof(" ink %s : %5d pages", labels[i], count) + } + + for b in blanks { + log.infof(" %s", b) + } + log.infof("render sweep: %d blank pages, %d near-blank, %d starved (a paragraph of text "+ + "under %g%% ink), %d of them a file's first page; %d drew nothing but extract "+ + "text, %d are genuinely empty sheets", + st.blank, st.near_blank, st.starved, f64(CORPUS_STARVED_INK) * 100, st.first_blank, + st.blank_with_text, st.empty) + + // A sweep that rendered nothing would satisfy every assertion above. + testing.expectf(t, st.pages > 0, "the sweep rendered no pages from %d files", st.files) + + // Blankness is the property this whole test exists for. A handful of + // genuinely empty pages is normal in any corpus -- a separator sheet, the + // back of a cover -- but a tenth of the corpus coming back white is a bug + // in the interpreter, not seventy documents full of empty paper. + if st.pages > 0 { + ratio := f64(st.blank) / f64(st.pages) + testing.expectf(t, ratio < 0.10, "%d of %d rendered pages (%.0f%%) came out blank", + st.blank, st.pages, ratio * 100) + } + + // The allocations a render makes are its own to free. Assert the tracker + // saw traffic first: a leak check over an allocator nothing ever reached + // passes for the wrong reason, and rewiring `context.allocator` is exactly + // the sort of thing a later edit breaks silently. + testing.expectf(t, track.total_allocation_count > 0, + "the render sweep tracked no allocations at all over %d pages", st.pages) + log.infof("render sweep: %d allocations, %d MiB peak, %d MiB total through the tracker", + track.total_allocation_count, track.peak_memory_allocated / (1 << 20), + track.total_memory_allocated / (1 << 20)) + + leaked := 0 + for _, entry in track.allocation_map { + leaked += entry.size + } + testing.expectf(t, len(track.allocation_map) == 0, + "%d allocations (%d bytes) survived the render sweep", len(track.allocation_map), leaked) + testing.expectf(t, len(track.bad_free_array) == 0, + "%d bad frees during the render sweep", len(track.bad_free_array)) +} + +@(private = "file") +corpus_blank_line :: proc(name: string, index: int, ink: f64, chars: int) -> string { + what := "empty sheet " + if chars > 0 { + what = ink < CORPUS_NEAR_BLANK ? "DREW NOTHING" : "STARVED " + } + return fmt.aprintf("%s %s page %d: %.4f%% ink, %d characters of text", + what, name, index + 1, ink * 100, chars) +} + +// --------------------------------------------------------------------------- +// Text extraction over the corpus +// +// The same interpreter, into the text device instead of the rasteriser. What +// it measures is coverage of the unicode chain -- /ToUnicode, the encoding +// differences, the predefined CMaps and the AGL -- because a code that +// resolves to nothing extracts as U+FFFD, and a page that is mostly U+FFFD is +// text nobody can search or copy even though it draws correctly. +// --------------------------------------------------------------------------- + +@(test) +test_corpus_text :: proc(t: ^testing.T) { + dir := corpus_dir() + defer delete(dir) + if dir == "" { + log.info("PDF_ODIN_CORPUS is unset; skipping the text sweep") + return + } + if !os.is_dir(dir) { return } + + paths := corpus_collect(dir) + defer corpus_release(&paths) + if len(paths) == 0 { return } + + files, pages, with_text, chars, runes_, bad := 0, 0, 0, 0, 0, 0 + + // Every page that is more than a tenth replacement characters, by name: + // a ratio alone says the chain is failing somewhere and not where, and + // "somewhere" is not something anyone can act on. + suspect := make([dynamic]string) + defer { + for s in suspect { delete(s) } + delete(suspect) + } + + for p in paths { + if corpus_size(p) > CORPUS_MAX_FILE { continue } + + doc, err := open_file(p) + defer close(doc) + if err != .None || doc == nil { continue } + files += 1 + name := filepath.base(p) + + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if perr != .None { continue } + + text, terr := extract_text(&page) + defer delete(text) + testing.expectf(t, terr == .None, "%s page %d: extract_text returned %s", + name, i + 1, error_string(terr)) + pages += 1 + + // Whatever comes out must be valid UTF-8: it is a Go-style string + // that goes straight into a caller's search index. + testing.expectf(t, utf8.valid_string(text), + "%s page %d: extracted text is not valid UTF-8", name, i + 1) + + nr, nbad := 0, 0 + for r in text { + nr += 1 + if r == utf8.RUNE_ERROR { nbad += 1 } + } + chars += len(text) + runes_ += nr + bad += nbad + if len(text) > 0 { with_text += 1 } + + // Short strings make wild ratios; a page with a handful of glyphs + // that all failed is not the signal this is looking for. + if nr >= 32 { + frac := f64(nbad) / f64(nr) + if frac > 0.10 { + append(&suspect, fmt.aprintf("%s page %d: %.0f%% U+FFFD (%d of %d runes)", + name, i + 1, frac * 100, nbad, nr)) + } + } + free_all(context.temp_allocator) + } + } + + if files == 0 { return } + log.infof("text sweep: %d files, %d pages, %d with text (%.0f%%), %d bytes, %d runes, "+ + "%d U+FFFD (%.3f%%)", + files, pages, with_text, pages > 0 ? f64(with_text) / f64(pages) * 100 : 0, + chars, runes_, bad, runes_ > 0 ? f64(bad) / f64(runes_) * 100 : 0) + for s in suspect { + log.infof(" UNRESOLVED %s", s) + } + if len(suspect) > 0 { + log.infof("text sweep: %d pages are over 10%% U+FFFD", len(suspect)) + } + + testing.expectf(t, with_text > 0, "no page in %d files yielded any text", files) + + // A corpus this size is mostly ordinary prose in ordinary fonts. If one + // rune in twenty comes out as U+FFFD the unicode chain is broken, not the + // files. + if runes_ > 0 { + ratio := f64(bad) / f64(runes_) + testing.expectf(t, ratio < 0.05, "%d of %d extracted runes (%.1f%%) are U+FFFD", + bad, runes_, ratio * 100) + } +} diff --git a/crypt.odin b/crypt.odin new file mode 100644 index 0000000..8277afd --- /dev/null +++ b/crypt.odin @@ -0,0 +1,1092 @@ +package pdf + +import "core:crypto" +import "core:crypto/aes" +import "core:crypto/legacy/md5" +import "core:crypto/sha2" + +// The standard security handler (ISO 32000-2, 7.6.4). +// +// Decryption is transparent: `crypt_init` derives the file encryption key once, +// and the parser then routes every string and every stream payload through +// `decrypt_string` / `decrypt_stream` as it reads them, so nothing above this +// file ever sees ciphertext. +// +// Two things are deliberately never decrypted, because they are the inputs the +// key was derived from: the /Encrypt dictionary itself, and the /ID strings in +// the trailer. A third is never encrypted in the first place, because it has +// to be readable before the /Encrypt dictionary it points at can be found: a +// cross-reference stream (ISO 32000-2, 7.5.8.2). A fourth is not decrypted +// here because it already was: anything that came out of an object stream, +// which is decrypted once as a whole when its container is. Those objects +// carry Ref{0,0}, and that is the signal used below. + +// Crypt_Method is what a crypt filter does to bytes. `Identity` covers both +// /Identity and a filter with /CFM /None -- "hand the bytes back untouched". +Crypt_Method :: enum u8 { + Identity, + Rc4, + Aes_128, + Aes_256, +} + +// Password_Kind records which of the two passwords the caller supplied. +// A file encrypted with an empty user password -- "opens without prompting, +// but you may not print it" -- authenticates as `User`. +Password_Kind :: enum u8 { + None, + User, + Owner, +} + +// Permission is one bit of the /P entry, numbered as the format numbers them +// minus one: /P bit position 3 (printing) is bit index 2 here. +// ISO 32000-2, table 22. +Permission :: enum u32 { + Print = 2, + Modify = 3, + Copy = 4, + Annotate = 5, + Fill_Forms = 8, + Extract = 9, // extraction for accessibility + Assemble = 10, + Print_High_Res = 11, +} + +Permissions :: bit_set[Permission; u32] + +// Encryption is the decrypted state of the standard security handler: what was +// negotiated, and the file encryption key everything else is derived from. +// It lives in the Document's arena and is reclaimed with it. +Encryption :: struct { + // The /Encrypt dictionary's own object, so it can be excluded from + // decryption if anything asks for it a second time. {0,0} when the + // dictionary was written directly in the trailer. + ref: Ref, + version: int, // /V + revision: int, // /R + key_bits: int, // effective file key length, in bits + // /P, as the signed 32-bit value the format defines. Stored signed because + // almost every file writes it negative (bits 13..32 are all 1s). + permission_bits: i32, + encrypt_metadata: bool, // /EncryptMetadata + authenticated_as: Password_Kind, + + stream_method: Crypt_Method, // /StmF + string_method: Crypt_Method, // /StrF + embedded_method: Crypt_Method, // /EFF + + // The file encryption key: 5..16 bytes for RC4 and AES-128, 32 for + // AES-256. Zeroed by `crypt_destroy`. + key: []byte, +} + +// The padding string of ISO 32000-2, 7.6.4.3.2 (Algorithm 2, step a). Every +// password shorter than 32 bytes is extended with a prefix of it. +@(private, rodata) +CRYPT_PAD := [32]byte{ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, + 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, +} + +// "sAlT", appended to the per-object key input for AES-128 so that a file +// cannot be downgraded from AESV2 to V2 with the same key (7.6.3.1). +@(private, rodata) +CRYPT_AES_SALT := [4]byte{0x73, 0x41, 0x6C, 0x54} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +// crypt_init reads the trailer's /Encrypt dictionary and derives the file +// encryption key from `password`. +// +// The password is tried as the user password first and then as the owner +// password, because the empty user password -- a document that opens without +// prompting but restricts printing or copying -- is by far the common case. +// An unencrypted document clears `doc.encrypt` and returns .None. +// +// Internal: this is a step of `open_bytes`, not an entry point. There is no +// public equivalent because there is nothing a caller could call it on -- a +// document whose password was refused is closed before `open_bytes` returns, +// so no locked `^Document` ever escapes the package. Retrying with a second +// password means calling `open_bytes` again with a different +// `Open_Options.password`; the file bytes are already in memory, and reusing +// the Document would mean re-decrypting objects the old key already cached. +@(private) +crypt_init :: proc(doc: ^Document, password: []byte) -> Error { + if doc == nil { return .Invalid_Argument } + doc.encrypt = nil + + raw := dict_get_raw(doc.trailer, "Encrypt") + if raw == nil { return .None } + + enc_ref, _ := to_ref(raw) + // Resolving happens while doc.encrypt is still nil, which is exactly why + // the /Encrypt dictionary's own strings come back untouched. + ed, is_dict := to_dict(resolve(doc, raw)) + if !is_dict || ed == nil { + // /Encrypt is present, so the file is encrypted, and we cannot read the + // dictionary that says how. There is nothing to degrade to: carrying on + // would hand ciphertext to every layer above. + return .Bad_Encrypt_Dict + } + + // A missing /Filter is assumed to be the standard handler; a named one that + // is not /Standard is a plug-in we do not have. + if f := dict_name(doc, ed, "Filter", "Standard"); f != "Standard" { + return .Unsupported_Encryption + } + + version := int(dict_int(doc, ed, "V", 0)) + revision := int(dict_int(doc, ed, "R", 0)) + + switch { + case version <= 0: version = 1 + // V3 is an unpublished Adobe variant with a 40..128 bit key; every viewer + // treats it as V2, and the files that use it decrypt correctly that way. + case version == 3: version = 2 + } + if version > 5 { return .Unsupported_Encryption } + if revision == 0 { + switch version { + case 1: revision = 2 + case 2: revision = 3 + case 4: revision = 4 + case: revision = 6 + } + } + if revision < 2 || revision > 6 { return .Unsupported_Encryption } + // V5 without R5/R6, or R5/R6 without V5, is a file that contradicts itself: + // /V 5 promises a 32-byte key and only R5/R6 can produce one. The /U value + // settles it -- 48 bytes is the R5/R6 layout, 32 the older one -- because + // that is the field the algorithms actually consume. + u_len := len(dict_bytes(doc, ed, "U")) + if revision >= 5 && version != 5 { + version = 5 + add_repair(doc, .Bad_Filter_Data, 0, enc_ref) + } else if version == 5 && revision < 5 { + if u_len >= 48 { + revision = 6 + } else { + version = 4 + } + add_repair(doc, .Bad_Filter_Data, 0, enc_ref) + } + + enc := new(Encryption, doc.allocator) + enc.ref = enc_ref + enc.version = version + enc.revision = revision + enc.encrypt_metadata = dict_bool(doc, ed, "EncryptMetadata", true) + + // /P is a 32-bit field. Files write it as a negative integer, as the + // equivalent unsigned value, and occasionally as a real; take the low 32 + // bits of whatever arrived and read them as signed. + p_raw := dict_int(doc, ed, "P", -1) + p_u := u32(transmute(u64)p_raw & 0xffff_ffff) + enc.permission_bits = transmute(i32)p_u + + // Crypt filters (V4 and V5). Below that, everything is RC4 with the file key. + cf_bits := 0 + if version >= 4 { + cf := dict_dict(doc, ed, "CF") + stmf := dict_name(doc, ed, "StmF", "Identity") + strf := dict_name(doc, ed, "StrF", "Identity") + // /EFF defaults to the stream filter, so an unspecified embedded-file + // filter never leaves attachments in the clear when streams are not. + eff := dict_name(doc, ed, "EFF", stmf) + + m, b, err := crypt_filter_lookup(doc, cf, stmf, enc_ref) + if err != .None { return err } + enc.stream_method = m + if b > cf_bits { cf_bits = b } + + m, b, err = crypt_filter_lookup(doc, cf, strf, enc_ref) + if err != .None { return err } + enc.string_method = m + if b > cf_bits { cf_bits = b } + + m, b, err = crypt_filter_lookup(doc, cf, eff, enc_ref) + if err != .None { return err } + enc.embedded_method = m + if b > cf_bits { cf_bits = b } + + // /CF is required for V5, so a V5 file that names no filter at all is + // broken and the only thing it can have meant is AES-256. An explicit + // /Identity is left alone: that one is a deliberate configuration. + if version == 5 { + if !dict_has(ed, "StmF") && enc.stream_method == .Identity { + enc.stream_method = .Aes_256 + add_repair(doc, .Bad_Filter_Data, 0, enc_ref) + } + if !dict_has(ed, "StrF") && enc.string_method == .Identity { + enc.string_method = .Aes_256 + } + if !dict_has(ed, "EFF") && enc.embedded_method == .Identity { + enc.embedded_method = enc.stream_method + } + } + } else { + enc.stream_method = .Rc4 + enc.string_method = .Rc4 + enc.embedded_method = .Rc4 + } + + // How long the file key is. /Length is in bits; a crypt filter's /Length is + // in bytes per the spec but in bits in plenty of files, so it is normalised + // in crypt_filter_lookup. + key_bits := 40 + if version >= 2 { + key_bits = int(dict_int(doc, ed, "Length", 40)) + } + if version == 4 && cf_bits > 0 { + key_bits = cf_bits + } + + uses_aes_128 := enc.stream_method == .Aes_128 || + enc.string_method == .Aes_128 || + enc.embedded_method == .Aes_128 + n: int + switch { + case version == 5: + n = 32 + // An AES-128 filter needs a full 16-byte key whatever /Length claims, and + // files that pair /AESV2 with /Length 40 exist. A 10-byte key is not a + // legal AES key at all, so the method wins over the declared length. + case uses_aes_128: + n = 16 + // V1 is 40-bit by definition, and so is R2 whatever /Length says + // (7.6.4.3.2, step i). + case version == 1, revision == 2: + n = 5 + case: + // The clamp is what keeps a /Length of 2^31 from sizing a buffer. + n = clamp(key_bits / 8, 5, 16) + } + enc.key_bits = n * 8 + enc.key = make([]byte, n, doc.allocator) + + id0 := crypt_first_id(doc) + + err: Error + if revision >= 5 { + err = crypt_auth_r56(doc, ed, enc, password) + } else { + err = crypt_auth_r234(doc, ed, enc, password, id0) + } + if err != .None { + return err + } + + doc.encrypt = enc + return .None +} + +// crypt_destroy wipes the file encryption key and detaches the handler. The +// memory itself belongs to the Document's arena and is released by `close`. +// +// Internal: `close` calls this, and calling it on a Document still in use +// would leave every later stream decoding to ciphertext. +@(private) +crypt_destroy :: proc(doc: ^Document) { + if doc == nil || doc.encrypt == nil { return } + if len(doc.encrypt.key) > 0 { + crypto.zero_explicit(raw_data(doc.encrypt.key), len(doc.encrypt.key)) + } + doc.encrypt.authenticated_as = .None + doc.encrypt = nil +} + +// --------------------------------------------------------------------------- +// Public accessors +// --------------------------------------------------------------------------- + +// document_is_encrypted reports whether the file carried an /Encrypt +// dictionary. It stays true after a successful open: the document was +// encrypted, it is simply readable now. +document_is_encrypted :: proc(doc: ^Document) -> bool { + return doc != nil && doc.encrypt != nil +} + +// encryption_permissions returns what the document's /P bits allow. +// +// An unencrypted document, and one opened with the owner password, allow +// everything: the owner is not subject to the restrictions they set +// (ISO 32000-2, 7.6.4.2). +encryption_permissions :: proc(doc: ^Document) -> Permissions { + all := Permissions{.Print, .Modify, .Copy, .Annotate, .Fill_Forms, .Extract, .Assemble, .Print_High_Res} + if doc == nil || doc.encrypt == nil { return all } + if doc.encrypt.authenticated_as == .Owner { return all } + + bits := transmute(u32)doc.encrypt.permission_bits + out: Permissions + for p in Permission { + if bits & (u32(1) << u32(p)) != 0 { + out += {p} + } + } + // Revision 2 only defined bits 3..6; the rest were reserved and set to 1, + // so reporting them would be meaningless. + if doc.encrypt.revision == 2 { + out &= {.Print, .Modify, .Copy, .Annotate} + } + return out +} + +// encryption_method_name names the scheme in use, for a UI or a log line. +// Not stable enough to parse. +encryption_method_name :: proc(doc: ^Document) -> string { + if doc == nil || doc.encrypt == nil { return "none" } + // Streams in the clear but strings encrypted is a real configuration -- a + // file that protects only its attachments -- so fall back to reporting the + // string filter rather than calling the whole document unencrypted. + if doc.encrypt.stream_method == .Identity { + if doc.encrypt.string_method == .Identity { + return "none (identity crypt filter)" + } + return crypt_method_name(doc.encrypt.string_method, doc.encrypt.key_bits) + } + return crypt_method_name(doc.encrypt.stream_method, doc.encrypt.key_bits) +} + +@(private) +crypt_method_name :: proc(method: Crypt_Method, key_bits: int) -> string { + switch method { + case .Identity: + return "none" + case .Rc4: + // 40 and 128 are the only lengths anything produces; a file that picked + // some other multiple of 8 is legal and gets the unqualified name. + switch key_bits { + case 40: return "RC4 40-bit" + case 128: return "RC4 128-bit" + } + return "RC4" + case .Aes_128: + return "AES-128" + case .Aes_256: + return "AES-256" + } + return "unknown" +} + +// --------------------------------------------------------------------------- +// Decryption of parsed objects +// --------------------------------------------------------------------------- + +// decrypt_string decrypts one string object read out of indirect object `ref`. +// The result lives in the Document's arena. When nothing has to be done -- no +// encryption, an /Identity filter, or an object that was already decrypted as +// part of its object stream -- `data` is returned unchanged. +decrypt_string :: proc(doc: ^Document, ref: Ref, data: []byte) -> []byte { + if doc == nil || doc.encrypt == nil { return data } + return crypt_apply(doc, ref, data, doc.encrypt.string_method) +} + +// decrypt_stream decrypts one stream payload read out of indirect object `ref`. +// The result lives in the Document's arena; see `decrypt_string` for when the +// input is passed through. +// +// `dict` is the stream's own dictionary when the caller has it, and is what +// says "this is a cross-reference stream, leave it alone". Passing it matters +// only after the document is open: `xref_load` reads the /Type /XRef stream +// before `crypt_init` has run, so `doc.encrypt` is still nil there and the +// exemption is free -- but the same object read again through `get_stream` +// reaches here with the key in place, and a second pass over plaintext turns +// the xref table into noise. +// +// One stream this cannot recognise even from its dictionary is the document's +// /Metadata, which is left in the clear when /EncryptMetadata is false. The +// caller knows which object that is; `Encryption.encrypt_metadata` says +// whether it matters. +decrypt_stream :: proc(doc: ^Document, ref: Ref, data: []byte, dict: ^Dict = nil) -> []byte { + if doc == nil || doc.encrypt == nil { return data } + // ISO 32000-2, 7.5.8.2: "the cross-reference stream shall not be + // encrypted". Read the /Type without resolving it: this runs while the + // object is still being parsed, and a genuine cross-reference stream + // always spells its /Type out directly. + if dict != nil { + if ty, ok := to_name(dict_get_raw(dict, "Type")); ok && ty == "XRef" { + return data + } + } + return crypt_apply(doc, ref, data, doc.encrypt.stream_method) +} + +// decrypt_embedded_file decrypts an embedded file stream, which a file may +// protect with a different filter from the one used for page content -- the +// "encrypt the attachments only" configuration, where /StmF and /StrF are +// /Identity and only /EFF names a real filter. +decrypt_embedded_file :: proc(doc: ^Document, ref: Ref, data: []byte) -> []byte { + if doc == nil || doc.encrypt == nil { return data } + return crypt_apply(doc, ref, data, doc.encrypt.embedded_method) +} + +@(private) +crypt_apply :: proc(doc: ^Document, ref: Ref, data: []byte, method: Crypt_Method) -> []byte { + enc := doc.encrypt + if method == .Identity || len(data) == 0 { return data } + // Ref{0,0} means "came out of an object stream", which was decrypted whole. + // Decrypting again would destroy it. + if ref.num == 0 { return data } + // The /Encrypt dictionary is the key's own input and is never encrypted. + if enc.ref.num != 0 && ref.num == enc.ref.num { return data } + + key_buf: [32]byte + key_len := crypt_object_key(enc, method, ref, key_buf[:]) + if key_len == 0 { return data } + key := key_buf[:key_len] + + switch method { + case .Identity: + return data + case .Rc4: + out := make([]byte, len(data), doc.allocator) + rc4_crypt(key, out, data) + return out + case .Aes_128, .Aes_256: + return crypt_aes_decrypt(doc, key, data, ref) + } + return data +} + +// crypt_aes_decrypt strips the 16-byte IV prefix, decrypts in CBC mode and +// removes the PKCS#7 padding (ISO 32000-2, 7.6.3.2). +// +// Anything that cannot be a valid payload -- shorter than one IV plus one +// block, a trailing partial block, padding that does not check out -- decrypts +// to nothing. A truncated or corrupt stream is a damaged object, not a reason +// to fail the document, and an empty stream is what every viewer shows for one. +@(private) +crypt_aes_decrypt :: proc(doc: ^Document, key, data: []byte, ref: Ref) -> []byte { + if len(data) <= aes.BLOCK_SIZE { + add_repair(doc, .Bad_Filter_Data, 0, ref) + return nil + } + body := data[aes.BLOCK_SIZE:] + // Trailing bytes that do not fill a block cannot be decrypted; dropping + // them recovers the blocks that came before, which is all that is left. + n := (len(body) / aes.BLOCK_SIZE) * aes.BLOCK_SIZE + if n == 0 { + add_repair(doc, .Bad_Filter_Data, 0, ref) + return nil + } + if n != len(body) { + add_repair(doc, .Bad_Filter_Data, 0, ref) + } + + out := make([]byte, n, doc.allocator) + if !aes_cbc_decrypt(out, body[:n], key, data[:aes.BLOCK_SIZE]) { + add_repair(doc, .Bad_Filter_Data, 0, ref) + return nil + } + + pad := int(out[n - 1]) + if pad < 1 || pad > aes.BLOCK_SIZE || pad > n { + add_repair(doc, .Bad_Filter_Data, 0, ref) + return nil + } + for i in n - pad ..< n { + if int(out[i]) != pad { + add_repair(doc, .Bad_Filter_Data, 0, ref) + return nil + } + } + return out[:n - pad] +} + +// crypt_object_key builds the key for one indirect object (Algorithm 1, +// ISO 32000-2, 7.6.3.1). Returns the number of bytes written to `out`. +// +// AES-256 skips this entirely: V5 uses the file key for every object, which is +// what made the 2048-object-per-key limit of the earlier scheme go away. +@(private) +crypt_object_key :: proc(enc: ^Encryption, method: Crypt_Method, ref: Ref, out: []byte) -> int { + if method == .Aes_256 { + if len(enc.key) != aes.KEY_SIZE_256 { return 0 } + copy(out, enc.key) + return aes.KEY_SIZE_256 + } + n := len(enc.key) + if n == 0 || n > 16 { return 0 } + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, enc.key) + // Low three bytes of the object number then low two of the generation, + // both low-order byte first. + ext := [5]byte{ + byte(ref.num), + byte(ref.num >> 8), + byte(ref.num >> 16), + byte(ref.gen), + byte(ref.gen >> 8), + } + md5.update(&ctx, ext[:]) + if method == .Aes_128 { + md5.update(&ctx, CRYPT_AES_SALT[:]) + } + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + + key_len := min(n + 5, 16) + copy(out[:key_len], digest[:key_len]) + return key_len +} + +// --------------------------------------------------------------------------- +// Crypt filters +// --------------------------------------------------------------------------- + +@(private) +crypt_filter_lookup :: proc(doc: ^Document, cf: ^Dict, name: string, ref: Ref) -> (method: Crypt_Method, bits: int, err: Error) { + if name == "" || name == "Identity" { + return .Identity, 0, .None + } + f := dict_dict(doc, cf, name) + if f == nil { + // /StmF naming a filter that /CF does not define. The file is broken; + // passing the bytes through at least leaves any part of the document + // that uses a filter we *can* read intact. + add_repair(doc, .Bad_Filter_Data, 0, ref) + return .Identity, 0, .None + } + + // The spec says a crypt filter's /Length is in bytes, but producers write + // bits often enough that every reader guesses. Nothing legal is both a + // plausible byte count and a plausible bit count, so 40 splits them. + n := int(dict_int(doc, f, "Length", 0)) + if n > 0 && n < 40 { n *= 8 } + if n > 256 { n = 256 } + + switch dict_name(doc, f, "CFM", "None") { + case "None": return .Identity, 0, .None + case "V2": return .Rc4, n, .None + case "AESV2": return .Aes_128, 128, .None + case "AESV3": return .Aes_256, 256, .None + } + // An unrecognised /CFM is a scheme we would silently mangle every object + // with. Better to say so. + return .Identity, 0, .Unsupported_Encryption +} + +// crypt_first_id returns the first element of the trailer's /ID array, an input +// to the key derivation for R2..R4. A missing or malformed /ID is common in +// damaged files and reads as empty, which is what other readers do. +@(private) +crypt_first_id :: proc(doc: ^Document) -> []byte { + id, _ := to_bytes(array_get(doc, dict_array(doc, doc.trailer, "ID"), 0)) + return id +} + +// --------------------------------------------------------------------------- +// Key derivation, revisions 2 to 4 +// --------------------------------------------------------------------------- + +@(private) +crypt_auth_r234 :: proc(doc: ^Document, ed: ^Dict, enc: ^Encryption, password, id0: []byte) -> Error { + ovalue, uvalue: [32]byte + crypt_fixed(dict_bytes(doc, ed, "O"), ovalue[:]) + crypt_fixed(dict_bytes(doc, ed, "U"), uvalue[:]) + + // As the user password. + crypt_key_r234(password, ovalue[:], enc.permission_bits, id0, + enc.revision, len(enc.key), enc.encrypt_metadata, enc.key) + if crypt_check_user(enc.key, enc.revision, id0, uvalue[:]) { + enc.authenticated_as = .User + return .None + } + + // As the owner password: /O decrypts to the user password (Algorithm 7), + // which is then checked the ordinary way. + recovered: [32]byte + crypt_owner_password(password, ovalue[:], enc.revision, len(enc.key), recovered[:]) + crypt_key_r234(recovered[:], ovalue[:], enc.permission_bits, id0, + enc.revision, len(enc.key), enc.encrypt_metadata, enc.key) + if crypt_check_user(enc.key, enc.revision, id0, uvalue[:]) { + enc.authenticated_as = .Owner + return .None + } + + crypto.zero_explicit(raw_data(enc.key), len(enc.key)) + return .Wrong_Password +} + +// crypt_pad_password writes the 32-byte padded password of Algorithm 2 step (a). +@(private) +crypt_pad_password :: proc(password: []byte, out: []byte) { + n := min(len(password), 32) + copy(out[:n], password[:n]) + copy(out[n:32], CRYPT_PAD[:32 - n]) +} + +// crypt_fixed copies `src` into `out`, zero-filling what is missing. /O and /U +// are fixed-width fields, and a file that stores them short would otherwise +// index out of bounds. +@(private) +crypt_fixed :: proc(src: []byte, out: []byte) { + n := min(len(src), len(out)) + copy(out[:n], src[:n]) + for i in n ..< len(out) { + out[i] = 0 + } +} + +// crypt_key_r234 is Algorithm 2: the file encryption key for revisions 2 to 4. +@(private) +crypt_key_r234 :: proc(password, ovalue: []byte, permissions: i32, id0: []byte, + revision, n: int, encrypt_metadata: bool, out: []byte) { + // The key cannot be longer than the MD5 digest it is cut from, whatever a + // contradictory /V and /R pair asked for. + key_len := min(n, len(out), md5.DIGEST_SIZE) + if key_len <= 0 { return } + + padded: [32]byte + crypt_pad_password(password, padded[:]) + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, padded[:]) + md5.update(&ctx, ovalue[:32]) + + // /P as four bytes, low-order first, of the *unsigned* interpretation. + pu := transmute(u32)permissions + pbytes := [4]byte{byte(pu), byte(pu >> 8), byte(pu >> 16), byte(pu >> 24)} + md5.update(&ctx, pbytes[:]) + + md5.update(&ctx, id0) + + // R4 with /EncryptMetadata false folds in four 0xFF bytes, so that a file + // whose metadata was left in the clear gets a different key from one whose + // metadata was not (7.6.4.3.2, step f). + if revision >= 4 && !encrypt_metadata { + ff := [4]byte{0xff, 0xff, 0xff, 0xff} + md5.update(&ctx, ff[:]) + } + + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + + // R3 and later iterate 50 times over the *first n bytes* of the digest. + // Algorithm 3, which builds /O, iterates over all 16 -- the asymmetry is + // real and getting it wrong only shows up on owner passwords. + if revision >= 3 { + for _ in 0 ..< 50 { + md5.init(&ctx) + md5.update(&ctx, digest[:key_len]) + md5.final(&ctx, digest[:]) + } + } + copy(out[:key_len], digest[:key_len]) +} + +// crypt_user_value is Algorithm 4 (R2) and Algorithm 5 (R3+): what /U must +// contain if `key` is the right file encryption key. +@(private) +crypt_user_value :: proc(key: []byte, revision: int, id0: []byte, out: []byte) { + n := min(len(key), 16) + if n == 0 { + crypt_fixed(nil, out[:32]) + return + } + + if revision == 2 { + copy(out[:32], CRYPT_PAD[:]) + rc4_crypt(key[:n], out[:32], out[:32]) + return + } + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, CRYPT_PAD[:]) + md5.update(&ctx, id0) + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + + rc4_crypt(key[:n], digest[:], digest[:]) + tmp: [16]byte + for i in 1 ..= 19 { + for j in 0 ..< n { + tmp[j] = key[j] ~ byte(i) + } + rc4_crypt(tmp[:n], digest[:], digest[:]) + } + + copy(out[:16], digest[:]) + // The last 16 bytes are explicitly arbitrary padding and are never checked. + for i in 16 ..< 32 { + out[i] = 0 + } +} + +// crypt_check_user is Algorithm 6. R2 compares all 32 bytes; R3 and later only +// the first 16, the rest being padding the producer chose freely. +@(private) +crypt_check_user :: proc(key: []byte, revision: int, id0: []byte, uvalue: []byte) -> bool { + computed: [32]byte + crypt_user_value(key, revision, id0, computed[:]) + n := revision == 2 ? 32 : 16 + return crypt_equal(computed[:n], uvalue[:n]) +} + +// crypt_owner_password is Algorithm 7: decrypt /O back into the padded user +// password. Whether it *is* the user password is decided by the caller. +@(private) +crypt_owner_password :: proc(password, ovalue: []byte, revision, n: int, out: []byte) { + padded: [32]byte + crypt_pad_password(password, padded[:]) + + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, padded[:]) + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + + if revision >= 3 { + for _ in 0 ..< 50 { + md5.init(&ctx) + md5.update(&ctx, digest[:]) + md5.final(&ctx, digest[:]) + } + } + + key_len := revision == 2 ? 5 : min(max(n, 1), 16) + copy(out[:32], ovalue[:32]) + if revision == 2 { + rc4_crypt(digest[:key_len], out[:32], out[:32]) + return + } + tmp: [16]byte + for i := 19; i >= 0; i -= 1 { + for j in 0 ..< key_len { + tmp[j] = digest[j] ~ byte(i) + } + rc4_crypt(tmp[:key_len], out[:32], out[:32]) + } +} + +// --------------------------------------------------------------------------- +// Key derivation, revisions 5 and 6 (AES-256) +// +// R5 is Adobe's deprecated extension: a bare SHA-256. R6 is what ISO 32000-2 +// standardised, with the hardening loop of Algorithm 2.B in front of it. +// Both share the /U and /O layout: 32 bytes of hash, an 8-byte validation +// salt, an 8-byte key salt. +// --------------------------------------------------------------------------- + +@(private) +crypt_auth_r56 :: proc(doc: ^Document, ed: ^Dict, enc: ^Encryption, password: []byte) -> Error { + ovalue, uvalue: [48]byte + crypt_fixed(dict_bytes(doc, ed, "O"), ovalue[:]) + crypt_fixed(dict_bytes(doc, ed, "U"), uvalue[:]) + + // ISO 32000-2 asks for SASLprep here. Every implementation in practice uses + // the UTF-8 bytes as given, truncated to 127, and files produced from + // non-ASCII passwords are rare enough that the difference has never bitten. + pw := password + if len(pw) > 127 { pw = pw[:127] } + + hash: [32]byte + intermediate: [32]byte + wrapped: []byte + + crypt_hash_r56(enc.revision, pw, uvalue[32:40], nil, hash[:]) + if crypt_equal(hash[:], uvalue[:32]) { + enc.authenticated_as = .User + crypt_hash_r56(enc.revision, pw, uvalue[40:48], nil, intermediate[:]) + wrapped = dict_bytes(doc, ed, "UE") + } else { + // The owner check folds in the whole 48-byte /U value, which is what + // stops an /O from being lifted into another document. + crypt_hash_r56(enc.revision, pw, ovalue[32:40], uvalue[:], hash[:]) + if !crypt_equal(hash[:], ovalue[:32]) { + crypto.zero_explicit(raw_data(enc.key), len(enc.key)) + return .Wrong_Password + } + enc.authenticated_as = .Owner + crypt_hash_r56(enc.revision, pw, ovalue[40:48], uvalue[:], intermediate[:]) + wrapped = dict_bytes(doc, ed, "OE") + } + + if len(wrapped) < 32 || len(enc.key) != 32 { + // Authenticated, but the wrapped file key is missing or truncated. + return .Bad_Encrypt_Dict + } + zero_iv: [16]byte + if !aes_cbc_decrypt(enc.key, wrapped[:32], intermediate[:], zero_iv[:]) { + return .Bad_Encrypt_Dict + } + + crypt_check_perms(doc, ed, enc) + return .None +} + +// crypt_check_perms is Algorithm 13: /Perms is the permission bits encrypted +// with the file key, so that they cannot be edited without it. A mismatch is +// recorded and ignored -- the document still decrypts, and refusing to open a +// file whose permissions were tampered with helps nobody. +@(private) +crypt_check_perms :: proc(doc: ^Document, ed: ^Dict, enc: ^Encryption) { + perms := dict_bytes(doc, ed, "Perms") + if len(perms) < 16 || len(enc.key) != aes.KEY_SIZE_256 { return } + + ctx: aes.Context_ECB + aes.init_ecb(&ctx, enc.key) + defer aes.reset_ecb(&ctx) + out: [16]byte + aes.decrypt_ecb(&ctx, out[:], perms[:16]) + + if out[9] != 'a' || out[10] != 'd' || out[11] != 'b' { + add_repair(doc, .Bad_Filter_Data, 0, enc.ref) + return + } + p := u32(out[0]) | u32(out[1]) << 8 | u32(out[2]) << 16 | u32(out[3]) << 24 + if transmute(i32)p != enc.permission_bits { + add_repair(doc, .Bad_Filter_Data, 0, enc.ref) + } +} + +// The hardening loop of Algorithm 2.B works on `password || K || udata` +// repeated 64 times. K is a SHA-2 digest, so at most 64 bytes; the password is +// truncated to 127 and udata is the 48-byte /U value. That caps one round at +// 239 * 64 = 15296 bytes, small enough to size the buffers for the worst case +// and keep the whole thing off the heap. +@(private) +CRYPT_R6_MAX_SEGMENT :: 127 + 64 + 48 +@(private) +CRYPT_R6_MAX_BLOCK :: CRYPT_R6_MAX_SEGMENT * 64 +// The exit test is `last byte of E <= round - 32`, and the last byte is at most +// 255, so round 288 ends the loop whatever the data does. The cap exists only +// so that a bug here cannot become a hang. +@(private) +CRYPT_R6_MAX_ROUNDS :: 512 + +// crypt_hash_r56 is Algorithm 2.B for R6, and the plain SHA-256 of R5. +// `out` receives 32 bytes. +@(private) +crypt_hash_r56 :: proc(revision: int, password, salt, udata: []byte, out: []byte) { + pw := password + if len(pw) > 127 { pw = pw[:127] } + ud := udata + if len(ud) > 48 { ud = ud[:48] } + + k: [64]byte + ctx: sha2.Context_256 + sha2.init_256(&ctx) + sha2.update(&ctx, pw) + sha2.update(&ctx, salt) + sha2.update(&ctx, ud) + sha2.final(&ctx, k[:32]) + + if revision >= 6 { + crypt_hash_2b(pw, ud, k[:]) + } + copy(out[:32], k[:32]) +} + +// crypt_hash_2b runs the hardening rounds of Algorithm 2.B in place on `k`, +// whose first 32 bytes arrive holding the initial SHA-256 and whose first 32 +// bytes leave holding the answer. +// +// It is a separate procedure so that its two 15 KB buffers are not on the stack +// of every R5 hash as well. +@(private) +crypt_hash_2b :: proc(pw, ud: []byte, k: []byte) { + k1: [CRYPT_R6_MAX_BLOCK]byte + e: [CRYPT_R6_MAX_BLOCK]byte + k_len := 32 + last: byte = 0 + + for round := 0; round < 64 || int(last) > round - 32; round += 1 { + if round >= CRYPT_R6_MAX_ROUNDS { break } + + seg := len(pw) + k_len + len(ud) + if seg <= 0 || seg > CRYPT_R6_MAX_SEGMENT { break } + off := 0 + copy(k1[off:], pw); off += len(pw) + copy(k1[off:], k[:k_len]); off += k_len + copy(k1[off:], ud) + for j in 1 ..< 64 { + copy(k1[j * seg:], k1[:seg]) + } + // seg * 64 is always a whole number of AES blocks, whatever seg is. + total := seg * 64 + + if !aes_cbc_encrypt(e[:total], k1[:total], k[:16], k[16:32]) { break } + + // The first 16 bytes of E as a big-endian integer, modulo 3. 256 is + // 1 mod 3, so the byte sum has the same residue. + sum := 0 + for z in 0 ..< 16 { + sum += int(e[z]) + } + switch sum % 3 { + case 0: + ctx: sha2.Context_256 + sha2.init_256(&ctx) + sha2.update(&ctx, e[:total]) + sha2.final(&ctx, k[:32]) + k_len = 32 + case 1: + ctx: sha2.Context_512 + sha2.init_384(&ctx) + sha2.update(&ctx, e[:total]) + sha2.final(&ctx, k[:48]) + k_len = 48 + case: + ctx: sha2.Context_512 + sha2.init_512(&ctx) + sha2.update(&ctx, e[:total]) + sha2.final(&ctx, k[:64]) + k_len = 64 + } + last = e[total - 1] + } +} + +// --------------------------------------------------------------------------- +// RC4 +// +// core has no ARC4 -- reasonably, since nothing new should use it -- so this is +// the whole of it. Keys here are 5 to 16 bytes. +// --------------------------------------------------------------------------- + +@(private) +Rc4_State :: struct { + s: [256]byte, + i, j: byte, +} + +@(private) +rc4_init :: proc "contextless" (ctx: ^Rc4_State, key: []byte) { + for i in 0 ..< 256 { + ctx.s[i] = byte(i) + } + ctx.i, ctx.j = 0, 0 + if len(key) == 0 { return } + + j: byte = 0 + for i in 0 ..< 256 { + j = j + ctx.s[i] + key[i % len(key)] + ctx.s[i], ctx.s[j] = ctx.s[j], ctx.s[i] + } +} + +// rc4_xor runs the keystream over `src` into `dst`. The two may be the same +// slice. Bytes past len(src) in dst are untouched. +@(private) +rc4_xor :: proc "contextless" (ctx: ^Rc4_State, dst, src: []byte) { + n := min(len(dst), len(src)) + i, j := ctx.i, ctx.j + for pos in 0 ..< n { + i += 1 + j += ctx.s[i] + ctx.s[i], ctx.s[j] = ctx.s[j], ctx.s[i] + dst[pos] = src[pos] ~ ctx.s[byte(ctx.s[i] + ctx.s[j])] + } + ctx.i, ctx.j = i, j +} + +// rc4_crypt is the one-shot form. RC4 is its own inverse, so this both +// encrypts and decrypts. An empty key passes the data through rather than +// dividing by zero. +@(private) +rc4_crypt :: proc "contextless" (key, dst, src: []byte) { + if len(key) == 0 { + copy(dst, src) + return + } + ctx: Rc4_State + rc4_init(&ctx, key) + rc4_xor(&ctx, dst, src) +} + +// --------------------------------------------------------------------------- +// AES-CBC +// +// core:crypto/aes has ECB, CTR and GCM but not CBC, which is the one mode PDF +// uses. Building it on encrypt_ecb/decrypt_ecb is a dozen lines and keeps the +// constant-time block implementation core provides. +// +// Neither of these does padding: PDF's PKCS#7 padding is handled by +// crypt_aes_decrypt, and Algorithm 2.B's inner loop is explicitly unpadded. +// --------------------------------------------------------------------------- + +@(private) +aes_key_size_ok :: proc "contextless" (key: []byte) -> bool { + switch len(key) { + case aes.KEY_SIZE_128, aes.KEY_SIZE_192, aes.KEY_SIZE_256: + return true + } + return false +} + +// aes_cbc_encrypt encrypts `src` into `dst`. `src` must be a whole number of +// blocks; `dst` must be at least as long. Returns false on a bad key size, a +// short destination or a partial final block -- never a panic, because every +// one of those can be reached from a file. +@(private) +aes_cbc_encrypt :: proc(dst, src, key, iv: []byte) -> bool { + if !aes_key_size_ok(key) { return false } + if len(iv) != aes.BLOCK_SIZE { return false } + if len(src) % aes.BLOCK_SIZE != 0 { return false } + if len(dst) < len(src) { return false } + + ctx: aes.Context_ECB + aes.init_ecb(&ctx, key) + defer aes.reset_ecb(&ctx) + + prev: [aes.BLOCK_SIZE]byte + copy(prev[:], iv) + block: [aes.BLOCK_SIZE]byte + for off := 0; off < len(src); off += aes.BLOCK_SIZE { + for i in 0 ..< aes.BLOCK_SIZE { + block[i] = src[off + i] ~ prev[i] + } + aes.encrypt_ecb(&ctx, dst[off:off + aes.BLOCK_SIZE], block[:]) + copy(prev[:], dst[off:off + aes.BLOCK_SIZE]) + } + return true +} + +// aes_cbc_decrypt decrypts `src` into `dst`, which may be the same slice. +@(private) +aes_cbc_decrypt :: proc(dst, src, key, iv: []byte) -> bool { + if !aes_key_size_ok(key) { return false } + if len(iv) != aes.BLOCK_SIZE { return false } + if len(src) % aes.BLOCK_SIZE != 0 { return false } + if len(dst) < len(src) { return false } + + ctx: aes.Context_ECB + aes.init_ecb(&ctx, key) + defer aes.reset_ecb(&ctx) + + prev: [aes.BLOCK_SIZE]byte + copy(prev[:], iv) + saved: [aes.BLOCK_SIZE]byte + for off := 0; off < len(src); off += aes.BLOCK_SIZE { + // Saved before decrypting, so that dst and src may alias. + copy(saved[:], src[off:off + aes.BLOCK_SIZE]) + aes.decrypt_ecb(&ctx, dst[off:off + aes.BLOCK_SIZE], src[off:off + aes.BLOCK_SIZE]) + for i in 0 ..< aes.BLOCK_SIZE { + dst[off + i] ~= prev[i] + } + copy(prev[:], saved[:]) + } + return true +} + +// crypt_equal compares in constant time. Password checks are the one place in +// this library where how long a comparison took is information. +@(private) +crypt_equal :: proc "contextless" (a, b: []byte) -> bool { + if len(a) != len(b) { return false } + diff: byte = 0 + for i in 0 ..< len(a) { + diff |= a[i] ~ b[i] + } + return diff == 0 +} diff --git a/crypt_test.odin b/crypt_test.odin new file mode 100644 index 0000000..960b76f --- /dev/null +++ b/crypt_test.odin @@ -0,0 +1,1537 @@ +package pdf + +import "core:fmt" +import "core:mem/virtual" +import "core:slice" +import "core:strings" +import "core:testing" + +// The RC4 keystreams below are RFC 6229's; the AES-CBC blocks are NIST +// SP 800-38A's. Everything that is specific to PDF -- the /O and /U values, the +// derived file keys, the per-object keys, the R6 hash -- was computed with an +// independent implementation (Python, cross-checked against PyPDF2, whose R6 +// hash comes from qpdf) and is pasted here as a literal. Nothing in this file +// asks crypt.odin to check its own work. + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +// The first /ID string used by every hand-built dictionary here: 10 11 .. 1f. +@(private = "file") +crypt_t_id0 := [16]byte{ + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, +} + +@(private = "file") +crypt_t_new_doc :: proc(t: ^testing.T) -> ^Document { + doc := new(Document) + if err := virtual.arena_init_growing(&doc.arena); err != nil { + testing.fail_now(t, "arena") + } + doc.allocator = virtual.arena_allocator(&doc.arena) + doc.backing = context.allocator + doc.xref = make([dynamic]Xref_Entry, doc.allocator) + doc.cache = make(map[u32]Object, 8, doc.allocator) + doc.loading = make(map[u32]bool, 4, doc.allocator) + doc.objstm_cache = make(map[u32]^Objstm, 4, doc.allocator) + doc.pages = make([dynamic]^Dict, doc.allocator) + doc.page_refs = make([dynamic]Ref, doc.allocator) + doc.repairs = make([dynamic]Repair, doc.allocator) + doc.trailer = new_dict(doc.allocator) + return doc +} + +// crypt_t_str stores a byte string in the document arena and returns it as a +// PDF string object, the way the parser would have. +@(private = "file") +crypt_t_str :: proc(doc: ^Document, val: []byte) -> Object { + b := make([]byte, len(val), doc.allocator) + copy(b, val) + return transmute(String)b +} + +// crypt_t_id installs a two-element /ID array in the trailer. +@(private = "file") +crypt_t_id :: proc(doc: ^Document, id0: []byte) { + a := new_array(doc.allocator, 2) + array_push(a, crypt_t_str(doc, id0)) + array_push(a, crypt_t_str(doc, id0)) + dict_set(doc.trailer, "ID", a) +} + +@(private = "file") +crypt_t_cf :: proc(doc: ^Document, cfm: string, length: i64) -> ^Dict { + cf := new_dict(doc.allocator, 2) + std := new_dict(doc.allocator, 4) + dict_set(std, "Type", Name("CryptFilter")) + dict_set(std, "CFM", Name(cfm)) + if length > 0 { + dict_set(std, "Length", length) + } + dict_set(cf, "StdCF", std) + return cf +} + +// --------------------------------------------------------------------------- +// RC4 -- RFC 6229 test vectors +// --------------------------------------------------------------------------- + +@(private = "file") +Rc4_Vector :: struct { + key: []byte, + at_0: []byte, // keystream bytes 0..15 + at_16: []byte, // keystream bytes 16..31 + at_240: []byte, // keystream bytes 240..255 + at_256: []byte, +} + +@(test) +test_crypt_rc4_rfc6229 :: proc(t: ^testing.T) { + vectors := []Rc4_Vector{ + { // 40-bit + key = {0x01, 0x02, 0x03, 0x04, 0x05}, + at_0 = {0xb2, 0x39, 0x63, 0x05, 0xf0, 0x3d, 0xc0, 0x27, 0xcc, 0xc3, 0x52, 0x4a, 0x0a, 0x11, 0x18, 0xa8}, + at_16 = {0x69, 0x82, 0x94, 0x4f, 0x18, 0xfc, 0x82, 0xd5, 0x89, 0xc4, 0x03, 0xa4, 0x7a, 0x0d, 0x09, 0x19}, + at_240 = {0x28, 0xcb, 0x11, 0x32, 0xc9, 0x6c, 0xe2, 0x86, 0x42, 0x1d, 0xca, 0xad, 0xb8, 0xb6, 0x9e, 0xae}, + at_256 = {0x1c, 0xfc, 0xf6, 0x2b, 0x03, 0xed, 0xdb, 0x64, 0x1d, 0x77, 0xdf, 0xcf, 0x7f, 0x8d, 0x8c, 0x93}, + }, + { // 56-bit + key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, + at_0 = {0x29, 0x3f, 0x02, 0xd4, 0x7f, 0x37, 0xc9, 0xb6, 0x33, 0xf2, 0xaf, 0x52, 0x85, 0xfe, 0xb4, 0x6b}, + at_16 = {0xe6, 0x20, 0xf1, 0x39, 0x0d, 0x19, 0xbd, 0x84, 0xe2, 0xe0, 0xfd, 0x75, 0x20, 0x31, 0xaf, 0xc1}, + at_240 = {0x91, 0x4f, 0x02, 0x53, 0x1c, 0x92, 0x18, 0x81, 0x0d, 0xf6, 0x0f, 0x67, 0xe3, 0x38, 0x15, 0x4c}, + at_256 = {0xd0, 0xfd, 0xb5, 0x83, 0x07, 0x3c, 0xe8, 0x5a, 0xb8, 0x39, 0x17, 0x74, 0x0e, 0xc0, 0x11, 0xd5}, + }, + { // 64-bit + key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, + at_0 = {0x97, 0xab, 0x8a, 0x1b, 0xf0, 0xaf, 0xb9, 0x61, 0x32, 0xf2, 0xf6, 0x72, 0x58, 0xda, 0x15, 0xa8}, + at_16 = {0x82, 0x63, 0xef, 0xdb, 0x45, 0xc4, 0xa1, 0x86, 0x84, 0xef, 0x87, 0xe6, 0xb1, 0x9e, 0x5b, 0x09}, + at_240 = {0x96, 0x36, 0xeb, 0xc9, 0x84, 0x19, 0x26, 0xf4, 0xf7, 0xd1, 0xf3, 0x62, 0xbd, 0xdf, 0x6e, 0x18}, + at_256 = {0xd0, 0xa9, 0x90, 0xff, 0x2c, 0x05, 0xfe, 0xf5, 0xb9, 0x03, 0x73, 0xc9, 0xff, 0x4b, 0x87, 0x0a}, + }, + { // 128-bit, the length every PDF above R2 uses + key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + at_0 = {0x9a, 0xc7, 0xcc, 0x9a, 0x60, 0x9d, 0x1e, 0xf7, 0xb2, 0x93, 0x28, 0x99, 0xcd, 0xe4, 0x1b, 0x97}, + at_16 = {0x52, 0x48, 0xc4, 0x95, 0x90, 0x14, 0x12, 0x6a, 0x6e, 0x8a, 0x84, 0xf1, 0x1d, 0x1a, 0x9e, 0x1c}, + at_240 = {0x06, 0x59, 0x02, 0xe4, 0xb6, 0x20, 0xf6, 0xcc, 0x36, 0xc8, 0x58, 0x9f, 0x66, 0x43, 0x2f, 0x2b}, + at_256 = {0xd3, 0x9d, 0x56, 0x6b, 0xc6, 0xbc, 0xe3, 0x01, 0x07, 0x68, 0x15, 0x15, 0x49, 0xf3, 0x87, 0x3f}, + }, + { // 40-bit, non-sequential key + key = {0x83, 0x32, 0x22, 0x77, 0x2a}, + at_0 = {0x80, 0xad, 0x97, 0xbd, 0xc9, 0x73, 0xdf, 0x8a, 0x2e, 0x87, 0x9e, 0x92, 0xa4, 0x97, 0xef, 0xda}, + at_16 = {0x20, 0xf0, 0x60, 0xc2, 0xf2, 0xe5, 0x12, 0x65, 0x01, 0xd3, 0xd4, 0xfe, 0xa1, 0x0d, 0x5f, 0xc0}, + at_240 = {0xfa, 0xa1, 0x48, 0xe9, 0x90, 0x46, 0x18, 0x1f, 0xec, 0x6b, 0x20, 0x85, 0xf3, 0xb2, 0x0e, 0xd9}, + at_256 = {0xf0, 0xda, 0xf5, 0xba, 0xb3, 0xd5, 0x96, 0x83, 0x98, 0x57, 0x84, 0x6f, 0x73, 0xfb, 0xfe, 0x5a}, + }, + } + + // Encrypting zeroes yields the keystream, which is what RFC 6229 tabulates. + stream: [272]byte + for v, i in vectors { + slice.zero(stream[:]) + rc4_crypt(v.key, stream[:], stream[:]) + testing.expectf(t, slice.equal(stream[0:16], v.at_0), + "vector %d offset 0: %x", i, stream[0:16]) + testing.expectf(t, slice.equal(stream[16:32], v.at_16), + "vector %d offset 16: %x", i, stream[16:32]) + testing.expectf(t, slice.equal(stream[240:256], v.at_240), + "vector %d offset 240: %x", i, stream[240:256]) + testing.expectf(t, slice.equal(stream[256:272], v.at_256), + "vector %d offset 256: %x", i, stream[256:272]) + } +} + +@(test) +test_crypt_rc4_classic :: proc(t: ^testing.T) { + Case :: struct { + key, plain, cipher: []byte, + } + cases := []Case{ + {transmute([]byte)string("Key"), transmute([]byte)string("Plaintext"), + {0xbb, 0xf3, 0x16, 0xe8, 0xd9, 0x40, 0xaf, 0x0a, 0xd3}}, + {transmute([]byte)string("Wiki"), transmute([]byte)string("pedia"), + {0x10, 0x21, 0xbf, 0x04, 0x20}}, + {transmute([]byte)string("Secret"), transmute([]byte)string("Attack at dawn"), + {0x45, 0xa0, 0x1f, 0x64, 0x5f, 0xc3, 0x5b, 0x38, 0x35, 0x52, 0x54, 0x4b, 0x9b, 0xf5}}, + } + out: [64]byte + for c, i in cases { + rc4_crypt(c.key, out[:len(c.plain)], c.plain) + testing.expectf(t, slice.equal(out[:len(c.plain)], c.cipher), + "case %d encrypt: %x", i, out[:len(c.plain)]) + // RC4 is an XOR with a keystream, so decryption is the same operation. + rc4_crypt(c.key, out[:len(c.cipher)], c.cipher) + testing.expectf(t, slice.equal(out[:len(c.plain)], c.plain), + "case %d decrypt: %x", i, out[:len(c.plain)]) + } +} + +@(test) +test_crypt_rc4_aliasing_and_empty_key :: proc(t: ^testing.T) { + // In-place is how decrypt_string uses it for the /O and /U round trips. + buf := [9]byte{'P', 'l', 'a', 'i', 'n', 't', 'e', 'x', 't'} + want := [9]byte{0xbb, 0xf3, 0x16, 0xe8, 0xd9, 0x40, 0xaf, 0x0a, 0xd3} + rc4_crypt(transmute([]byte)string("Key"), buf[:], buf[:]) + testing.expectf(t, slice.equal(buf[:], want[:]), "in place: %x", buf) + + // An empty key would divide by zero in the key schedule; it passes through. + src := [4]byte{1, 2, 3, 4} + dst: [4]byte + rc4_crypt(nil, dst[:], src[:]) + testing.expect(t, slice.equal(dst[:], src[:]), "empty key should pass data through") +} + +// --------------------------------------------------------------------------- +// AES-CBC -- NIST SP 800-38A test vectors +// --------------------------------------------------------------------------- + +@(private = "file") +crypt_t_nist_iv := [16]byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, +} + +@(private = "file") +crypt_t_nist_plain := [64]byte{ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a, + 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51, + 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef, + 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10, +} + +@(test) +test_crypt_aes_cbc_nist :: proc(t: ^testing.T) { + // SP 800-38A F.2.1 / F.2.2, AES-128. + key128 := [16]byte{ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, + } + want128 := [64]byte{ + 0x76, 0x49, 0xab, 0xac, 0x81, 0x19, 0xb2, 0x46, 0xce, 0xe9, 0x8e, 0x9b, 0x12, 0xe9, 0x19, 0x7d, + 0x50, 0x86, 0xcb, 0x9b, 0x50, 0x72, 0x19, 0xee, 0x95, 0xdb, 0x11, 0x3a, 0x91, 0x76, 0x78, 0xb2, + 0x73, 0xbe, 0xd6, 0xb8, 0xe3, 0xc1, 0x74, 0x3b, 0x71, 0x16, 0xe6, 0x9e, 0x22, 0x22, 0x95, 0x16, + 0x3f, 0xf1, 0xca, 0xa1, 0x68, 0x1f, 0xac, 0x09, 0x12, 0x0e, 0xca, 0x30, 0x75, 0x86, 0xe1, 0xa7, + } + // SP 800-38A F.2.5 / F.2.6, AES-256. + key256 := [32]byte{ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, + 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, + } + want256 := [64]byte{ + 0xf5, 0x8c, 0x4c, 0x04, 0xd6, 0xe5, 0xf1, 0xba, 0x77, 0x9e, 0xab, 0xfb, 0x5f, 0x7b, 0xfb, 0xd6, + 0x9c, 0xfc, 0x4e, 0x96, 0x7e, 0xdb, 0x80, 0x8d, 0x67, 0x9f, 0x77, 0x7b, 0xc6, 0x70, 0x2c, 0x7d, + 0x39, 0xf2, 0x33, 0x69, 0xa9, 0xd9, 0xba, 0xcf, 0xa5, 0x30, 0xe2, 0x63, 0x04, 0x23, 0x14, 0x61, + 0xb2, 0xeb, 0x05, 0xe2, 0xc3, 0x9b, 0xe9, 0xfc, 0xda, 0x6c, 0x19, 0x07, 0x8c, 0x6a, 0x9d, 0x1b, + } + + out: [64]byte + back: [64]byte + + testing.expect(t, aes_cbc_encrypt(out[:], crypt_t_nist_plain[:], key128[:], crypt_t_nist_iv[:])) + testing.expectf(t, slice.equal(out[:], want128[:]), "aes-128 encrypt: %x", out) + testing.expect(t, aes_cbc_decrypt(back[:], want128[:], key128[:], crypt_t_nist_iv[:])) + testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-128 decrypt: %x", back) + + testing.expect(t, aes_cbc_encrypt(out[:], crypt_t_nist_plain[:], key256[:], crypt_t_nist_iv[:])) + testing.expectf(t, slice.equal(out[:], want256[:]), "aes-256 encrypt: %x", out) + testing.expect(t, aes_cbc_decrypt(back[:], want256[:], key256[:], crypt_t_nist_iv[:])) + testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-256 decrypt: %x", back) + + // In-place decryption: crypt_aes_decrypt relies on dst and src aliasing + // being harmless. + copy(back[:], want256[:]) + testing.expect(t, aes_cbc_decrypt(back[:], back[:], key256[:], crypt_t_nist_iv[:])) + testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-256 in place: %x", back) +} + +@(test) +test_crypt_aes_cbc_rejects_bad_shapes :: proc(t: ^testing.T) { + key := [16]byte{} + iv := [16]byte{} + buf := [64]byte{} + + testing.expect(t, !aes_cbc_encrypt(buf[:], buf[:17], key[:], iv[:]), "partial block") + testing.expect(t, !aes_cbc_decrypt(buf[:], buf[:17], key[:], iv[:]), "partial block") + testing.expect(t, !aes_cbc_encrypt(buf[:15], buf[:16], key[:], iv[:]), "short destination") + testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], key[:7], iv[:]), "7-byte key") + testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], key[:], iv[:8]), "8-byte iv") + testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], nil, iv[:]), "no key") + // Zero blocks is legal and does nothing. + testing.expect(t, aes_cbc_encrypt(buf[:0], buf[:0], key[:], iv[:]), "empty input") +} + +// --------------------------------------------------------------------------- +// Key derivation, revisions 2 to 4 +// --------------------------------------------------------------------------- + +@(private = "file") +Kd_Case :: struct { + name: string, + version: i64, + revision: i64, + length: i64, // /Length, 0 to omit + cfm: string, // "" for V < 4 + permissions: i64, + encrypt_metadata: bool, + user_password: string, + owner_password: string, + o: []byte, + u: []byte, + want_key: []byte, + want_method: Crypt_Method, +} + +@(private = "file") +crypt_t_build :: proc(doc: ^Document, c: Kd_Case) -> ^Dict { + ed := new_dict(doc.allocator, 12) + dict_set(ed, "Filter", Name("Standard")) + dict_set(ed, "V", c.version) + dict_set(ed, "R", c.revision) + if c.length > 0 { + dict_set(ed, "Length", c.length) + } + dict_set(ed, "P", c.permissions) + dict_set(ed, "O", crypt_t_str(doc, c.o)) + dict_set(ed, "U", crypt_t_str(doc, c.u)) + if !c.encrypt_metadata { + dict_set(ed, "EncryptMetadata", false) + } + if c.cfm != "" { + dict_set(ed, "CF", crypt_t_cf(doc, c.cfm, 16)) + dict_set(ed, "StmF", Name("StdCF")) + dict_set(ed, "StrF", Name("StdCF")) + } + crypt_t_id(doc, crypt_t_id0[:]) + dict_set(doc.trailer, "Encrypt", ed) + return ed +} + +// The five revision/filter combinations, each with an /Encrypt dictionary +// built by an independent implementation. +@(private = "file") +crypt_t_cases := []Kd_Case{ + { + name = "R2 RC4 40-bit, empty user password", + version = 1, revision = 2, length = 0, cfm = "", + permissions = -1, encrypt_metadata = true, + user_password = "", owner_password = "owner", + o = {0xc9, 0x24, 0x22, 0x68, 0x7f, 0xac, 0xee, 0x68, 0x6e, 0x37, 0x3f, 0x10, + 0xb5, 0xc7, 0xd0, 0x47, 0x38, 0x05, 0x31, 0x52, 0xf7, 0xe2, 0xee, 0x30, + 0xe1, 0x1c, 0x69, 0xec, 0x44, 0x25, 0x76, 0xab}, + u = {0x53, 0xbc, 0x45, 0x8d, 0xf0, 0xc3, 0x97, 0xc5, 0xf6, 0xd1, 0xaa, 0x43, + 0xe3, 0x59, 0x33, 0x3f, 0x52, 0xd3, 0xaa, 0x81, 0xc2, 0xab, 0xf0, 0xe9, + 0xc1, 0x1f, 0x59, 0x2c, 0xb3, 0xad, 0x8c, 0xe5}, + want_key = {0xdd, 0xc8, 0xcf, 0x63, 0x76}, + want_method = .Rc4, + }, + { + name = "R3 RC4 128-bit, empty user password", + version = 2, revision = 3, length = 128, cfm = "", + permissions = -4, encrypt_metadata = true, + user_password = "", owner_password = "owner", + o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, + 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, + 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, + u = {0xc3, 0x60, 0xd1, 0xa9, 0xe2, 0xbe, 0x8a, 0xde, 0x6b, 0x1f, 0x07, 0xcc, + 0x97, 0xa5, 0x0a, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want_key = {0xf0, 0x4b, 0x6a, 0x74, 0x07, 0x17, 0x51, 0x50, 0x48, 0xf1, 0xe6, 0x38, + 0x8e, 0x8a, 0xb2, 0x77}, + want_method = .Rc4, + }, + { + name = "R3 RC4 40-bit, user password 'user'", + version = 2, revision = 3, length = 40, cfm = "", + permissions = -64, encrypt_metadata = true, + user_password = "user", owner_password = "owner", + o = {0x3c, 0x48, 0x21, 0x62, 0x00, 0x8f, 0xaf, 0xcb, 0x22, 0x8b, 0x7d, 0xb3, + 0xc4, 0x3a, 0x10, 0x90, 0xbc, 0x5b, 0x56, 0xe9, 0xb1, 0x55, 0x6e, 0x89, + 0xfc, 0x06, 0x56, 0xfd, 0x29, 0x1f, 0x49, 0x08}, + u = {0x42, 0xf1, 0xc3, 0x99, 0x3f, 0x9b, 0xa0, 0x35, 0x59, 0x22, 0x27, 0x0c, + 0xb4, 0xf0, 0x49, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want_key = {0x33, 0x4c, 0xc2, 0x25, 0xc5}, + want_method = .Rc4, + }, + { + name = "R4 crypt filter /V2 (RC4 128-bit)", + version = 4, revision = 4, length = 128, cfm = "V2", + permissions = -1340, encrypt_metadata = true, + user_password = "", owner_password = "owner", + o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, + 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, + 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, + u = {0xa5, 0x62, 0xc9, 0x4b, 0x96, 0x94, 0x67, 0x3f, 0x4b, 0x05, 0xcd, 0xfc, + 0xe9, 0x9a, 0x0d, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want_key = {0x3f, 0xe3, 0xcb, 0x9a, 0x5d, 0xea, 0x1e, 0xac, 0x1c, 0xea, 0xb4, 0xe4, + 0xb4, 0x21, 0x23, 0xfb}, + want_method = .Rc4, + }, + { + // /EncryptMetadata false: the four 0xFF bytes fold into the key, so + // the same password and the same /O give a different key. + name = "R4 crypt filter /AESV2, EncryptMetadata false", + version = 4, revision = 4, length = 128, cfm = "AESV2", + permissions = -1340, encrypt_metadata = false, + user_password = "", owner_password = "owner", + o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, + 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, + 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, + u = {0x18, 0x15, 0x6a, 0x0f, 0x71, 0x2b, 0xdc, 0x8c, 0x21, 0x03, 0xd3, 0xf0, + 0x50, 0x3a, 0xf0, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want_key = {0xd8, 0x1a, 0x8d, 0x4a, 0x89, 0x5f, 0x5a, 0xeb, 0x51, 0xfa, 0x19, 0xb8, + 0x91, 0x91, 0x37, 0x34}, + want_method = .Aes_128, +}, +} + +@(test) +test_crypt_key_derivation :: proc(t: ^testing.T) { + for c in crypt_t_cases { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, c) + + err := crypt_init(doc, transmute([]byte)c.user_password) + testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) + if doc.encrypt == nil { + testing.expectf(t, false, "%s: no encryption state", c.name) + continue + } + testing.expectf(t, slice.equal(doc.encrypt.key, c.want_key), + "%s: key = %x, want %x", c.name, doc.encrypt.key, c.want_key) + testing.expectf(t, doc.encrypt.authenticated_as == .User, + "%s: authenticated as %v", c.name, doc.encrypt.authenticated_as) + testing.expectf(t, doc.encrypt.stream_method == c.want_method, + "%s: stream method %v", c.name, doc.encrypt.stream_method) + testing.expectf(t, doc.encrypt.revision == int(c.revision), + "%s: revision %d", c.name, doc.encrypt.revision) + } +} + +@(test) +test_crypt_owner_password :: proc(t: ^testing.T) { + // The same documents, opened with the owner password instead. The file key + // is identical -- /O only ever encodes the user password. + for c in crypt_t_cases { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, c) + + err := crypt_init(doc, transmute([]byte)c.owner_password) + testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) + if doc.encrypt == nil { continue } + testing.expectf(t, doc.encrypt.authenticated_as == .Owner, + "%s: authenticated as %v, want Owner", c.name, doc.encrypt.authenticated_as) + testing.expectf(t, slice.equal(doc.encrypt.key, c.want_key), + "%s: owner key = %x, want %x", c.name, doc.encrypt.key, c.want_key) + } +} + +@(test) +test_crypt_wrong_password :: proc(t: ^testing.T) { + for c in crypt_t_cases { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, c) + + err := crypt_init(doc, transmute([]byte)string("definitely not it")) + testing.expectf(t, err == .Wrong_Password, "%s: err = %v", c.name, err) + testing.expectf(t, doc.encrypt == nil, + "%s: encryption state left behind after a failed password", c.name) + } + + // A file with a non-empty user password must not open on the empty one. + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[2]) + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "empty password accepted") +} + +// --------------------------------------------------------------------------- +// Per-object keys (Algorithm 1) +// --------------------------------------------------------------------------- + +@(test) +test_crypt_object_key :: proc(t: ^testing.T) { + enc128 := Encryption{ + key = []byte{0xf0, 0x4b, 0x6a, 0x74, 0x07, 0x17, 0x51, 0x50, + 0x48, 0xf1, 0xe6, 0x38, 0x8e, 0x8a, 0xb2, 0x77}, + } + enc40 := Encryption{ + key = []byte{0xdd, 0xc8, 0xcf, 0x63, 0x76}, + } + enc256 := Encryption{ + key = []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }, + } + + Case :: struct { + name: string, + enc: ^Encryption, + method: Crypt_Method, + ref: Ref, + want: []byte, + } + e128, e40, e256 := enc128, enc40, enc256 + cases := []Case{ + {"128-bit RC4, object 7 gen 0", &e128, .Rc4, {7, 0}, + {0xaa, 0x9b, 0xec, 0x87, 0xa8, 0x18, 0xe4, 0xcb, 0x60, 0x88, 0x96, 0x3e, + 0x64, 0xc3, 0x11, 0x41}}, + // The "sAlT" suffix is the only difference from the line above. + {"128-bit AES, object 7 gen 0", &e128, .Aes_128, {7, 0}, + {0x58, 0x2d, 0xb9, 0xf3, 0xe9, 0xca, 0xe2, 0x66, 0xed, 0x0a, 0x49, 0x07, + 0xa3, 0x6e, 0xbd, 0x22}}, + // n + 5 = 10 bytes for a 40-bit file key, not 16. + {"40-bit RC4, object 3 gen 5", &e40, .Rc4, {3, 5}, + {0x42, 0x42, 0x24, 0x85, 0x5e, 0xc1, 0x48, 0x3e, 0x0e, 0xd4}}, + // Only the low three bytes of the number and the low two of the + // generation take part. + {"128-bit RC4, object 0xABCDEF gen 0x1234", &e128, .Rc4, {0xABCDEF, 0x1234}, + {0x18, 0x02, 0xdf, 0xb8, 0x5c, 0x35, 0x6b, 0x7e, 0x00, 0xe6, 0x37, 0x9a, + 0xe3, 0xeb, 0x73, 0xc1}}, + // AES-256 uses the file key untouched for every object. + {"256-bit AES uses the file key", &e256, .Aes_256, {7, 0}, e256.key}, + } + + out: [32]byte + for c in cases { + n := crypt_object_key(c.enc, c.method, c.ref, out[:]) + testing.expectf(t, n == len(c.want), "%s: %d bytes, want %d", c.name, n, len(c.want)) + if n != len(c.want) { continue } + testing.expectf(t, slice.equal(out[:n], c.want), "%s: %x", c.name, out[:n]) + } + + // A file key of an impossible length yields nothing rather than a bad key. + bad := Encryption{key = make([]byte, 20)} + defer delete(bad.key) + testing.expect(t, crypt_object_key(&bad, .Rc4, {1, 0}, out[:]) == 0, "20-byte RC4 key") + testing.expect(t, crypt_object_key(&bad, .Aes_256, {1, 0}, out[:]) == 0, "20-byte AES-256 key") + empty := Encryption{} + testing.expect(t, crypt_object_key(&empty, .Rc4, {1, 0}, out[:]) == 0, "empty key") +} + +// --------------------------------------------------------------------------- +// End-to-end decryption +// --------------------------------------------------------------------------- + +@(test) +test_crypt_decrypt_rc4_string :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[1]) // R3, RC4 128-bit + testing.expect(t, crypt_init(doc, nil) == .None) + + // Produced outside this library from the same file key. + cipher := []byte{ + 0x0b, 0x73, 0xcf, 0xc8, 0x51, 0x25, 0x25, 0x2c, 0x20, 0x46, + 0x94, 0x78, 0x4d, 0xb0, 0x30, 0x40, 0xa0, 0xf3, 0x3e, + } + want := transmute([]byte)string("The quick brown fox") + + got := decrypt_string(doc, Ref{7, 0}, cipher) + testing.expectf(t, slice.equal(got, want), "string: %q", string(got)) + + // The stream filter is the same one here, so the same bytes decrypt the + // same way through the stream path. + got = decrypt_stream(doc, Ref{7, 0}, cipher) + testing.expectf(t, slice.equal(got, want), "stream: %q", string(got)) + + // A different object number is a different key, so it must not decrypt. + got = decrypt_string(doc, Ref{8, 0}, cipher) + testing.expect(t, !slice.equal(got, want), "object 8 decrypted with object 7's key") +} + +@(test) +test_crypt_decrypt_aes128_string :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[4]) // R4, AESV2, EncryptMetadata false + testing.expect(t, crypt_init(doc, nil) == .None) + + // 16-byte IV, then AES-128-CBC over PKCS#7-padded plaintext. + cipher := []byte{ + 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, + 0x03, 0x02, 0x01, 0x00, 0x47, 0x35, 0x63, 0xcc, 0xd0, 0x02, 0xf6, 0x35, + 0xe9, 0xde, 0xb8, 0xb2, 0x5d, 0x64, 0x56, 0xe5, 0x58, 0x5a, 0xa1, 0xef, + 0xc3, 0xfe, 0xa0, 0x89, 0xc1, 0x84, 0xd0, 0xa1, 0xae, 0x17, 0x2e, 0x6c, + } + want := transmute([]byte)string("Hello, encrypted PDF!") + + got := decrypt_string(doc, Ref{7, 0}, cipher) + testing.expectf(t, slice.equal(got, want), "%q", string(got)) + // 21 bytes of plaintext in 32 bytes of ciphertext: the padding is gone. + testing.expectf(t, len(got) == 21, "length %d", len(got)) +} + +@(test) +test_crypt_never_decrypted :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + + // Make /Encrypt indirect, the way real files write it, so crypt_init can + // record which object must never be decrypted. + doc.cache[9] = ed + dict_set(doc.trailer, "Encrypt", Ref{9, 0}) + testing.expect(t, crypt_init(doc, nil) == .None) + testing.expect(t, doc.encrypt.ref == Ref{9, 0}, "encrypt ref not recorded") + + data := []byte{1, 2, 3, 4, 5, 6, 7, 8} + // The /Encrypt dictionary's own strings are the input to the key. + testing.expect(t, slice.equal(decrypt_string(doc, Ref{9, 0}, data), data), + "/Encrypt dictionary was decrypted") + // Ref{0,0} is an object that came out of an object stream and was already + // decrypted with its container. + testing.expect(t, slice.equal(decrypt_string(doc, Ref{0, 0}, data), data), + "object-stream member was decrypted twice") + // Empty input never allocates. + testing.expect(t, len(decrypt_string(doc, Ref{7, 0}, nil)) == 0, "empty input") +} + +// --------------------------------------------------------------------------- +// Crypt filters +// --------------------------------------------------------------------------- + +@(test) +test_crypt_identity_filter :: proc(t: ^testing.T) { + // /StmF /Identity with a real /StrF: strings are encrypted, streams are not. + doc := crypt_t_new_doc(t) + defer close(doc) + c := crypt_t_cases[3] // R4 /V2 + ed := crypt_t_build(doc, c) + dict_set(ed, "StmF", Name("Identity")) + + testing.expect(t, crypt_init(doc, nil) == .None) + testing.expect(t, doc.encrypt.stream_method == .Identity, "stream method") + testing.expect(t, doc.encrypt.string_method == .Rc4, "string method") + + data := []byte{'n', 'o', 't', ' ', 'c', 'i', 'p', 'h', 'e', 'r'} + testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, data), data), + "/Identity stream filter modified the data") + testing.expect(t, !slice.equal(decrypt_string(doc, Ref{7, 0}, data), data), + "/StrF should still have decrypted") + + // A /CFM of /None is the other spelling of the same thing. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + ed2 := crypt_t_build(doc2, c) + dict_set(ed2, "CF", crypt_t_cf(doc2, "None", 0)) + testing.expect(t, crypt_init(doc2, nil) == .None) + testing.expect(t, doc2.encrypt.stream_method == .Identity, "/CFM /None stream") + testing.expect(t, slice.equal(decrypt_stream(doc2, Ref{7, 0}, data), data), + "/CFM /None modified the data") +} + +@(test) +test_crypt_embedded_files_only :: proc(t: ^testing.T) { + // The "encrypt the attachments, leave the document readable" configuration: + // /StmF and /StrF are /Identity and only /EFF names a filter. Page content + // must come through untouched. + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[3]) + dict_set(ed, "StmF", Name("Identity")) + dict_set(ed, "StrF", Name("Identity")) + dict_set(ed, "EFF", Name("StdCF")) + + testing.expect(t, crypt_init(doc, nil) == .None) + testing.expect(t, doc.encrypt.stream_method == .Identity, "stream method") + testing.expect(t, doc.encrypt.string_method == .Identity, "string method") + testing.expect(t, doc.encrypt.embedded_method == .Rc4, "embedded method") + + content := transmute([]byte)string("BT /F1 12 Tf (hi) Tj ET") + testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, content), content), + "page content was decrypted by an embedded-file-only filter") + testing.expect(t, slice.equal(decrypt_string(doc, Ref{7, 0}, content), content), + "a string was decrypted by an embedded-file-only filter") + testing.expect(t, !slice.equal(decrypt_embedded_file(doc, Ref{7, 0}, content), content), + "an embedded file was not decrypted") +} + +@(test) +test_crypt_filter_edge_cases :: proc(t: ^testing.T) { + // /StmF naming a filter /CF does not define: degrade to Identity and record + // the damage rather than refusing the file. + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[3]) + dict_set(ed, "StmF", Name("NoSuchFilter")) + testing.expect(t, crypt_init(doc, nil) == .None) + testing.expect(t, doc.encrypt.stream_method == .Identity, "missing filter") + testing.expect(t, doc.is_damaged, "missing crypt filter should be recorded") + + // An unrecognised /CFM is a scheme we would silently mangle every object + // with, so it is an error. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + ed2 := crypt_t_build(doc2, crypt_t_cases[3]) + dict_set(ed2, "CF", crypt_t_cf(doc2, "AESV9", 32)) + testing.expect(t, crypt_init(doc2, nil) == .Unsupported_Encryption, "unknown /CFM") + testing.expect(t, doc2.encrypt == nil, "state left behind") + + // A crypt filter /Length written in bits instead of bytes. 128 and 16 must + // both give a 16-byte key, which is what makes the case in crypt_t_cases + // (which writes 16) and this one agree. + doc3 := crypt_t_new_doc(t) + defer close(doc3) + ed3 := crypt_t_build(doc3, crypt_t_cases[3]) + dict_set(ed3, "CF", crypt_t_cf(doc3, "V2", 128)) + testing.expect(t, crypt_init(doc3, nil) == .None) + testing.expectf(t, len(doc3.encrypt.key) == 16, "key length %d", len(doc3.encrypt.key)) + testing.expect(t, slice.equal(doc3.encrypt.key, crypt_t_cases[3].want_key), "key") +} + +// --------------------------------------------------------------------------- +// Revisions 5 and 6 (AES-256) +// --------------------------------------------------------------------------- + +@(private = "file") +crypt_t_filekey := [32]byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, +} + +@(private = "file") +R56_Case :: struct { + name: string, + revision: i64, + o, oe: []byte, + u, ue: []byte, + perms: []byte, +} + +@(private = "file") +crypt_t_r56_cases := []R56_Case{ + { + name = "R5 (deprecated Adobe extension, bare SHA-256)", + revision = 5, + u = {0x65, 0x88, 0x6c, 0x9f, 0x45, 0x07, 0xdf, 0x2b, 0x52, 0x6c, 0x2b, 0xcb, + 0x9b, 0x33, 0xcd, 0x5d, 0xba, 0xb3, 0x88, 0xf3, 0x14, 0x3e, 0x03, 0x9e, + 0x01, 0x20, 0x83, 0x49, 0x29, 0xc7, 0x04, 0xca, 0xa1, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8}, + ue = {0x69, 0xc8, 0xe0, 0xaf, 0x10, 0x2e, 0x3f, 0xb1, 0x89, 0x9f, 0x4b, 0x93, + 0x06, 0x25, 0xb0, 0xb3, 0xcc, 0x90, 0x20, 0xf6, 0x3c, 0x77, 0x4a, 0x21, + 0x3b, 0x8c, 0xb4, 0xe3, 0x0f, 0x0e, 0xbe, 0xcd}, + o = {0xf2, 0x1b, 0xbf, 0xb1, 0x73, 0xd3, 0xa7, 0xfb, 0x7f, 0xad, 0xdd, 0x08, + 0x6f, 0x6f, 0x05, 0xe4, 0xc9, 0xb8, 0xff, 0x3a, 0x51, 0xbe, 0x6f, 0x1c, + 0x81, 0xf2, 0x1b, 0x94, 0x0f, 0xa5, 0xac, 0x9b, 0xc1, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8}, + oe = {0x67, 0x0b, 0x2b, 0x88, 0xaf, 0x0d, 0x7e, 0xd9, 0x3c, 0x14, 0x33, 0x66, + 0xb1, 0x1e, 0x7e, 0xad, 0xf4, 0xf7, 0x7b, 0xaa, 0x68, 0x2f, 0x41, 0x07, + 0x87, 0xb7, 0xfc, 0x7e, 0x34, 0x71, 0xac, 0x87}, + perms = {0xb4, 0x5b, 0xaa, 0xc2, 0x1d, 0xad, 0xf0, 0x36, 0x8e, 0xe2, 0x6f, 0xd5, + 0x91, 0x2c, 0xae, 0xa8}, + }, + { + name = "R6 (ISO 32000-2, Algorithm 2.B)", + revision = 6, + u = {0xed, 0x88, 0x42, 0x72, 0x92, 0x52, 0x99, 0x61, 0xae, 0xd2, 0x36, 0xc4, + 0x6f, 0xa5, 0x10, 0xb2, 0x69, 0xbe, 0xdf, 0xf2, 0xbe, 0xb2, 0xf0, 0x19, + 0x6f, 0xce, 0x1e, 0x7a, 0xe3, 0x50, 0x8a, 0x15, 0xa1, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8}, + ue = {0x40, 0x68, 0xff, 0xf1, 0xbf, 0xab, 0xf4, 0x98, 0x48, 0xd9, 0x78, 0x54, + 0x92, 0xdd, 0x59, 0xda, 0x97, 0x4d, 0x44, 0x19, 0x74, 0x9f, 0x58, 0x1f, + 0xff, 0x12, 0x97, 0xb9, 0x73, 0x9f, 0x1e, 0x9a}, + o = {0xb9, 0xa6, 0x58, 0x86, 0x99, 0x3b, 0xd1, 0x65, 0xbe, 0x49, 0x92, 0x6b, + 0x72, 0x8c, 0x5c, 0x4f, 0x56, 0xba, 0xf2, 0xa0, 0xee, 0x47, 0x82, 0x33, + 0x25, 0xc6, 0xee, 0x8f, 0xfa, 0xd6, 0x05, 0xdb, 0xc1, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8}, + oe = {0xb4, 0x80, 0x18, 0xe0, 0xe3, 0x46, 0xc8, 0x2d, 0x90, 0xb0, 0xed, 0xcf, + 0xcc, 0x1c, 0x31, 0x38, 0xea, 0x8a, 0x47, 0x6b, 0x34, 0x02, 0xf8, 0x0e, + 0x42, 0x4e, 0x89, 0x40, 0xc2, 0x73, 0x5d, 0x4b}, + perms = {0xb4, 0x5b, 0xaa, 0xc2, 0x1d, 0xad, 0xf0, 0x36, 0x8e, 0xe2, 0x6f, 0xd5, + 0x91, 0x2c, 0xae, 0xa8}, +}, +} + +@(private = "file") +crypt_t_build_r56 :: proc(doc: ^Document, c: R56_Case) -> ^Dict { + ed := new_dict(doc.allocator, 12) + dict_set(ed, "Filter", Name("Standard")) + dict_set(ed, "V", i64(5)) + dict_set(ed, "R", c.revision) + dict_set(ed, "Length", i64(256)) + dict_set(ed, "P", i64(-3904)) + dict_set(ed, "O", crypt_t_str(doc, c.o)) + dict_set(ed, "U", crypt_t_str(doc, c.u)) + dict_set(ed, "OE", crypt_t_str(doc, c.oe)) + dict_set(ed, "UE", crypt_t_str(doc, c.ue)) + dict_set(ed, "Perms", crypt_t_str(doc, c.perms)) + dict_set(ed, "CF", crypt_t_cf(doc, "AESV3", 32)) + dict_set(ed, "StmF", Name("StdCF")) + dict_set(ed, "StrF", Name("StdCF")) + // /ID takes no part in the R5/R6 key, but real files still carry one. + crypt_t_id(doc, crypt_t_id0[:]) + dict_set(doc.trailer, "Encrypt", ed) + return ed +} + +@(test) +test_crypt_aes256 :: proc(t: ^testing.T) { + for c in crypt_t_r56_cases { + // User password (empty). + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build_r56(doc, c) + + err := crypt_init(doc, nil) + testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) + if doc.encrypt == nil { + testing.expectf(t, false, "%s: no encryption state", c.name) + continue + } + testing.expectf(t, slice.equal(doc.encrypt.key, crypt_t_filekey[:]), + "%s: user key = %x", c.name, doc.encrypt.key) + testing.expectf(t, doc.encrypt.authenticated_as == .User, + "%s: %v", c.name, doc.encrypt.authenticated_as) + testing.expectf(t, doc.encrypt.stream_method == .Aes_256, "%s: method", c.name) + // /Perms decrypts to the same /P, so nothing should have been repaired. + testing.expectf(t, !doc.is_damaged, "%s: /Perms check failed", c.name) + + // Owner password: a different wrapped key, the same file key. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + crypt_t_build_r56(doc2, c) + err = crypt_init(doc2, transmute([]byte)string("owner")) + testing.expectf(t, err == .None, "%s owner: crypt_init = %v", c.name, err) + if doc2.encrypt == nil { continue } + testing.expectf(t, slice.equal(doc2.encrypt.key, crypt_t_filekey[:]), + "%s: owner key = %x", c.name, doc2.encrypt.key) + testing.expectf(t, doc2.encrypt.authenticated_as == .Owner, + "%s owner: %v", c.name, doc2.encrypt.authenticated_as) + + // Wrong password. + doc3 := crypt_t_new_doc(t) + defer close(doc3) + crypt_t_build_r56(doc3, c) + testing.expectf(t, crypt_init(doc3, transmute([]byte)string("nope")) == .Wrong_Password, + "%s: wrong password accepted", c.name) + } +} + +@(test) +test_crypt_aes256_stream :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build_r56(doc, crypt_t_r56_cases[1]) // R6 + testing.expect(t, crypt_init(doc, nil) == .None) + + // Encrypted outside this library with the file key directly: AES-256 does + // not derive a per-object key, so the object number is irrelevant. + cipher := []byte{ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x00, 0x3a, 0xee, 0xe0, 0x20, 0x26, 0xc8, 0x6a, 0xf8, + 0xe6, 0x2d, 0x73, 0xc1, 0xcc, 0xe3, 0xed, 0x73, 0x41, 0x0a, 0x22, 0x1b, + 0x82, 0x91, 0x46, 0x04, 0x97, 0x5d, 0x3a, 0xaa, 0x12, 0x75, 0x20, 0x57, + } + want := transmute([]byte)string("R6 stream payload") + testing.expectf(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, cipher), want), + "object 7: %q", string(decrypt_stream(doc, Ref{7, 0}, cipher))) + testing.expectf(t, slice.equal(decrypt_stream(doc, Ref{4001, 9}, cipher), want), + "object 4001: %q", string(decrypt_stream(doc, Ref{4001, 9}, cipher))) +} + +@(test) +test_crypt_r6_hash :: proc(t: ^testing.T) { + Case :: struct { + revision: int, + password: string, + salt: []byte, + udata: []byte, + want: []byte, + } + cases := []Case{ + { // Algorithm 2.B with an empty password: the loop still runs. + 6, "", {0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8}, nil, + {0xed, 0x88, 0x42, 0x72, 0x92, 0x52, 0x99, 0x61, 0xae, 0xd2, 0x36, 0xc4, + 0x6f, 0xa5, 0x10, 0xb2, 0x69, 0xbe, 0xdf, 0xf2, 0xbe, 0xb2, 0xf0, 0x19, + 0x6f, 0xce, 0x1e, 0x7a, 0xe3, 0x50, 0x8a, 0x15}, + }, + { + 6, "password", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, nil, + {0xbb, 0xa2, 0xc9, 0xd8, 0xe7, 0x68, 0x12, 0x06, 0x75, 0x1a, 0x9b, 0xb3, + 0x9f, 0x2c, 0xf5, 0xeb, 0x40, 0x8f, 0x48, 0xfc, 0x2d, 0x3a, 0x44, 0xd1, + 0x53, 0xbc, 0x2f, 0xff, 0xaf, 0x9c, 0xc8, 0x7c}, + }, + { // R5 is the same input hashed once. + 5, "password", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, nil, + {0xab, 0x47, 0xa5, 0x51, 0xc8, 0x47, 0x88, 0x48, 0x19, 0x01, 0x9c, 0x30, + 0xe7, 0xb5, 0x0c, 0xb3, 0xa2, 0x6d, 0xf8, 0xbe, 0x39, 0xfb, 0xbf, 0x39, + 0x43, 0xc6, 0x1c, 0x55, 0x47, 0xfc, 0x55, 0xf7}, + }, + } + // The owner check feeds the whole 48-byte /U value in as udata. + udata: [48]byte + for i in 0 ..< 48 { udata[i] = byte(i) } + owner_case := Case{ + 6, "owner", {0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8}, udata[:], + {0xf9, 0x27, 0xe1, 0xbc, 0xd5, 0x68, 0x60, 0x51, 0xb0, 0x6e, 0xd7, 0x55, + 0x55, 0x73, 0x65, 0xeb, 0xf0, 0x0c, 0x92, 0x2d, 0x88, 0x89, 0xb4, 0x67, + 0xed, 0x5c, 0xff, 0xa0, 0x89, 0xb8, 0x8d, 0xdd}, + } + + out: [32]byte + for c in cases { + crypt_hash_r56(c.revision, transmute([]byte)c.password, c.salt, c.udata, out[:]) + testing.expectf(t, slice.equal(out[:], c.want), + "R%d %q: %x", c.revision, c.password, out) + } + crypt_hash_r56(owner_case.revision, transmute([]byte)owner_case.password, + owner_case.salt, owner_case.udata, out[:]) + testing.expectf(t, slice.equal(out[:], owner_case.want), "owner hash: %x", out) +} + +@(test) +test_crypt_r6_long_password :: proc(t: ^testing.T) { + // A password over 127 bytes is truncated, so 127 and 200 bytes of the same + // character must hash identically. This is also the case that would blow + // the fixed buffers in Algorithm 2.B if the truncation were missing. + long: [200]byte + for i in 0 ..< len(long) { long[i] = 'x' } + + a, b: [32]byte + crypt_hash_r56(6, long[:127], {1, 2, 3, 4, 5, 6, 7, 8}, nil, a[:]) + crypt_hash_r56(6, long[:], {1, 2, 3, 4, 5, 6, 7, 8}, nil, b[:]) + testing.expect(t, slice.equal(a[:], b[:]), "a >127-byte password was not truncated") + + crypt_hash_r56(6, long[:126], {1, 2, 3, 4, 5, 6, 7, 8}, nil, b[:]) + testing.expect(t, !slice.equal(a[:], b[:]), "126 and 127 bytes hashed the same") +} + +// --------------------------------------------------------------------------- +// Malformed input +// --------------------------------------------------------------------------- + +@(test) +test_crypt_aes_short_and_garbage :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[4]) // R4 /AESV2 + testing.expect(t, crypt_init(doc, nil) == .None) + + junk: [64]byte + for i in 0 ..< len(junk) { junk[i] = byte(i * 37 + 11) } + + // Every length up to and including a full block plus a partial one. None of + // these can be a valid payload, and none of them may crash. + for n in 0 ..= 47 { + got := decrypt_string(doc, Ref{7, 0}, junk[:n]) + if n == 0 { + testing.expect(t, len(got) == 0, "empty in, empty out") + continue + } + if n < 32 { + testing.expectf(t, len(got) == 0, "length %d gave %d bytes", n, len(got)) + } else { + // 32..47 decrypts one or two blocks of noise; the padding check + // almost always rejects it, and when it does not the result is + // still bounded by the input. + testing.expectf(t, len(got) < n, "length %d gave %d bytes", n, len(got)) + } + } + + // Ciphertext that decrypts cleanly but to bytes 00..0f, whose last byte is + // 0x0f while the preceding bytes are not: invalid PKCS#7. + bad_pad := []byte{ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x00, 0x4e, 0x89, 0x37, 0x98, 0x56, 0xc5, 0x2e, 0x44, + 0x57, 0x60, 0x84, 0x6c, 0xac, 0x7a, 0xba, 0x62, + } + doc6 := crypt_t_new_doc(t) + defer close(doc6) + crypt_t_build_r56(doc6, crypt_t_r56_cases[1]) + testing.expect(t, crypt_init(doc6, nil) == .None) + testing.expect(t, len(decrypt_stream(doc6, Ref{7, 0}, bad_pad)) == 0, + "invalid PKCS#7 padding should decrypt to nothing") + testing.expect(t, doc6.is_damaged, "a bad payload should be recorded as a repair") +} + +@(test) +test_crypt_malformed_dictionaries :: proc(t: ^testing.T) { + // No /Encrypt at all. + { + doc := crypt_t_new_doc(t) + defer close(doc) + testing.expect(t, crypt_init(doc, nil) == .None, "unencrypted") + testing.expect(t, doc.encrypt == nil, "unencrypted document has encryption state") + data := []byte{1, 2, 3} + testing.expect(t, slice.equal(decrypt_string(doc, Ref{1, 0}, data), data), "passthrough") + testing.expect(t, slice.equal(decrypt_stream(doc, Ref{1, 0}, data), data), "passthrough") + testing.expect(t, !document_is_encrypted(doc), "document_is_encrypted") + } + // /Encrypt pointing at an object that is not there. + { + doc := crypt_t_new_doc(t) + defer close(doc) + dict_set(doc.trailer, "Encrypt", Ref{99, 0}) + testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "dangling /Encrypt") + } + // /Encrypt that is a number. + { + doc := crypt_t_new_doc(t) + defer close(doc) + dict_set(doc.trailer, "Encrypt", i64(42)) + testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "/Encrypt is a number") + } + // A security handler we do not have. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "Filter", Name("FOPN_foweb")) + testing.expect(t, crypt_init(doc, nil) == .Unsupported_Encryption, "third-party handler") + } + // A version from the future. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "V", i64(9)) + testing.expect(t, crypt_init(doc, nil) == .Unsupported_Encryption, "/V 9") + } + // /O and /U truncated to nothing: no crash, no match. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "O", crypt_t_str(doc, {1, 2, 3})) + dict_set(ed, "U", crypt_t_str(doc, {})) + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "truncated /O and /U") + } + // /O and /U absent entirely. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + delete_key(ed, "O") + delete_key(ed, "U") + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "missing /O and /U") + } + // R6 with /UE missing: the password checks out but there is no key to + // unwrap, which is a broken dictionary rather than a wrong password. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) + delete_key(ed, "UE") + testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "R6 without /UE") + } + // R5/R6 /U truncated below its 48 bytes. + { + doc := crypt_t_new_doc(t) + defer close(doc) + c := crypt_t_r56_cases[1] + ed := crypt_t_build_r56(doc, c) + dict_set(ed, "U", crypt_t_str(doc, c.u[:20])) + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "truncated R6 /U") + } + // A /Length that is nonsense clamps to the widest legal key instead of + // sizing an allocation from the file. 1000000 bits clamps to 128, which is + // what this document actually uses, so it still opens. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "Length", i64(1_000_000)) + testing.expect(t, crypt_init(doc, nil) == .None, "absurd /Length") + testing.expectf(t, len(doc.encrypt.key) == 16, "key length %d", len(doc.encrypt.key)) + } + // A negative /Length falls back to the 40-bit default, which is not this + // document's key length, so it fails the password check rather than + // indexing a negative-length slice. + { + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "Length", i64(-99)) + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "negative /Length") + } + // Missing /ID. The key derivation folds in an empty string, which is what + // other readers do, so this must not crash. + { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[1]) + delete_key(doc.trailer, "ID") + testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "missing /ID") + } +} + +@(test) +test_crypt_revision_repair :: proc(t: ^testing.T) { + // R6 with /V written as 4: the revision decides the algorithms, so trust it + // and note the damage. Files from at least one producer do this. + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) + dict_set(ed, "V", i64(4)) + + testing.expect(t, crypt_init(doc, nil) == .None, "R6 with /V 4") + testing.expect(t, doc.encrypt != nil) + testing.expect(t, slice.equal(doc.encrypt.key, crypt_t_filekey[:]), "key") + testing.expect(t, doc.is_damaged, "the /V mismatch should be recorded") + + // /V 3 is the unpublished Adobe variant; it decrypts as /V 2. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + ed2 := crypt_t_build(doc2, crypt_t_cases[1]) + dict_set(ed2, "V", i64(3)) + testing.expect(t, crypt_init(doc2, nil) == .None, "/V 3") + testing.expect(t, slice.equal(doc2.encrypt.key, crypt_t_cases[1].want_key), "/V 3 key") + + // A missing /R is inferred from /V. + doc3 := crypt_t_new_doc(t) + defer close(doc3) + ed3 := crypt_t_build(doc3, crypt_t_cases[1]) + delete_key(ed3, "R") + testing.expect(t, crypt_init(doc3, nil) == .None, "missing /R") + testing.expect(t, doc3.encrypt.revision == 3, "inferred revision") +} + +// --------------------------------------------------------------------------- +// Reporting +// --------------------------------------------------------------------------- + +@(test) +test_crypt_permissions :: proc(t: ^testing.T) { + // /P = -1340 is 0xFFFFFAC4: printing, accessibility extraction and + // high-resolution printing allowed, everything else denied. + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[3]) + testing.expect(t, crypt_init(doc, nil) == .None) + + want := Permissions{.Print, .Extract, .Print_High_Res} + got := encryption_permissions(doc) + testing.expectf(t, got == want, "user permissions %v, want %v", got, want) + testing.expectf(t, doc.encrypt.permission_bits == -1340, + "/P read as %d", doc.encrypt.permission_bits) + + // The owner is not bound by the restrictions they set. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + crypt_t_build(doc2, crypt_t_cases[3]) + testing.expect(t, crypt_init(doc2, transmute([]byte)string("owner")) == .None) + all := Permissions{.Print, .Modify, .Copy, .Annotate, .Fill_Forms, .Extract, .Assemble, .Print_High_Res} + testing.expectf(t, encryption_permissions(doc2) == all, + "owner permissions %v", encryption_permissions(doc2)) + + // An unencrypted document allows everything. + doc3 := crypt_t_new_doc(t) + defer close(doc3) + testing.expect(t, encryption_permissions(doc3) == all, "unencrypted permissions") + testing.expect(t, encryption_permissions(nil) == all, "nil document") + + // R2 only ever defined four of the bits. + doc4 := crypt_t_new_doc(t) + defer close(doc4) + crypt_t_build(doc4, crypt_t_cases[0]) + testing.expect(t, crypt_init(doc4, nil) == .None) + r2 := encryption_permissions(doc4) + testing.expectf(t, r2 == {.Print, .Modify, .Copy, .Annotate}, "R2 permissions %v", r2) + + // /P written as the equivalent unsigned value, which some producers do. + doc5 := crypt_t_new_doc(t) + defer close(doc5) + ed := crypt_t_build(doc5, crypt_t_cases[3]) + dict_set(ed, "P", i64(4294965956)) // 0xFFFFFAC4 + testing.expect(t, crypt_init(doc5, nil) == .None, "unsigned /P") + testing.expectf(t, doc5.encrypt.permission_bits == -1340, + "unsigned /P read as %d", doc5.encrypt.permission_bits) +} + +@(test) +test_crypt_method_name :: proc(t: ^testing.T) { + Case :: struct { + index: int, + want: string, + } + cases := []Case{ + {0, "RC4 40-bit"}, + {1, "RC4 128-bit"}, + {2, "RC4 40-bit"}, + {3, "RC4 128-bit"}, + {4, "AES-128"}, + } + for c in cases { + doc := crypt_t_new_doc(t) + defer close(doc) + kd := crypt_t_cases[c.index] + crypt_t_build(doc, kd) + testing.expect(t, crypt_init(doc, transmute([]byte)kd.user_password) == .None) + testing.expectf(t, encryption_method_name(doc) == c.want, + "%s: %q, want %q", kd.name, encryption_method_name(doc), c.want) + testing.expect(t, document_is_encrypted(doc), "document_is_encrypted") + } + + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build_r56(doc, crypt_t_r56_cases[1]) + testing.expect(t, crypt_init(doc, nil) == .None) + testing.expectf(t, encryption_method_name(doc) == "AES-256", "%q", encryption_method_name(doc)) + + testing.expect(t, encryption_method_name(nil) == "none", "nil document") + doc2 := crypt_t_new_doc(t) + defer close(doc2) + testing.expect(t, encryption_method_name(doc2) == "none", "unencrypted") +} + +@(test) +test_crypt_destroy_wipes_the_key :: proc(t: ^testing.T) { + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[1]) + testing.expect(t, crypt_init(doc, nil) == .None) + + enc := doc.encrypt + crypt_destroy(doc) + testing.expect(t, doc.encrypt == nil, "handler still attached") + zero := true + for b in enc.key { + if b != 0 { zero = false } + } + testing.expect(t, zero, "the file key was left in memory") + + // Idempotent: `close` calls it again after an explicit teardown. + crypt_destroy(doc) + crypt_destroy(nil) +} + +@(test) +test_crypt_v5_without_crypt_filters :: proc(t: ^testing.T) { + // /CF is required for V5, and a file that omits it can only have meant + // AES-256. Left as /Identity the document would render as noise. + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) + delete_key(ed, "CF") + delete_key(ed, "StmF") + delete_key(ed, "StrF") + + testing.expect(t, crypt_init(doc, nil) == .None, "V5 without /CF") + testing.expect(t, doc.encrypt.stream_method == .Aes_256, "stream method") + testing.expect(t, doc.encrypt.string_method == .Aes_256, "string method") + testing.expect(t, doc.is_damaged, "the missing /CF should be recorded") + + cipher := []byte{ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x00, 0x3a, 0xee, 0xe0, 0x20, 0x26, 0xc8, 0x6a, 0xf8, + 0xe6, 0x2d, 0x73, 0xc1, 0xcc, 0xe3, 0xed, 0x73, 0x41, 0x0a, 0x22, 0x1b, + 0x82, 0x91, 0x46, 0x04, 0x97, 0x5d, 0x3a, 0xaa, 0x12, 0x75, 0x20, 0x57, + } + testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, cipher), + transmute([]byte)string("R6 stream payload")), "decrypt") + + // An explicit /Identity is a deliberate configuration and is respected. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + ed2 := crypt_t_build_r56(doc2, crypt_t_r56_cases[1]) + dict_set(ed2, "StmF", Name("Identity")) + testing.expect(t, crypt_init(doc2, nil) == .None) + testing.expect(t, doc2.encrypt.stream_method == .Identity, "explicit /Identity overridden") +} + +@(test) +test_crypt_r2_ignores_length :: proc(t: ^testing.T) { + // R2 is 40-bit whatever /Length claims. A file that writes 128 there still + // has to be opened with a 5-byte key. + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[0]) + dict_set(ed, "Length", i64(128)) + + testing.expect(t, crypt_init(doc, nil) == .None, "R2 with /Length 128") + testing.expectf(t, len(doc.encrypt.key) == 5, "key length %d", len(doc.encrypt.key)) + testing.expect(t, slice.equal(doc.encrypt.key, crypt_t_cases[0].want_key), "key") +} + +@(test) +test_crypt_long_password :: proc(t: ^testing.T) { + // Algorithm 2 truncates to 32 bytes, so a 32-byte password and a 500-byte + // one that starts with it are the same password. The 500-byte form is also + // what would overrun a fixed padding buffer. + long: [500]byte + for i in 0 ..< len(long) { long[i] = byte('a' + i % 26) } + + a, b: [32]byte + crypt_pad_password(long[:], a[:]) + crypt_pad_password(long[:32], b[:]) + testing.expect(t, slice.equal(a[:], b[:]), "password not truncated to 32 bytes") + + // A short password is extended from the padding string, and an empty one + // becomes the padding string entire. + crypt_pad_password(nil, a[:]) + testing.expect(t, slice.equal(a[:], CRYPT_PAD[:]), "empty password padding") + crypt_pad_password(transmute([]byte)string("ab"), a[:]) + testing.expect(t, a[0] == 'a' && a[1] == 'b' && a[2] == CRYPT_PAD[0], "short password padding") + + // And the whole path survives one. + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[1]) + testing.expect(t, crypt_init(doc, long[:]) == .Wrong_Password, "500-byte password") + + doc2 := crypt_t_new_doc(t) + defer close(doc2) + crypt_t_build_r56(doc2, crypt_t_r56_cases[1]) + testing.expect(t, crypt_init(doc2, long[:]) == .Wrong_Password, "500-byte R6 password") +} + +@(test) +test_crypt_cyclic_encrypt_reference :: proc(t: ^testing.T) { + // /Encrypt pointing at an object that is itself a reference to /Encrypt. + // resolve gives up after MAX_REF_CHAIN hops; crypt_init must not spin. + doc := crypt_t_new_doc(t) + defer close(doc) + doc.cache[9] = Ref{9, 0} + dict_set(doc.trailer, "Encrypt", Ref{9, 0}) + testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "self-referential /Encrypt") + + // A two-object cycle. + doc2 := crypt_t_new_doc(t) + defer close(doc2) + doc2.cache[9] = Ref{10, 0} + doc2.cache[10] = Ref{9, 0} + dict_set(doc2.trailer, "Encrypt", Ref{9, 0}) + testing.expect(t, crypt_init(doc2, nil) == .Bad_Encrypt_Dict, "cyclic /Encrypt") +} + +@(test) +test_crypt_dictionary_sweep :: proc(t: ^testing.T) { + // Every combination of the structural fields a file controls, over a + // dictionary whose /O and /U are truncated to a range of lengths. Nothing + // here is expected to decrypt; the assertions are the invariants that must + // hold whatever the file says. + versions := []i64{0, 1, 2, 4, 5, 9, -1, 1 << 40} + revisions := []i64{0, 2, 3, 4, 5, 6, -3} + lengths := []i64{0, 5, 40, 41, 128, 256, -8, 1 << 33} + cfms := []string{"", "None", "V2", "AESV2", "AESV3", "Identity"} + cuts := []int{0, 1, 15, 16, 31, 32, 40, 47, 48} + + base := crypt_t_cases[3] + n := 0 + for v in versions { + for r in revisions { + for l in lengths { + for cfm in cfms { + cut := cuts[n % len(cuts)] + n += 1 + + doc := crypt_t_new_doc(t) + defer close(doc) + + c := base + c.version, c.revision, c.length, c.cfm = v, r, l, cfm + c.o = base.o[:min(cut, len(base.o))] + c.u = base.u[:min(cut, len(base.u))] + ed := crypt_t_build(doc, c) + // R5/R6 fields, so the V5 branches are exercised too. + dict_set(ed, "OE", crypt_t_str(doc, crypt_t_r56_cases[1].oe[:min(cut, 32)])) + dict_set(ed, "UE", crypt_t_str(doc, crypt_t_r56_cases[1].ue[:min(cut, 32)])) + dict_set(ed, "Perms", crypt_t_str(doc, crypt_t_r56_cases[1].perms[:min(cut, 16)])) + + err := crypt_init(doc, transmute([]byte)string("pw")) + #partial switch err { + case .None, .Wrong_Password, .Unsupported_Encryption, .Bad_Encrypt_Dict: + // The only outcomes crypt_init is allowed to have. + case: + testing.expectf(t, false, "V=%d R=%d L=%d CFM=%q: %v", v, r, l, cfm, err) + } + if err == .None { + testing.expectf(t, doc.encrypt != nil, + "V=%d R=%d L=%d CFM=%q: success without state", v, r, l, cfm) + testing.expectf(t, len(doc.encrypt.key) >= 5 && len(doc.encrypt.key) <= 32, + "V=%d R=%d L=%d CFM=%q: key length %d", v, r, l, cfm, + len(doc.encrypt.key)) + // Whatever it decided, decryption must stay in bounds. + payload := crypt_t_id0[:] + out := decrypt_string(doc, Ref{7, 0}, payload) + testing.expectf(t, len(out) <= len(payload), + "V=%d R=%d L=%d CFM=%q: %d bytes out of %d", + v, r, l, cfm, len(out), len(payload)) + } else { + testing.expectf(t, doc.encrypt == nil, + "V=%d R=%d L=%d CFM=%q: state left after %v", v, r, l, cfm, err) + } + } + } + } + } +} + +@(test) +test_crypt_payload_sweep :: proc(t: ^testing.T) { + // Arbitrary bytes fed to every filter at every length up to a few blocks. + // None of it is valid ciphertext; none of it may crash, and none of it may + // produce more bytes than it was given. + payload: [80]byte + for i in 0 ..< len(payload) { payload[i] = byte(i * 91 + 7) } + + indices := []int{1, 3, 4} // RC4 128-bit, RC4 via crypt filter, AES-128 + for idx in indices { + doc := crypt_t_new_doc(t) + defer close(doc) + kd := crypt_t_cases[idx] + crypt_t_build(doc, kd) + testing.expect(t, crypt_init(doc, transmute([]byte)kd.user_password) == .None) + + for n in 0 ..= len(payload) { + for ref in ([]Ref{{1, 0}, {7, 0}, {0xFFFFFF, 0xFFFF}, {0, 0}}) { + s := decrypt_string(doc, ref, payload[:n]) + testing.expectf(t, len(s) <= n, "string %d bytes -> %d", n, len(s)) + d := decrypt_stream(doc, ref, payload[:n]) + testing.expectf(t, len(d) <= n, "stream %d bytes -> %d", n, len(d)) + e := decrypt_embedded_file(doc, ref, payload[:n]) + testing.expectf(t, len(e) <= n, "embedded %d bytes -> %d", n, len(e)) + } + } + } + + // The same for AES-256, which takes a different path through the key code. + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build_r56(doc, crypt_t_r56_cases[1]) + testing.expect(t, crypt_init(doc, nil) == .None) + for n in 0 ..= len(payload) { + got := decrypt_stream(doc, Ref{7, 0}, payload[:n]) + testing.expectf(t, len(got) <= n, "aes-256 %d bytes -> %d", n, len(got)) + } +} + +@(test) +test_crypt_u_check_length :: proc(t: ^testing.T) { + // Algorithm 6 compares all 32 bytes of /U for R2, but only the first 16 for + // R3 and later -- the rest is padding the producer chose freely, and files + // do write different things there. + scratch: [32]byte + + copy(scratch[:], crypt_t_cases[1].u) + for i in 16 ..< 32 { scratch[i] = 0xA5 } + doc := crypt_t_new_doc(t) + defer close(doc) + ed := crypt_t_build(doc, crypt_t_cases[1]) + dict_set(ed, "U", crypt_t_str(doc, scratch[:])) + testing.expect(t, crypt_init(doc, nil) == .None, + "R3 should ignore the last 16 bytes of /U") + + copy(scratch[:], crypt_t_cases[0].u) + scratch[31] ~= 0xFF + doc2 := crypt_t_new_doc(t) + defer close(doc2) + ed2 := crypt_t_build(doc2, crypt_t_cases[0]) + dict_set(ed2, "U", crypt_t_str(doc2, scratch[:])) + testing.expect(t, crypt_init(doc2, nil) == .Wrong_Password, + "R2 should compare all 32 bytes of /U") + + // A single flipped bit in the checked half fails at every revision. + copy(scratch[:], crypt_t_cases[1].u) + scratch[15] ~= 0x01 + doc3 := crypt_t_new_doc(t) + defer close(doc3) + ed3 := crypt_t_build(doc3, crypt_t_cases[1]) + dict_set(ed3, "U", crypt_t_str(doc3, scratch[:])) + testing.expect(t, crypt_init(doc3, nil) == .Wrong_Password, "flipped bit in /U") +} + +@(test) +test_crypt_id_survives_init :: proc(t: ^testing.T) { + // The /ID strings feed the key derivation and are never themselves + // encrypted, so they must read back exactly as the file wrote them after + // the handler has been set up. + doc := crypt_t_new_doc(t) + defer close(doc) + crypt_t_build(doc, crypt_t_cases[1]) + testing.expect(t, crypt_init(doc, nil) == .None) + + id, ok := to_bytes(array_get(doc, dict_array(doc, doc.trailer, "ID"), 0)) + testing.expect(t, ok, "/ID is not a string") + testing.expectf(t, slice.equal(id, crypt_t_id0[:]), "/ID = %x", id) + + // And the /O and /U in the /Encrypt dictionary likewise. + ed := dict_dict(doc, doc.trailer, "Encrypt") + testing.expect(t, slice.equal(dict_bytes(doc, ed, "O"), crypt_t_cases[1].o), "/O") + testing.expect(t, slice.equal(dict_bytes(doc, ed, "U"), crypt_t_cases[1].u), "/U") +} + +// --------------------------------------------------------------------------- +// The cross-reference stream is never encrypted +// --------------------------------------------------------------------------- + +// crypt_t_hex spells bytes as the body of a PDF hex string. +@(private = "file") +crypt_t_hex :: proc(b: []byte) -> string { + out := strings.builder_make(0, len(b) * 2, context.temp_allocator) + for v in b { + fmt.sbprintf(&out, "%02x", v) + } + return strings.to_string(out) +} + +// crypt_t_encrypted_xref_stream_file writes an encrypted document whose whole +// structure is a /Type /XRef stream: catalog, page tree, page, the /Encrypt +// dictionary as object 4, and the cross-reference stream as object 5. +@(private = "file") +crypt_t_encrypted_xref_stream_file :: proc(c: Kd_Case, rows_out: ^[]byte) -> Pdf_Builder { + pb := pb_make("%PDF-1.5\n") + pb_skeleton(&pb) + pb_obj(&pb, 4, fmt.tprintf( + "<< /Filter /Standard /V %d /R %d /Length %d /P %d /O <%s> /U <%s> >>", + c.version, c.revision, c.length, c.permissions, + crypt_t_hex(c.o[:]), crypt_t_hex(c.u[:]))) + + at := pb_here(&pb) + rows := [6]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(pb.offs[2]), 0}, + {1, u64(pb.offs[3]), 0}, + {1, u64(pb.offs[4]), 0}, + {1, u64(at), 0}, + } + rows_out^ = xref_rows_encode(rows[:], 1, 4, 2) + pb_stream_obj(&pb, 5, fmt.tprintf( + "/Type /XRef /Size 6 /W [1 4 2] /Root 1 0 R /Encrypt 4 0 R /ID [<%s> <%s>]", + crypt_t_hex(crypt_t_id0[:]), crypt_t_hex(crypt_t_id0[:])), rows_out^) + pb_startxref(&pb, at) + return pb +} + +@(test) +test_crypt_xref_stream_is_never_decrypted :: proc(t: ^testing.T) { + // ISO 32000-2, 7.5.8.2: "the cross-reference stream shall not be + // encrypted". `xref_load` gets this right for free -- it runs before + // `crypt_init`, so `doc.encrypt` is still nil while the table is read -- + // but the same object read again through the public API arrives with the + // key in place. Decrypting plaintext hands back noise, which `stream_data` + // then caches on the Stream; `find_catalog`'s fallback scan walks every + // object this way. + rows: []byte + pb := crypt_t_encrypted_xref_stream_file(crypt_t_cases[1], &rows) + defer pb_destroy(&pb) + defer delete(rows) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + defer close(doc) + testing.expectf(t, err == .None, "open: %s", error_string(err)) + if doc == nil { return } + testing.expect(t, doc.encrypt != nil, "the document did not come up encrypted") + + s := get_stream(doc, Ref{5, 0}) + if !testing.expect(t, s != nil, "object 5 is not a stream") { return } + data, derr := stream_data(doc, s) + testing.expectf(t, derr == .None, "stream_data: %s", error_string(derr)) + testing.expectf(t, slice.equal(data, rows), + "the xref stream was decrypted on re-read: got %x, want %x", data, rows) + + // The other objects in the same file are encrypted, so the exemption has + // to be the /Type and not "this document is in the clear". + content := transmute([]byte)string("payload") + testing.expect(t, !slice.equal(decrypt_stream(doc, Ref{5, 0}, content), content), + "object 5's key is an identity transform, so this proves nothing") +} diff --git a/device.odin b/device.odin new file mode 100644 index 0000000..9b211a7 --- /dev/null +++ b/device.odin @@ -0,0 +1,275 @@ +package pdf + +// A Device is the sink the content-stream interpreter draws into. +// +// Everything above this line is PDF knowledge -- resolving resources, tracking +// graphics state, mapping character codes to glyphs. Everything below is +// output. Three devices ship with the library: `device_raster` produces a +// Bitmap, `device_text` produces extracted text, and `device_list` records a +// display list a caller can replay on its own GPU backend. They share one +// interpreter, which is why the text a page extracts to always agrees with the +// text it draws. +// +// Every hook may be nil. The interpreter checks before calling, so a device +// only implements what it cares about -- the text device leaves every path and +// image hook nil. + +// Soft_Mask is a /SMask from an ExtGState: a form XObject rendered to a mask +// that modulates everything painted while it is in force (ISO 32000-2, 11.6.5). +// +// It is built in the interpreter's SCRATCH and does not outlive the run, like +// the `Path` and `Text` handed to the hooks below. A device that retains one +// past the call it arrived on must copy it by value -- `device_list` does, and +// says so. Two Soft_Masks with equal contents are the same mask: everything a +// render of one depends on is in here, which is what lets a device that caches +// rendered planes recognise the same mask installed twice. +Soft_Mask :: struct { + form: ^Stream, + luminosity: bool, + // Backdrop colour for a luminosity mask, in the group's colour space. + backdrop: Color, + transfer: ^Function, + // The CTM in force when the ExtGState installed the mask. The mask is + // positioned by this, not by the CTM at the time of the paint operation. + ctm: Matrix, +} + +// Text_Item is one positioned glyph. +Text_Item :: struct { + // Glyph index into the font program. 0 is .notdef. + gid: u32, + // The character code taken from the string, and the CID it mapped to. + // For a simple font these are equal. + code: u32, + cid: u32, + // Glyph space to device space, complete: it already folds in the text + // matrix, the font size, the horizontal scale, the rise and the CTM. + // Carrying the matrix rather than a point keeps rotated and skewed text + // correct without every device re-deriving it. + trm: Matrix, + // Horizontal displacement this glyph contributes, in unscaled text space. + advance: f64, + // Unicode for extraction, from /ToUnicode or the encoding. Negative when + // the font offers none. + unicode: rune, + // The code was a single byte 32, which is what word spacing keys off and + // what text extraction treats as a word break. + is_space: bool, +} + +// Text is the glyph run produced by one text-showing operator. +Text :: struct { + font: ^Font, + // 0 horizontal, 1 vertical (a Type0 font with a vertical CMap). + wmode: int, + render_mode: Text_Render_Mode, + items: [dynamic]Text_Item, +} + +text_make :: proc(allocator := context.allocator) -> Text { + return Text{items = make([dynamic]Text_Item, 0, 32, allocator)} +} + +text_destroy :: proc(t: ^Text) { + if t == nil { return } + delete(t.items) + t^ = {} +} + +// Device_Hint lets a device tell the interpreter what it can skip. Decoding a +// 4000x4000 JPEG to extract text from the page it sits on is pure waste. +Device_Hint :: enum u8 { + Ignore_Image, + Ignore_Shade, + // Text is wanted as glyph indices and unicode only; do not load outlines. + No_Glyph_Outlines, + // Do not execute tiling patterns; paint their average colour instead. + No_Tiling, +} + +Device_Hints :: bit_set[Device_Hint; u8] + +Device :: struct { + // Backend state. The interpreter never looks at it. + user: rawptr, + hints: Device_Hints, + + // -- paths ------------------------------------------------------------ + // `path` is in user space; `ctm` maps it to device space. Devices must not + // retain the path -- it is reused by the interpreter after the call. + fill_path: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint), + stroke_path: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint), + + // -- clipping --------------------------------------------------------- + // Every clip_* pushes one entry; `pop_clip` removes one. The interpreter + // guarantees they balance, including on the error paths. + clip_path: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix), + clip_stroke_path: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix), + clip_text: proc(dev: ^Device, text: ^Text, ctm: Matrix), + clip_image_mask: proc(dev: ^Device, img: ^Image, ctm: Matrix), + pop_clip: proc(dev: ^Device), + + // -- text ------------------------------------------------------------- + fill_text: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint), + stroke_text: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint), + // Render mode 3 (invisible) and 7 (clip-only). Nothing is painted, but the + // text is still there to be extracted -- which is exactly how a scanned + // page with an OCR layer is built. + ignore_text: proc(dev: ^Device, text: ^Text, ctm: Matrix), + + // -- images and shadings ---------------------------------------------- + fill_image: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint), + // A 1-bit stencil mask painted in the current fill colour. + fill_image_mask: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint), + fill_shade: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint), + + // -- transparency ----------------------------------------------------- + // begin_mask/end_mask bracket the rendering of a soft mask's form; what is + // drawn between them becomes the mask, not page content. + // + // `end_mask` is told WHICH Soft_Mask it has just drawn. A device that + // caches rendered planes needs that: it is the same pointer that later + // arrives on `Paint.soft_mask`, and without it the owner of a plane can + // only be guessed from whatever paints next. + begin_mask: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint), + end_mask: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function), + // 11.6.6: the outer alpha, blend mode AND soft mask apply to the group's + // RESULT, not to the operations inside it, so all three are handed over + // here -- the interpreter has cleared them for the group's contents and + // this is the only channel left for them. + begin_group: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask), + end_group: proc(dev: ^Device), + // Reports that the device already holds a rendered plane for a mask equal + // to this one, so the interpreter may skip re-running its form. Rendering + // a mask is a whole form XObject rasterised into a plane the size of its + // BBox, and an ExtGState naming an /SMask is commonly applied to run after + // run of content -- once per `gs`, thousands of times on a page. A device + // that caches nothing leaves this nil and every install renders, which is + // what `device_list` does: a recorded list has to carry the mask's drawing + // for whoever replays it. + has_soft_mask: proc(dev: ^Device, mask: ^Soft_Mask) -> bool, + + // -- tiling patterns -------------------------------------------------- + // Returns the number of times the interpreter should replay the tile's + // content stream: 0 means the device rendered the tile itself and wants + // the content run once, into the tile. A device that cannot tile returns + // a count and lets the interpreter place each copy. + begin_tile: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int, + end_tile: proc(dev: ^Device), + + // -- optional content (layers) ---------------------------------------- + begin_layer: proc(dev: ^Device, name: string), + end_layer: proc(dev: ^Device), + + // Called once when the page is finished, before the device is torn down. + close: proc(dev: ^Device), +} + +// --------------------------------------------------------------------------- +// Safe invocation +// +// The interpreter calls these rather than the fields directly, so a device +// that leaves a hook nil costs one predictable branch instead of a crash. +// --------------------------------------------------------------------------- + +dev_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + if dev != nil && dev.fill_path != nil { dev.fill_path(dev, path, even_odd, ctm, paint) } +} + +dev_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + if dev != nil && dev.stroke_path != nil { dev.stroke_path(dev, path, stroke, ctm, paint) } +} + +dev_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { + if dev != nil && dev.clip_path != nil { dev.clip_path(dev, path, even_odd, ctm) } +} + +dev_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { + if dev != nil && dev.clip_stroke_path != nil { dev.clip_stroke_path(dev, path, stroke, ctm) } +} + +dev_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + if dev != nil && dev.clip_text != nil { dev.clip_text(dev, text, ctm) } +} + +dev_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + if dev != nil && dev.clip_image_mask != nil { dev.clip_image_mask(dev, img, ctm) } +} + +dev_pop_clip :: proc(dev: ^Device) { + if dev != nil && dev.pop_clip != nil { dev.pop_clip(dev) } +} + +dev_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { + if dev != nil && dev.fill_text != nil { dev.fill_text(dev, text, ctm, paint) } +} + +dev_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + if dev != nil && dev.stroke_text != nil { dev.stroke_text(dev, text, stroke, ctm, paint) } +} + +dev_ignore_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + if dev != nil && dev.ignore_text != nil { dev.ignore_text(dev, text, ctm) } +} + +dev_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + if dev != nil && dev.fill_image != nil { dev.fill_image(dev, img, ctm, paint) } +} + +dev_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + if dev != nil && dev.fill_image_mask != nil { dev.fill_image_mask(dev, img, ctm, paint) } +} + +dev_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { + if dev != nil && dev.fill_shade != nil { dev.fill_shade(dev, shade, ctm, paint) } +} + +dev_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { + if dev != nil && dev.begin_mask != nil { dev.begin_mask(dev, area, luminosity, backdrop) } +} + +dev_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { + if dev != nil && dev.end_mask != nil { dev.end_mask(dev, mask, transfer) } +} + +dev_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + if dev != nil && dev.begin_group != nil { dev.begin_group(dev, area, isolated, knockout, blend, alpha, soft_mask) } +} + +dev_end_group :: proc(dev: ^Device) { + if dev != nil && dev.end_group != nil { dev.end_group(dev) } +} + +dev_has_soft_mask :: proc(dev: ^Device, mask: ^Soft_Mask) -> bool { + if dev != nil && dev.has_soft_mask != nil { return dev.has_soft_mask(dev, mask) } + return false +} + +dev_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { + if dev != nil && dev.begin_tile != nil { return dev.begin_tile(dev, area, view, xstep, ystep, ctm) } + return 0 +} + +dev_end_tile :: proc(dev: ^Device) { + if dev != nil && dev.end_tile != nil { dev.end_tile(dev) } +} + +dev_begin_layer :: proc(dev: ^Device, name: string) { + if dev != nil && dev.begin_layer != nil { dev.begin_layer(dev, name) } +} + +dev_end_layer :: proc(dev: ^Device) { + if dev != nil && dev.end_layer != nil { dev.end_layer(dev) } +} + +dev_close :: proc(dev: ^Device) { + if dev != nil && dev.close != nil { dev.close(dev) } +} + +dev_wants_images :: proc(dev: ^Device) -> bool { + return dev != nil && .Ignore_Image not_in dev.hints +} + +dev_wants_shades :: proc(dev: ^Device) -> bool { + return dev != nil && .Ignore_Shade not_in dev.hints +} diff --git a/device_list.odin b/device_list.odin new file mode 100644 index 0000000..5ff6f0d --- /dev/null +++ b/device_list.odin @@ -0,0 +1,1090 @@ +package pdf + +import "core:mem" +import "core:strings" + +// The display-list Device: it records the drawing calls the interpreter makes +// instead of painting them, so a caller can replay them onto its own backend +// -- a GPU renderer, a PostScript writer, a hit-tester -- or replay them twice +// without re-running the content stream. +// +// SELF-CONTAINMENT IS THE WHOLE POINT +// +// `device.odin` tells devices they must not retain the Path and the Text they +// are handed: the interpreter reuses both for the next operator. A device that +// paints immediately does not care. A device that records has to copy, and the +// classic display-list bug is exactly this -- the list keeps the pointer, the +// interpreter clears the path for the next `re`, and every recorded fill +// silently becomes the last shape on the page. So every Path, every Text, every +// /OC name and every Soft_Mask is copied into the list's own allocator when it +// is recorded, and the list stays valid after the interpreter, its scratch +// arena and the original path are all gone. +// +// What is NOT copied is what the Document owns and therefore outlives the +// list: an ^Image, an ^Shading, an ^Font, an ^Colorspace, an ^Pattern and the +// ^Function on a soft mask all live in the Document arena until `close`. +// Copying a decoded 40 MB image per fill to gain nothing would be absurd. +// A Soft_Mask is the exception among the pointers: it is built by the +// interpreter, in interpreter scratch, so it is copied by value. +// +// THE LIST IS ALWAYS BALANCED +// +// Every `clip_*` pushes and `pop_clip` pops; every `begin_*` has its `end_*`. +// The interpreter guarantees that, but a device that trusts a content stream +// is a device waiting for a crash, so the recorder tracks its own depth: a +// push that is refused (because a cap was hit) also refuses its matching pop, +// and `close` unwinds whatever is still open. A recorded list can therefore be +// replayed into a real device without unbalancing that device's clip stack, +// however malformed the page was. +// +// CAPS +// +// A content stream is attacker-controlled and can emit fills forever, so the +// recorder is bounded twice: by command count and by the bytes it has copied. +// Past either cap it stops recording new drawing commands and sets `overflow`; +// what it has already recorded stays valid and replayable. Both caps are +// fields on the list rather than constants so a caller -- and the tests -- +// can tighten them. + +// MAX_DISPLAY_CMDS bounds the command count. Two million is far past the +// busiest real page (a dense map is tens of thousands) and still a bounded +// amount of memory for a generated one. +MAX_DISPLAY_CMDS :: 1 << 21 +// MAX_DISPLAY_BYTES bounds the copied geometry -- path points, glyph runs, +// layer names -- plus the command array itself. 256 MiB of *copies* is already +// pathological; whichever of the two caps bites first wins, and in practice +// this one does, since a command is a few hundred bytes. +MAX_DISPLAY_BYTES :: 1 << 28 +// Depth caps for the two nesting stacks the recorder keeps. Groups nest a +// handful deep in real files; clips follow q/Q, which `gstate.odin` caps at 64. +MAX_DISPLAY_SCOPES :: 256 +MAX_DISPLAY_CLIPS :: 4096 + +// --------------------------------------------------------------------------- +// The recorded commands +// +// One variant per Device hook that draws, clips or nests. `close` is not a +// command: it marks the list finished rather than describing any ink. +// --------------------------------------------------------------------------- + +Display_Fill_Path :: struct { + path: ^Path, + even_odd: bool, + ctm: Matrix, + paint: Paint, +} + +Display_Stroke_Path :: struct { + path: ^Path, + // By value: a Stroke_State is a plain value -- the dash array is a fixed + // array, not a slice -- so there is nothing here to alias. + stroke: Stroke_State, + ctm: Matrix, + paint: Paint, +} + +Display_Clip_Path :: struct { + path: ^Path, + even_odd: bool, + ctm: Matrix, +} + +Display_Clip_Stroke_Path :: struct { + path: ^Path, + stroke: Stroke_State, + ctm: Matrix, +} + +Display_Clip_Text :: struct { + text: ^Text, + ctm: Matrix, +} + +Display_Clip_Image_Mask :: struct { + img: ^Image, + ctm: Matrix, +} + +Display_Pop_Clip :: struct {} + +Display_Fill_Text :: struct { + text: ^Text, + ctm: Matrix, + paint: Paint, +} + +Display_Stroke_Text :: struct { + text: ^Text, + stroke: Stroke_State, + ctm: Matrix, + paint: Paint, +} + +Display_Ignore_Text :: struct { + text: ^Text, + ctm: Matrix, +} + +Display_Fill_Image :: struct { + img: ^Image, + ctm: Matrix, + paint: Paint, +} + +Display_Fill_Image_Mask :: struct { + img: ^Image, + ctm: Matrix, + paint: Paint, +} + +Display_Fill_Shade :: struct { + shade: ^Shading, + ctm: Matrix, + paint: Paint, +} + +Display_Begin_Mask :: struct { + area: Rect, + luminosity: bool, + backdrop: Paint, +} + +Display_End_Mask :: struct { + // The Soft_Mask this plane belongs to, copied into the list like every + // other one: it is what a replay device matches against the `soft_mask` on + // the paints that follow. + mask: ^Soft_Mask, + transfer: ^Function, +} + +Display_Begin_Group :: struct { + area: Rect, + isolated: bool, + knockout: bool, + blend: Blend_Mode, + alpha: f64, + // The mask the group's RESULT composites through (11.6.6). + soft_mask: ^Soft_Mask, +} + +Display_End_Group :: struct {} + +Display_Begin_Tile :: struct { + area: Rect, + view: Rect, + xstep: f64, + ystep: f64, + ctm: Matrix, +} + +Display_End_Tile :: struct {} + +Display_Begin_Layer :: struct { + // Copied into the list: an /OC name can come from interpreter scratch. + name: string, +} + +Display_End_Layer :: struct {} + +Display_Cmd :: union { + Display_Fill_Path, + Display_Stroke_Path, + Display_Clip_Path, + Display_Clip_Stroke_Path, + Display_Clip_Text, + Display_Clip_Image_Mask, + Display_Pop_Clip, + Display_Fill_Text, + Display_Stroke_Text, + Display_Ignore_Text, + Display_Fill_Image, + Display_Fill_Image_Mask, + Display_Fill_Shade, + Display_Begin_Mask, + Display_End_Mask, + Display_Begin_Group, + Display_End_Group, + Display_Begin_Tile, + Display_End_Tile, + Display_Begin_Layer, + Display_End_Layer, +} + +// --------------------------------------------------------------------------- +// The list +// --------------------------------------------------------------------------- + +// Display_List is a page's drawing calls, in order, owning everything they +// point at except what the Document owns. It is produced by a List_Device and +// dies with it. +Display_List :: struct { + cmds: [dynamic]Display_Cmd, + // Where the copies live, so `display_list_destroy` can give them back. + allocator: mem.Allocator, + + // Set when a cap stopped a command being recorded: the list is a valid + // prefix of the page, not the whole page. + overflow: bool, + // Set when something recorded has no bound `display_list_bounds` can + // compute -- an unbounded shading, an infinite group area. The bounds it + // returns then covers everything else, but not that. + unbounded: bool, + // Set by `close`, once the page is finished. + closed: bool, + + // Bytes copied into the list so far -- paths, glyph runs, layer names, + // soft masks. The command array itself is counted against `max_bytes` + // separately, because one command is a few hundred bytes (most of it + // Paint's 32-component Color) and a cap that ignored it would let a + // generated page cost a gigabyte of commands carrying no geometry at all. + bytes: int, + max_cmds: int, + max_bytes: int, + + // Soft masks copied out of interpreter scratch, kept for freeing. A run of + // commands under one mask shares one copy. + masks: [dynamic]^Soft_Mask, +} + +// Display_Scope tags one open begin_* while recording, so the matching end_* +// knows whether its begin made it into the list. +@(private) +Display_Scope :: enum u8 { + Dropped = 0, + Mask, + Group, + Tile, + Layer, +} + +// List_Device is the Device that fills a Display_List. +List_Device :: struct { + dev: Device, + list: Display_List, + + // Recording bookkeeping, not part of the result. `clips` holds one entry + // per live clip -- true when it was recorded -- and `clip_dropped` counts + // the ones too deep to track. Same for `scopes`. + // The allocator the device itself came from. The list carries the same + // one, but `display_list_destroy` zeroes the list, and freeing the device + // afterwards must still work. + allocator: mem.Allocator, + + clips: [dynamic]bool, + clip_dropped: int, + scopes: [dynamic]Display_Scope, + scope_dropped: int, +} + +// list_device_make builds a recording device. The device, its list and every +// copy the list makes come from `allocator`; `list_device_destroy` frees them. +list_device_make :: proc(allocator := context.allocator) -> (^List_Device, Error) { + ld, err := new(List_Device, allocator) + if err != nil || ld == nil { + return nil, .Out_Of_Memory + } + ld.list = Display_List{ + cmds = make([dynamic]Display_Cmd, 0, 64, allocator), + masks = make([dynamic]^Soft_Mask, 0, 2, allocator), + allocator = allocator, + max_cmds = MAX_DISPLAY_CMDS, + max_bytes = MAX_DISPLAY_BYTES, + } + ld.allocator = allocator + ld.clips = make([dynamic]bool, 0, 8, allocator) + ld.scopes = make([dynamic]Display_Scope, 0, 8, allocator) + + ld.dev = Device{ + user = ld, + fill_path = list_fill_path, + stroke_path = list_stroke_path, + clip_path = list_clip_path, + clip_stroke_path = list_clip_stroke_path, + clip_text = list_clip_text, + clip_image_mask = list_clip_image_mask, + pop_clip = list_pop_clip, + fill_text = list_fill_text, + stroke_text = list_stroke_text, + ignore_text = list_ignore_text, + fill_image = list_fill_image, + fill_image_mask = list_fill_image_mask, + fill_shade = list_fill_shade, + begin_mask = list_begin_mask, + end_mask = list_end_mask, + begin_group = list_begin_group, + end_group = list_end_group, + begin_tile = list_begin_tile, + end_tile = list_end_tile, + begin_layer = list_begin_layer, + end_layer = list_end_layer, + close = list_close, + } + return ld, .None +} + +list_device_destroy :: proc(ld: ^List_Device) { + if ld == nil { return } + display_list_destroy(&ld.list) + delete(ld.clips) + delete(ld.scopes) + free(ld, ld.allocator) +} + +// list_device_as_device returns the vtable to hand the interpreter. +list_device_as_device :: proc(ld: ^List_Device) -> ^Device { + if ld == nil { return nil } + return &ld.dev +} + +// list_device_result returns the recorded list. It stays valid, and keeps +// growing, until `list_device_destroy`. +list_device_result :: proc(ld: ^List_Device) -> ^Display_List { + if ld == nil { return nil } + return &ld.list +} + +// display_list_destroy releases every copy the list made. The Images, Fonts, +// Shadings and Colorspaces it points at belong to the Document and are not +// touched. +display_list_destroy :: proc(dl: ^Display_List) { + if dl == nil { return } + allocator := dl.allocator + for cmd in dl.cmds { + switch v in cmd { + case Display_Fill_Path: list_free_path(v.path, allocator) + case Display_Stroke_Path: list_free_path(v.path, allocator) + case Display_Clip_Path: list_free_path(v.path, allocator) + case Display_Clip_Stroke_Path: list_free_path(v.path, allocator) + case Display_Clip_Text: list_free_text(v.text, allocator) + case Display_Fill_Text: list_free_text(v.text, allocator) + case Display_Stroke_Text: list_free_text(v.text, allocator) + case Display_Ignore_Text: list_free_text(v.text, allocator) + case Display_Begin_Layer: delete(v.name, allocator) + case Display_Clip_Image_Mask, Display_Pop_Clip, Display_Fill_Image, + Display_Fill_Image_Mask, Display_Fill_Shade, Display_Begin_Mask, + Display_End_Mask, Display_Begin_Group, Display_End_Group, + Display_Begin_Tile, Display_End_Tile, Display_End_Layer: + // Nothing owned: every pointer in these belongs to the Document. + } + } + for m in dl.masks { + free(m, allocator) + } + delete(dl.masks) + delete(dl.cmds) + dl^ = {} +} + +// --------------------------------------------------------------------------- +// Copying into the list +// --------------------------------------------------------------------------- + +@(private) +list_free_path :: proc(p: ^Path, allocator: mem.Allocator) { + if p == nil { return } + path_destroy(p) + free(p, allocator) +} + +@(private) +list_free_text :: proc(t: ^Text, allocator: mem.Allocator) { + if t == nil { return } + text_destroy(t) + free(t, allocator) +} + +// list_full reports whether either cap has been reached. A zero or negative +// cap means the default, so a caller that zeroes the struct still gets a +// bounded recorder rather than an unbounded one. +@(private) +list_full :: proc(dl: ^Display_List) -> bool { + max_cmds := dl.max_cmds > 0 ? dl.max_cmds : MAX_DISPLAY_CMDS + max_bytes := dl.max_bytes > 0 ? dl.max_bytes : MAX_DISPLAY_BYTES + total := dl.bytes + len(dl.cmds) * size_of(Display_Cmd) + return len(dl.cmds) >= max_cmds || total >= max_bytes +} + +@(private) +list_copy_path :: proc(dl: ^Display_List, p: ^Path) -> ^Path { + if p == nil { return nil } + out, err := new(Path, dl.allocator) + if err != nil || out == nil { return nil } + out^ = path_clone(p, dl.allocator) + dl.bytes += size_of(Path) + + len(p.verbs) * size_of(Path_Verb) + + len(p.points) * size_of(Point) + return out +} + +@(private) +list_copy_text :: proc(dl: ^Display_List, t: ^Text) -> ^Text { + if t == nil { return nil } + out, err := new(Text, dl.allocator) + if err != nil || out == nil { return nil } + out.font = t.font + out.wmode = t.wmode + out.render_mode = t.render_mode + n := len(t.items) + out.items = make([dynamic]Text_Item, n, max(n, 1), dl.allocator) + if n > 0 { + copy(out.items[:], t.items[:]) + } + dl.bytes += size_of(Text) + n * size_of(Text_Item) + return out +} + +// list_copy_paint copies the one thing in a Paint that the Document does not +// own. Everything else in it is a value or a Document-owned pointer. +@(private) +list_copy_paint :: proc(dl: ^Display_List, p: Paint) -> Paint { + out := p + out.soft_mask = list_copy_mask(dl, p.soft_mask) + return out +} + +// list_copy_mask copies a Soft_Mask by value, because the interpreter builds +// it in scratch that does not outlive the page. +// +// A run of hundreds of fills under one /SMask shares one copy, but the test is +// on the CONTENTS of the last copy rather than on the pointer: interpreter +// scratch is reused, so the same address can carry a different mask later in +// the page, and deduplicating on the address alone would record the first +// mask's backdrop for the second mask's fills. +@(private) +list_copy_mask :: proc(dl: ^Display_List, m: ^Soft_Mask) -> ^Soft_Mask { + if m == nil { return nil } + if n := len(dl.masks); n > 0 { + last := dl.masks[n - 1] + if last != nil && last^ == m^ { return last } + } + out, err := new(Soft_Mask, dl.allocator) + if err != nil || out == nil { return nil } + out^ = m^ + append(&dl.masks, out) + dl.bytes += size_of(Soft_Mask) + return out +} + +@(private) +list_from_dev :: proc(dev: ^Device) -> ^List_Device { + if dev == nil || dev.user == nil { return nil } + return cast(^List_Device)dev.user +} + +// --------------------------------------------------------------------------- +// Nesting bookkeeping +// --------------------------------------------------------------------------- + +// list_clip_note remembers whether a clip push made it into the list. The +// matching pop is recorded only if the push was, which is what keeps the list +// balanced when a cap refuses a push in the middle of a page. +@(private) +list_clip_note :: proc(ld: ^List_Device, recorded: bool) { + if len(ld.clips) >= MAX_DISPLAY_CLIPS { + ld.clip_dropped += 1 + return + } + append(&ld.clips, recorded) +} + +@(private) +list_clip_can_record :: proc(ld: ^List_Device) -> bool { + if len(ld.clips) >= MAX_DISPLAY_CLIPS { + ld.list.overflow = true + return false + } + if list_full(&ld.list) { + ld.list.overflow = true + return false + } + return true +} + +// list_scope_open pushes one begin_* and reports whether to record it. +@(private) +list_scope_open :: proc(ld: ^List_Device, kind: Display_Scope) -> bool { + if len(ld.scopes) >= MAX_DISPLAY_SCOPES { + ld.scope_dropped += 1 + ld.list.overflow = true + return false + } + if list_full(&ld.list) { + ld.list.overflow = true + append(&ld.scopes, Display_Scope.Dropped) + return false + } + append(&ld.scopes, kind) + return true +} + +// list_scope_close pops one begin_* and reports the kind whose end_* should be +// recorded, if any. The kind comes from the stack rather than from the caller +// so that the list is balanced even if the interpreter's begins and ends were +// not; the alternative is a recorded begin whose end never appears, which +// unbalances every device the list is ever replayed into. +@(private) +list_scope_close :: proc(ld: ^List_Device) -> (Display_Scope, bool) { + if ld.scope_dropped > 0 { + ld.scope_dropped -= 1 + return .Dropped, false + } + if len(ld.scopes) == 0 { return .Dropped, false } + kind := pop(&ld.scopes) + return kind, kind != .Dropped +} + +@(private) +list_emit_end :: proc(dl: ^Display_List, kind: Display_Scope) { + switch kind { + case .Dropped: + case .Mask: append(&dl.cmds, Display_End_Mask{}) + case .Group: append(&dl.cmds, Display_End_Group{}) + case .Tile: append(&dl.cmds, Display_End_Tile{}) + case .Layer: append(&dl.cmds, Display_End_Layer{}) + } +} + +// --------------------------------------------------------------------------- +// The hooks +// --------------------------------------------------------------------------- + +@(private) +list_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || path == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + p := list_copy_path(dl, path) + if p == nil { return } + append(&dl.cmds, Display_Fill_Path{ + path = p, even_odd = even_odd, ctm = ctm, paint = list_copy_paint(dl, paint), + }) +} + +@(private) +list_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || path == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + p := list_copy_path(dl, path) + if p == nil { return } + append(&dl.cmds, Display_Stroke_Path{ + path = p, + stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT, + ctm = ctm, + paint = list_copy_paint(dl, paint), + }) +} + +@(private) +list_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { + ld := list_from_dev(dev) + if ld == nil { return } + dl := &ld.list + recorded := false + if path != nil && list_clip_can_record(ld) { + if p := list_copy_path(dl, path); p != nil { + append(&dl.cmds, Display_Clip_Path{path = p, even_odd = even_odd, ctm = ctm}) + recorded = true + } + } + list_clip_note(ld, recorded) +} + +@(private) +list_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { + ld := list_from_dev(dev) + if ld == nil { return } + dl := &ld.list + recorded := false + if path != nil && list_clip_can_record(ld) { + if p := list_copy_path(dl, path); p != nil { + append(&dl.cmds, Display_Clip_Stroke_Path{ + path = p, + stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT, + ctm = ctm, + }) + recorded = true + } + } + list_clip_note(ld, recorded) +} + +@(private) +list_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + ld := list_from_dev(dev) + if ld == nil { return } + dl := &ld.list + recorded := false + if text != nil && list_clip_can_record(ld) { + if t := list_copy_text(dl, text); t != nil { + append(&dl.cmds, Display_Clip_Text{text = t, ctm = ctm}) + recorded = true + } + } + list_clip_note(ld, recorded) +} + +@(private) +list_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + ld := list_from_dev(dev) + if ld == nil { return } + dl := &ld.list + recorded := false + if img != nil && list_clip_can_record(ld) { + append(&dl.cmds, Display_Clip_Image_Mask{img = img, ctm = ctm}) + recorded = true + } + list_clip_note(ld, recorded) +} + +@(private) +list_pop_clip :: proc(dev: ^Device) { + ld := list_from_dev(dev) + if ld == nil { return } + if ld.clip_dropped > 0 { + ld.clip_dropped -= 1 + return + } + // A pop with nothing pushed is a broken content stream, or an interpreter + // unwinding an error path twice. Dropping it is what keeps a replay from + // popping a clip the target device never pushed. + if len(ld.clips) == 0 { return } + if pop(&ld.clips) { + append(&ld.list.cmds, Display_Pop_Clip{}) + } +} + +@(private) +list_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || text == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + t := list_copy_text(dl, text) + if t == nil { return } + append(&dl.cmds, Display_Fill_Text{text = t, ctm = ctm, paint = list_copy_paint(dl, paint)}) +} + +@(private) +list_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || text == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + t := list_copy_text(dl, text) + if t == nil { return } + append(&dl.cmds, Display_Stroke_Text{ + text = t, + stroke = stroke != nil ? stroke^ : STROKE_STATE_DEFAULT, + ctm = ctm, + paint = list_copy_paint(dl, paint), + }) +} + +@(private) +list_ignore_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + ld := list_from_dev(dev) + if ld == nil || text == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + t := list_copy_text(dl, text) + if t == nil { return } + append(&dl.cmds, Display_Ignore_Text{text = t, ctm = ctm}) +} + +@(private) +list_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || img == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + append(&dl.cmds, Display_Fill_Image{img = img, ctm = ctm, paint = list_copy_paint(dl, paint)}) +} + +@(private) +list_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || img == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + append(&dl.cmds, Display_Fill_Image_Mask{img = img, ctm = ctm, paint = list_copy_paint(dl, paint)}) +} + +@(private) +list_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { + ld := list_from_dev(dev) + if ld == nil || shade == nil { return } + dl := &ld.list + if list_full(dl) { dl.overflow = true; return } + // A shading with no /BBox paints the whole clip: there is no box to fold + // into the list's bounds, so say so rather than under-report. + if !shade.has_bbox { dl.unbounded = true } + append(&dl.cmds, Display_Fill_Shade{shade = shade, ctm = ctm, paint = list_copy_paint(dl, paint)}) +} + +@(private) +list_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { + ld := list_from_dev(dev) + if ld == nil { return } + if !list_scope_open(ld, .Mask) { return } + dl := &ld.list + append(&dl.cmds, Display_Begin_Mask{ + area = list_safe_area(dl, area), + luminosity = luminosity, + backdrop = list_copy_paint(dl, backdrop), + }) +} + +@(private) +list_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { + ld := list_from_dev(dev) + if ld == nil { return } + kind, ok := list_scope_close(ld) + if !ok { return } + if kind == .Mask { + append(&ld.list.cmds, Display_End_Mask{ + mask = list_copy_mask(&ld.list, mask), + transfer = transfer, + }) + return + } + list_emit_end(&ld.list, kind) +} + +@(private) +list_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + ld := list_from_dev(dev) + if ld == nil { return } + if !list_scope_open(ld, .Group) { return } + dl := &ld.list + append(&dl.cmds, Display_Begin_Group{ + area = list_safe_area(dl, area), + isolated = isolated, + knockout = knockout, + blend = blend, + alpha = clamp_finite(alpha, 1), + soft_mask = list_copy_mask(dl, soft_mask), + }) +} + +@(private) +list_end_group :: proc(dev: ^Device) { + ld := list_from_dev(dev) + if ld == nil { return } + kind, ok := list_scope_close(ld) + if ok { list_emit_end(&ld.list, kind) } +} + +// list_begin_tile returns 0: the recorder wants the tile's content stream run +// once, into the tile, and records that one cell together with the lattice +// (`area`, `view`, the two steps and the CTM) that repeats it. A consumer of +// the list places the copies -- exactly as a GPU backend would rather do it, +// with one draw call and an instance count, instead of receiving the same cell +// a thousand times. +@(private) +list_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { + ld := list_from_dev(dev) + if ld == nil { return 0 } + if !list_scope_open(ld, .Tile) { return 0 } + dl := &ld.list + append(&dl.cmds, Display_Begin_Tile{ + area = list_safe_area(dl, area), + view = list_safe_area(dl, view), + xstep = clamp_finite(xstep, 0), + ystep = clamp_finite(ystep, 0), + ctm = ctm, + }) + return 0 +} + +@(private) +list_end_tile :: proc(dev: ^Device) { + ld := list_from_dev(dev) + if ld == nil { return } + kind, ok := list_scope_close(ld) + if ok { list_emit_end(&ld.list, kind) } +} + +@(private) +list_begin_layer :: proc(dev: ^Device, name: string) { + ld := list_from_dev(dev) + if ld == nil { return } + if !list_scope_open(ld, .Layer) { return } + dl := &ld.list + // The name is copied: a /OC entry's name may be built in scratch, and a + // list that outlives the page must not point into it. + copied := strings.clone(name, dl.allocator) + dl.bytes += len(copied) + append(&dl.cmds, Display_Begin_Layer{name = copied}) +} + +@(private) +list_end_layer :: proc(dev: ^Device) { + ld := list_from_dev(dev) + if ld == nil { return } + kind, ok := list_scope_close(ld) + if ok { list_emit_end(&ld.list, kind) } +} + +// list_close unwinds anything the page left open, so that a list recorded off +// a content stream that ended inside three groups and a clip is still a list +// that replays into a balanced device. +// +// Groups, masks, tiles and layers unwind before the clips do. The two stacks +// carry no ordering between them, and a page that ended inside both was +// already broken; what matters is that both reach zero, so that the device the +// list is replayed into ends where it started. +@(private) +list_close :: proc(dev: ^Device) { + ld := list_from_dev(dev) + if ld == nil { return } + dl := &ld.list + for len(ld.scopes) > 0 { + list_emit_end(dl, pop(&ld.scopes)) + } + ld.scope_dropped = 0 + for len(ld.clips) > 0 { + if pop(&ld.clips) { + append(&dl.cmds, Display_Pop_Clip{}) + } + } + ld.clip_dropped = 0 + dl.closed = true +} + +// list_safe_area flags an area that carries no usable bound, and keeps a +// non-finite one out of the recorded geometry: RECT_INFINITE is what "no +// bound" already means to everything below the Device line, whereas a NaN +// corner reaching a consumer's scanline loop is how a renderer hangs. +@(private) +list_safe_area :: proc(dl: ^Display_List, area: Rect) -> Rect { + if !rect_is_finite(area) { + dl.unbounded = true + return RECT_INFINITE + } + if area == RECT_INFINITE { + dl.unbounded = true + } + return area +} + +// --------------------------------------------------------------------------- +// Replay +// --------------------------------------------------------------------------- + +// display_list_run replays every recorded command into `dev`, in order. This +// is the identity that makes a recording worth trusting: recording a page and +// replaying it into the rasteriser must produce the same bitmap as rasterising +// it directly. +// +// It goes through the same `dev_*` wrappers the interpreter uses, so a target +// device may leave any hook nil, and it honours the target's hints -- a text +// device replaying a list has no more use for a decoded image than it had the +// first time. +// +// `close` is NOT replayed: closing is the owner's business, and a list may be +// replayed into the same device several times. +// +// One deviation from a live interpreter run: a tile was recorded as a single +// cell plus its lattice, so `begin_tile`'s return value is ignored here. A +// target device that asks for `n` placements gets the cell once and must place +// the copies itself from the `Display_Begin_Tile` it was handed. +display_list_run :: proc(dl: ^Display_List, dev: ^Device) -> Error { + if dl == nil { return .Invalid_Argument } + if dev == nil { return .None } + + want_images := dev_wants_images(dev) + want_shades := dev_wants_shades(dev) + + // The command count is snapshotted, and the walk is by index. Replaying a + // list into the device that is still recording it is a thing a caller can + // do by accident; iterating the array while appending to it would never + // terminate, and would keep allocating while it did not. + n := len(dl.cmds) + for i in 0 ..< n { + cmd := dl.cmds[i] + switch v in cmd { + case Display_Fill_Path: + dev_fill_path(dev, v.path, v.even_odd, v.ctm, v.paint) + case Display_Stroke_Path: + s := v.stroke + dev_stroke_path(dev, v.path, &s, v.ctm, v.paint) + case Display_Clip_Path: + dev_clip_path(dev, v.path, v.even_odd, v.ctm) + case Display_Clip_Stroke_Path: + s := v.stroke + dev_clip_stroke_path(dev, v.path, &s, v.ctm) + case Display_Clip_Text: + dev_clip_text(dev, v.text, v.ctm) + case Display_Clip_Image_Mask: + dev_clip_image_mask(dev, v.img, v.ctm) + case Display_Pop_Clip: + dev_pop_clip(dev) + case Display_Fill_Text: + dev_fill_text(dev, v.text, v.ctm, v.paint) + case Display_Stroke_Text: + s := v.stroke + dev_stroke_text(dev, v.text, &s, v.ctm, v.paint) + case Display_Ignore_Text: + dev_ignore_text(dev, v.text, v.ctm) + case Display_Fill_Image: + if want_images { dev_fill_image(dev, v.img, v.ctm, v.paint) } + case Display_Fill_Image_Mask: + if want_images { dev_fill_image_mask(dev, v.img, v.ctm, v.paint) } + case Display_Fill_Shade: + if want_shades { dev_fill_shade(dev, v.shade, v.ctm, v.paint) } + case Display_Begin_Mask: + dev_begin_mask(dev, v.area, v.luminosity, v.backdrop) + case Display_End_Mask: + dev_end_mask(dev, v.mask, v.transfer) + case Display_Begin_Group: + dev_begin_group(dev, v.area, v.isolated, v.knockout, v.blend, v.alpha, v.soft_mask) + case Display_End_Group: + dev_end_group(dev) + case Display_Begin_Tile: + _ = dev_begin_tile(dev, v.area, v.view, v.xstep, v.ystep, v.ctm) + case Display_End_Tile: + dev_end_tile(dev) + case Display_Begin_Layer: + dev_begin_layer(dev, v.name) + case Display_End_Layer: + dev_end_layer(dev) + } + } + return .None +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +// display_list_bounds is the union, in device space, of everything recorded -- +// what a caller needs to size a surface to replay onto. +// +// It is deliberately conservative in three ways. Clips are unioned in rather +// than intersected: a clip cannot add ink, but a consumer still has to be able +// to address the region it covers, and tracking the intersection would mean +// re-implementing the clip stack here. A stroke's bound is its path's bound +// grown by the transformed line width and the miter allowance rather than by +// stroking the path. And a glyph's bound is its em box under its own text +// rendering matrix rather than its outline, which costs no glyph loading and +// is wrong only for a font whose glyphs leave the em square. +// +// Invisible text (render mode 3, the OCR layer under a scan) is the one thing +// left out: it paints nothing, so it cannot enlarge the surface anything is +// drawn on. +// +// `Display_List.unbounded` says whether anything recorded had no bound at all +// -- an unbounded shading, an infinite group area -- in which case what comes +// back covers everything else but not that. +display_list_bounds :: proc(dl: ^Display_List) -> Rect { + if dl == nil { return RECT_EMPTY } + r := RECT_EMPTY + for cmd in dl.cmds { + switch v in cmd { + case Display_Fill_Path: + r = list_bounds_add(r, path_tight_bounds(v.path, v.ctm)) + case Display_Clip_Path: + r = list_bounds_add(r, path_tight_bounds(v.path, v.ctm)) + case Display_Stroke_Path: + r = list_bounds_add(r, list_stroke_bounds(v.path, v.stroke, v.ctm)) + case Display_Clip_Stroke_Path: + r = list_bounds_add(r, list_stroke_bounds(v.path, v.stroke, v.ctm)) + case Display_Clip_Text: + r = list_bounds_add(r, list_text_bounds(v.text)) + case Display_Fill_Text: + r = list_bounds_add(r, list_text_bounds(v.text)) + case Display_Stroke_Text: + r = list_bounds_add(r, list_text_bounds(v.text)) + case Display_Ignore_Text: + // Invisible text paints nothing, so it cannot enlarge the surface. + case Display_Clip_Image_Mask: + r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT)) + case Display_Fill_Image: + r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT)) + case Display_Fill_Image_Mask: + r = list_bounds_add(r, transform_rect(v.ctm, DISPLAY_UNIT_RECT)) + case Display_Fill_Shade: + if v.shade != nil && v.shade.has_bbox { + r = list_bounds_add(r, transform_rect(v.ctm, v.shade.bbox)) + } + case Display_Begin_Mask: + r = list_bounds_add(r, v.area) + case Display_Begin_Group: + r = list_bounds_add(r, v.area) + case Display_Begin_Tile: + r = list_bounds_add(r, v.area) + case Display_Pop_Clip, Display_End_Mask, Display_End_Group, + Display_End_Tile, Display_Begin_Layer, Display_End_Layer: + // No geometry of their own. + } + } + return r +} + +// The unit square an image occupies in user space (ISO 32000-2, 8.9.5.2). +@(private) +DISPLAY_UNIT_RECT :: Rect{0, 0, 1, 1} + +@(private) +list_bounds_add :: proc(r: Rect, b: Rect) -> Rect { + if !rect_is_finite(b) || b == RECT_INFINITE { return r } + if rect_is_empty(b) { return r } + return rect_union(r, b) +} + +// list_stroke_bounds grows the path's own box by half the device-space line +// width, times the miter limit when the joins can spike. Cheaper than stroking +// and never smaller than the ink. +@(private) +list_stroke_bounds :: proc(p: ^Path, s: Stroke_State, ctm: Matrix) -> Rect { + b := path_tight_bounds(p, ctm) + if !rect_is_finite(b) { return RECT_EMPTY } + if p == nil || len(p.verbs) == 0 { return RECT_EMPTY } + + width := max(clamp_finite(s.line_width, 1), 0) + pad := 0.5 * width * matrix_max_expansion(ctm) + // A zero-width line is a one-pixel hairline (8.4.3.2), so it still has + // half a pixel of ink on each side. + pad = max(pad, 0.5) + if s.line_join == .Miter { + limit := clamp_finite(s.miter_limit, 10) + pad *= clamp(limit, 1, 10) + } + if !is_finite(pad) { return b } + return rect_expand(b, pad) +} + +// list_text_bounds boxes each glyph by its em square under its own text +// rendering matrix. The em box is taken as one em up and a third of an em +// down, which covers the ascender and descender of any text face; a glyph that +// draws outside its em square -- a swash, a big initial -- can escape it, and +// that is the price of not loading an outline to size a surface. +@(private) +list_text_bounds :: proc(t: ^Text) -> Rect { + if t == nil || len(t.items) == 0 { return RECT_EMPTY } + + // Glyph space per em: 1000 for Type1 and CFF, unitsPerEm for TrueType. + // The trm maps glyph space, so the box has to be in glyph units too. + upem := 1000.0 + m := font_matrix(t.font) + if is_finite(m.a) && abs(m.a) > 1e-12 { + upem = abs(1 / m.a) + } + if !is_finite(upem) || upem <= 0 || upem > 1e6 { upem = 1000 } + + r := RECT_EMPTY + for it in t.items { + adv := abs(clamp_finite(it.advance, 1)) * upem + box: Rect + if t.wmode == 1 { + box = Rect{-0.6 * upem, -adv - 0.3 * upem, 0.6 * upem, 0.3 * upem} + } else { + box = Rect{0, -0.35 * upem, max(adv, 0.1 * upem), upem} + } + r = list_bounds_add(r, transform_rect(it.trm, box)) + } + return r +} diff --git a/device_list_test.odin b/device_list_test.odin new file mode 100644 index 0000000..ea9b7fc --- /dev/null +++ b/device_list_test.odin @@ -0,0 +1,1274 @@ +package pdf + +import "core:mem" +import "core:testing" + +// --------------------------------------------------------------------------- +// Helpers +// +// Comparisons are structural: a recorded command holds its OWN copy of the +// path and the glyph run, so the pointers deliberately differ and only the +// contents may be compared. +// --------------------------------------------------------------------------- + +@(private = "file") +dl_path_eq :: proc(a, b: ^Path) -> bool { + if a == nil || b == nil { return a == b } + if len(a.verbs) != len(b.verbs) || len(a.points) != len(b.points) { return false } + for v, i in a.verbs { + if v != b.verbs[i] { return false } + } + for p, i in a.points { + if p != b.points[i] { return false } + } + return true +} + +@(private = "file") +dl_text_eq :: proc(a, b: ^Text) -> bool { + if a == nil || b == nil { return a == b } + if a.font != b.font || a.wmode != b.wmode || a.render_mode != b.render_mode { return false } + if len(a.items) != len(b.items) { return false } + for it, i in a.items { + if it != b.items[i] { return false } + } + return true +} + +// dl_paint_eq compares two paints, taking the soft mask by value: the list +// copies the mask, so the two pointers must NOT be equal and the two masks +// must be. +@(private = "file") +dl_paint_eq :: proc(a, b: Paint) -> bool { + x, y := a, b + x.soft_mask, y.soft_mask = nil, nil + if x != y { return false } + if (a.soft_mask == nil) != (b.soft_mask == nil) { return false } + if a.soft_mask != nil && a.soft_mask^ != b.soft_mask^ { return false } + return true +} + +// dl_mask_eq compares two recorded masks the same way, for the hooks that +// carry a `^Soft_Mask` of their own rather than one inside a Paint: +// `begin_group`, whose mask is the one the group's RESULT composites through, +// and `end_mask`, whose mask names the plane just drawn. +@(private = "file") +dl_mask_eq :: proc(a, b: ^Soft_Mask) -> bool { + if (a == nil) != (b == nil) { return false } + return a == nil || a^ == b^ +} + +// dl_find_group returns the first recorded `begin_group`, by pointer so that +// the mask it carries can be compared against the other list's copy. +@(private = "file") +dl_find_group :: proc(dl: ^Display_List) -> ^Display_Begin_Group { + for &cmd in dl.cmds { + if g, ok := &cmd.(Display_Begin_Group); ok { return g } + } + return nil +} + +@(private = "file") +dl_cmd_eq :: proc(a, b: Display_Cmd) -> bool { + switch av in a { + case Display_Fill_Path: + bv := b.(Display_Fill_Path) or_return + return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd && + av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Stroke_Path: + bv := b.(Display_Stroke_Path) or_return + return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke && + av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Clip_Path: + bv := b.(Display_Clip_Path) or_return + return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd && av.ctm == bv.ctm + case Display_Clip_Stroke_Path: + bv := b.(Display_Clip_Stroke_Path) or_return + return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke && av.ctm == bv.ctm + case Display_Clip_Text: + bv := b.(Display_Clip_Text) or_return + return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm + case Display_Clip_Image_Mask: + bv := b.(Display_Clip_Image_Mask) or_return + return av.img == bv.img && av.ctm == bv.ctm + case Display_Pop_Clip: + _ = b.(Display_Pop_Clip) or_return + return true + case Display_Fill_Text: + bv := b.(Display_Fill_Text) or_return + return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Stroke_Text: + bv := b.(Display_Stroke_Text) or_return + return dl_text_eq(av.text, bv.text) && av.stroke == bv.stroke && + av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Ignore_Text: + bv := b.(Display_Ignore_Text) or_return + return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm + case Display_Fill_Image: + bv := b.(Display_Fill_Image) or_return + return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Fill_Image_Mask: + bv := b.(Display_Fill_Image_Mask) or_return + return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Fill_Shade: + bv := b.(Display_Fill_Shade) or_return + return av.shade == bv.shade && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) + case Display_Begin_Mask: + bv := b.(Display_Begin_Mask) or_return + return av.area == bv.area && av.luminosity == bv.luminosity && + dl_paint_eq(av.backdrop, bv.backdrop) + case Display_End_Mask: + bv := b.(Display_End_Mask) or_return + return av.transfer == bv.transfer && dl_mask_eq(av.mask, bv.mask) + case Display_Begin_Group: + bv := b.(Display_Begin_Group) or_return + return av.area == bv.area && av.isolated == bv.isolated && + av.knockout == bv.knockout && av.blend == bv.blend && + av.alpha == bv.alpha && dl_mask_eq(av.soft_mask, bv.soft_mask) + case Display_End_Group: + _ = b.(Display_End_Group) or_return + return true + case Display_Begin_Tile: + bv := b.(Display_Begin_Tile) or_return + return av == bv + case Display_End_Tile: + _ = b.(Display_End_Tile) or_return + return true + case Display_Begin_Layer: + bv := b.(Display_Begin_Layer) or_return + return av.name == bv.name + case Display_End_Layer: + _ = b.(Display_End_Layer) or_return + return true + } + return false +} + +// dl_kinds spells the recorded sequence as a list of tags, which is how the +// ordering tests read. +@(private = "file") +dl_kind :: proc(cmd: Display_Cmd) -> string { + switch _ in cmd { + case Display_Fill_Path: return "fill" + case Display_Stroke_Path: return "stroke" + case Display_Clip_Path: return "clip" + case Display_Clip_Stroke_Path: return "clip_stroke" + case Display_Clip_Text: return "clip_text" + case Display_Clip_Image_Mask: return "clip_image_mask" + case Display_Pop_Clip: return "pop" + case Display_Fill_Text: return "text" + case Display_Stroke_Text: return "stroke_text" + case Display_Ignore_Text: return "ignore_text" + case Display_Fill_Image: return "image" + case Display_Fill_Image_Mask: return "image_mask" + case Display_Fill_Shade: return "shade" + case Display_Begin_Mask: return "begin_mask" + case Display_End_Mask: return "end_mask" + case Display_Begin_Group: return "begin_group" + case Display_End_Group: return "end_group" + case Display_Begin_Tile: return "begin_tile" + case Display_End_Tile: return "end_tile" + case Display_Begin_Layer: return "begin_layer" + case Display_End_Layer: return "end_layer" + } + return "?" +} + +@(private = "file") +dl_expect_kinds :: proc(t: ^testing.T, dl: ^Display_List, want: []string, loc := #caller_location) { + if !testing.expect_value(t, len(dl.cmds), len(want), loc = loc) { + for cmd, i in dl.cmds { + testing.expectf(t, false, " recorded [%d] %s", i, dl_kind(cmd), loc = loc) + } + return + } + for cmd, i in dl.cmds { + got := dl_kind(cmd) + testing.expectf(t, got == want[i], "command %d is %s, want %s", i, got, want[i], loc = loc) + } +} + +@(private = "file") +dl_test_paint :: proc(cs: ^Colorspace) -> Paint { + return Paint{ + cs = cs, + color = color_make(0.25, 0.5, 0.75), + rgb = {0.25, 0.5, 0.75}, + alpha = 0.5, + blend = .Multiply, + } +} + +@(private = "file") +dl_rect_near :: proc(t: ^testing.T, got, want: Rect, tol: f64, loc := #caller_location) { + ok := abs(got.x0 - want.x0) <= tol && abs(got.y0 - want.y0) <= tol && + abs(got.x1 - want.x1) <= tol && abs(got.y1 - want.y1) <= tol + testing.expectf(t, ok, "bounds %v, want %v (tolerance %v)", got, want, tol, loc = loc) +} + +// dl_contains reports whether `outer` covers `inner` -- what a surface sized +// from the bounds has to do for every command in the list. +@(private = "file") +dl_contains :: proc(outer, inner: Rect) -> bool { + return outer.x0 <= inner.x0 + 1e-9 && outer.y0 <= inner.y0 + 1e-9 && + outer.x1 >= inner.x1 - 1e-9 && outer.y1 >= inner.y1 - 1e-9 +} + +// --------------------------------------------------------------------------- +// A probe device: a foreign backend, to prove a replay reaches one with the +// values it was recorded with rather than only agreeing with another list. +// --------------------------------------------------------------------------- + +@(private = "file") +Dl_Event :: struct { + kind: string, + path: ^Path, + text: ^Text, + even_odd: bool, + ctm: Matrix, + paint: Paint, + stroke: Stroke_State, +} + +@(private = "file") +Dl_Probe :: struct { + dev: Device, + events: [dynamic]Dl_Event, +} + +@(private = "file") +dl_probe_init :: proc(p: ^Dl_Probe) { + p.events = make([dynamic]Dl_Event, 0, 8) + p.dev = Device{ + user = p, + fill_path = proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{ + kind = "fill", path = path, even_odd = even_odd, ctm = ctm, paint = paint, + }) + }, + stroke_path = proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{ + kind = "stroke", path = path, ctm = ctm, paint = paint, + stroke = stroke != nil ? stroke^ : {}, + }) + }, + fill_text = proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{kind = "text", text = text, ctm = ctm, paint = paint}) + }, + pop_clip = proc(dev: ^Device) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{kind = "pop"}) + }, + begin_group = proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{kind = "begin_group"}) + }, + end_group = proc(dev: ^Device) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{kind = "end_group"}) + }, + fill_image = proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + p := cast(^Dl_Probe)dev.user + append(&p.events, Dl_Event{kind = "image", ctm = ctm, paint = paint}) + }, + } +} + +@(private = "file") +dl_probe_destroy :: proc(p: ^Dl_Probe) { + delete(p.events) +} + +// --------------------------------------------------------------------------- +// Recording +// --------------------------------------------------------------------------- + +@(test) +test_display_list_records_a_fill_verbatim :: proc(t: ^testing.T) { + ld, err := list_device_make() + testing.expect_value(t, err, Error.None) + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 10, 20, 30, 40) + + cs := Colorspace{kind = .Device_RGB, n = 3} + paint := dl_test_paint(&cs) + ctm := Matrix{2, 0, 0, 2, 5, 7} + + dev_fill_path(dev, &p, true, ctm, paint) + + dl := list_device_result(ld) + testing.expect_value(t, len(dl.cmds), 1) + cmd, ok := dl.cmds[0].(Display_Fill_Path) + testing.expect(t, ok, "a fill must record a Display_Fill_Path") + if !ok { return } + + testing.expect(t, cmd.path != &p, "the list must not alias the caller's path") + testing.expect(t, dl_path_eq(cmd.path, &p), "the recorded path must equal the one given") + testing.expect_value(t, cmd.even_odd, true) + testing.expect_value(t, cmd.ctm, ctm) + testing.expect(t, dl_paint_eq(cmd.paint, paint), "the paint must be recorded unchanged") + testing.expect_value(t, cmd.paint.cs, &cs) + testing.expect_value(t, cmd.paint.alpha, 0.5) + testing.expect_value(t, cmd.paint.blend, Blend_Mode.Multiply) + testing.expect_value(t, cmd.paint.color.v[1], 0.5) + testing.expect(t, !dl.overflow, "one fill is not an overflow") + testing.expect(t, !dl.closed, "the list is not closed until close is called") +} + +// THE headline: the interpreter reuses its path for the next operator, so a +// list that kept the pointer records whatever the path became later. Mutating +// the original after recording must not be visible in the list. +@(test) +test_display_list_copies_the_path :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + + want := path_clone(&p) + defer path_destroy(&want) + + // Everything the interpreter does to a path between two operators. + path_transform(&p, Matrix{3, 0, 0, 3, 100, 100}) + path_line_to(&p, 999, 999) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + path_clear(&p) + path_move_to(&p, -5, -5) + + dl := list_device_result(ld) + testing.expect_value(t, len(dl.cmds), 2) + + first, ok := dl.cmds[0].(Display_Fill_Path) + testing.expect(t, ok, "") + if !ok { return } + testing.expect(t, dl_path_eq(first.path, &want), + "the first recording changed when the caller's path was reused") + testing.expect_value(t, len(first.path.points), 4) + testing.expect_value(t, first.path.points[2], Point{10, 10}) + + // And the second recording is its own copy of the mutated path, not a + // second view of the same one. + second, ok2 := dl.cmds[1].(Display_Fill_Path) + testing.expect(t, ok2, "") + if !ok2 { return } + testing.expect(t, second.path != first.path, "each command owns its own copy") + testing.expect_value(t, len(second.path.points), 5) + testing.expect_value(t, second.path.points[0], Point{100, 100}) + testing.expect_value(t, second.path.points[4], Point{999, 999}) + + // The list outlives the path it was recorded from: destroy it entirely and + // the recording is still readable. + path_destroy(&p) + testing.expect_value(t, len(first.path.points), 4) + testing.expect(t, !rect_is_empty(display_list_bounds(dl)), "bounds after the source path is gone") +} + +@(test) +test_display_list_copies_the_text_run :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} + txt := text_make() + defer text_destroy(&txt) + txt.font = &font + // A vertical CMap: the writing mode is part of the run and has to survive + // the copy, and leaving it at its default would hide a copy that dropped it. + txt.wmode = 1 + txt.render_mode = .Fill_Stroke + append(&txt.items, Text_Item{gid = 36, code = 'A', cid = 36, trm = Matrix{0.01, 0, 0, 0.01, 100, 200}, advance = 0.6, unicode = 'A'}) + append(&txt.items, Text_Item{gid = 37, code = 'B', cid = 37, trm = Matrix{0.01, 0, 0, 0.01, 106, 200}, advance = 0.6, unicode = 'B'}) + append(&txt.items, Text_Item{gid = 3, code = 32, cid = 3, trm = Matrix{0.01, 0, 0, 0.01, 112, 200}, advance = 0.25, unicode = ' ', is_space = true}) + + dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{alpha = 1}) + + // The interpreter reuses one Text for every show operator on the page. + clear(&txt.items) + append(&txt.items, Text_Item{gid = 99, unicode = 'Z'}) + txt.render_mode = .Invisible + + dl := list_device_result(ld) + testing.expect_value(t, len(dl.cmds), 1) + cmd, ok := dl.cmds[0].(Display_Fill_Text) + testing.expect(t, ok, "") + if !ok { return } + + testing.expect(t, cmd.text != &txt, "the list must not alias the interpreter's Text") + testing.expect_value(t, cmd.text.font, &font) + testing.expect_value(t, cmd.text.render_mode, Text_Render_Mode.Fill_Stroke) + testing.expect_value(t, cmd.text.wmode, 1) + testing.expect_value(t, len(cmd.text.items), 3) + testing.expect_value(t, cmd.text.items[0].gid, 36) + testing.expect_value(t, cmd.text.items[1].unicode, 'B') + testing.expect_value(t, cmd.text.items[1].trm, Matrix{0.01, 0, 0, 0.01, 106, 200}) + testing.expect_value(t, cmd.text.items[2].is_space, true) + testing.expect_value(t, cmd.text.items[2].advance, 0.25) +} + +@(test) +test_display_list_copies_the_soft_mask :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 1, 1) + + // Interpreter scratch: one Soft_Mask value, reused for a second mask later + // in the page, which is why the recorder compares contents and not the + // address. + mask := Soft_Mask{luminosity = true, backdrop = color_make(0.5), ctm = Matrix{1, 0, 0, 1, 3, 4}} + paint := Paint{alpha = 1, soft_mask = &mask} + + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + mask.luminosity = false + mask.backdrop = color_make(0.25) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + + dl := list_device_result(ld) + testing.expect_value(t, len(dl.cmds), 3) + a := dl.cmds[0].(Display_Fill_Path).paint.soft_mask + b := dl.cmds[1].(Display_Fill_Path).paint.soft_mask + c := dl.cmds[2].(Display_Fill_Path).paint.soft_mask + testing.expect(t, a != nil && b != nil && c != nil, "") + if a == nil || b == nil || c == nil { return } + + testing.expect(t, a != &mask, "the mask must be copied out of interpreter scratch") + testing.expect(t, a == b, "two fills under one unchanged mask share one copy") + testing.expect(t, c != a, "a changed mask at the same address is a different mask") + testing.expect_value(t, a.luminosity, true) + testing.expect_value(t, a.backdrop.v[0], 0.5) + testing.expect_value(t, a.ctm, Matrix{1, 0, 0, 1, 3, 4}) + testing.expect_value(t, c.luminosity, false) + testing.expect_value(t, c.backdrop.v[0], 0.25) +} + +@(test) +test_display_list_copies_the_layer_name :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + scratch := make([]byte, 5) + defer delete(scratch) + copy(scratch, "Layer") + + dev_begin_layer(dev, string(scratch)) + dev_end_layer(dev) + + // The interpreter's buffer is reused for the next /OC name. + copy(scratch, "XXXXX") + + dl := list_device_result(ld) + cmd, ok := dl.cmds[0].(Display_Begin_Layer) + testing.expect(t, ok, "") + if !ok { return } + testing.expect_value(t, cmd.name, "Layer") + testing.expect(t, raw_data(cmd.name) != raw_data(scratch), "the name must be copied") + + // An /OC with no usable name still nests, and copying "" must not upset + // the teardown that gives every copied name back. + dev_begin_layer(dev, "") + dev_end_layer(dev) + empty, ok2 := dl.cmds[2].(Display_Begin_Layer) + testing.expect(t, ok2, "") + if ok2 { + testing.expect_value(t, empty.name, "") + } + dl_expect_kinds(t, dl, {"begin_layer", "end_layer", "begin_layer", "end_layer"}) +} + +// --------------------------------------------------------------------------- +// Ordering and nesting +// --------------------------------------------------------------------------- + +@(test) +test_display_list_clip_pushes_and_pops_in_order :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + stroke := STROKE_STATE_DEFAULT + + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY) + dev_fill_path(dev, &p, true, MATRIX_IDENTITY, Paint{}) + dev_pop_clip(dev) + dev_pop_clip(dev) + + dl := list_device_result(ld) + dl_expect_kinds(t, dl, {"clip", "fill", "clip_stroke", "fill", "pop", "pop"}) + + // The even-odd flag belongs to the fill it was given with, not to the + // clip that preceded it. + testing.expect_value(t, dl.cmds[1].(Display_Fill_Path).even_odd, false) + testing.expect_value(t, dl.cmds[3].(Display_Fill_Path).even_odd, true) + testing.expect_value(t, dl.cmds[0].(Display_Clip_Path).even_odd, false) +} + +@(test) +test_display_list_nested_groups :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + dev_begin_group(dev, Rect{0, 0, 100, 100}, true, false, .Multiply, 0.5, nil) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_begin_layer(dev, "inner") + dev_begin_group(dev, Rect{10, 10, 20, 20}, false, true, .Screen, 0.25, nil) + dev_begin_mask(dev, Rect{0, 0, 5, 5}, true, Paint{}) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_end_mask(dev, nil, nil) + dev_begin_tile(dev, Rect{0, 0, 8, 8}, Rect{0, 0, 8, 8}, 8, 8, MATRIX_IDENTITY) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_end_tile(dev) + dev_end_group(dev) + dev_end_layer(dev) + dev_end_group(dev) + + dl := list_device_result(ld) + dl_expect_kinds(t, dl, { + "begin_group", "fill", "begin_layer", "begin_group", + "begin_mask", "fill", "end_mask", + "begin_tile", "fill", "end_tile", + "end_group", "end_layer", "end_group", + }) + + outer := dl.cmds[0].(Display_Begin_Group) + inner := dl.cmds[3].(Display_Begin_Group) + testing.expect_value(t, outer.area, Rect{0, 0, 100, 100}) + testing.expect_value(t, outer.isolated, true) + testing.expect_value(t, outer.knockout, false) + testing.expect_value(t, outer.blend, Blend_Mode.Multiply) + testing.expect_value(t, outer.alpha, 0.5) + testing.expect_value(t, inner.area, Rect{10, 10, 20, 20}) + testing.expect_value(t, inner.isolated, false) + testing.expect_value(t, inner.knockout, true) + testing.expect_value(t, inner.blend, Blend_Mode.Screen) + testing.expect_value(t, inner.alpha, 0.25) + + tile := dl.cmds[7].(Display_Begin_Tile) + testing.expect_value(t, tile.xstep, 8) + testing.expect_value(t, tile.ystep, 8) + testing.expect_value(t, dl.cmds[4].(Display_Begin_Mask).luminosity, true) +} + +// A recorder that wants the tile's content once, into the tile, must say 0 -- +// a positive count would have the interpreter replay the cell for every +// placement and the list would hold a thousand copies of one cell. +@(test) +test_display_list_begin_tile_asks_for_one_pass :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + n := dev_begin_tile(dev, Rect{0, 0, 4, 4}, Rect{0, 0, 4, 4}, 4, 4, MATRIX_IDENTITY) + testing.expect_value(t, n, 0) + dev_end_tile(dev) + testing.expect_value(t, len(list_device_result(ld).cmds), 2) +} + +// --------------------------------------------------------------------------- +// Replay +// --------------------------------------------------------------------------- + +@(test) +test_display_list_replay_is_faithful :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 3, 4, 20, 30) + path_move_to(&p, 1, 2) + path_curve_to(&p, 3, 4, 5, 6, 7, 8) + + font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} + txt := text_make() + defer text_destroy(&txt) + txt.font = &font + append(&txt.items, Text_Item{gid = 7, trm = Matrix{0.012, 0, 0, 0.012, 50, 60}, advance = 0.5, unicode = 'x'}) + + cs := Colorspace{kind = .Device_RGB, n = 3} + paint := dl_test_paint(&cs) + mask := Soft_Mask{luminosity = true, ctm = MATRIX_IDENTITY} + paint.soft_mask = &mask + + img := Image{width = 4, height = 4, bpc = 8} + shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}} + stroke := Stroke_State{line_width = 2.5, line_cap = .Round, line_join = .Bevel, miter_limit = 4} + stroke.dash[0] = 3 + stroke.dash[1] = 1 + stroke.dash_count = 2 + + dev_clip_path(dev, &p, true, Matrix{1, 0, 0, 1, 9, 9}) + dev_fill_path(dev, &p, true, MATRIX_IDENTITY, paint) + dev_stroke_path(dev, &p, &stroke, Matrix{2, 0, 0, 2, 0, 0}, paint) + dev_clip_text(dev, &txt, MATRIX_IDENTITY) + dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint) + dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint) + dev_ignore_text(dev, &txt, MATRIX_IDENTITY) + dev_fill_image(dev, &img, Matrix{10, 0, 0, 10, 1, 1}, paint) + dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint) + dev_clip_image_mask(dev, &img, MATRIX_IDENTITY) + dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint) + // The group's own mask is a different mask from the paints', so a replay + // that confused the two would be visible: 11.6.6 hands `begin_group` the + // mask the group's RESULT composites through, and it has to survive being + // recorded and played back like every other operand. + gmask := Soft_Mask{luminosity = true, ctm = Matrix{1, 0, 0, 1, 5, 6}} + dev_begin_group(dev, Rect{0, 0, 9, 9}, true, true, .Darken, 0.75, &gmask) + dev_begin_layer(dev, "L1") + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + dev_end_layer(dev) + dev_end_group(dev) + dev_begin_mask(dev, Rect{1, 1, 2, 2}, false, paint) + dev_end_mask(dev, &gmask, nil) + dev_pop_clip(dev) + dev_pop_clip(dev) + dev_pop_clip(dev) + + first := list_device_result(ld) + n_first := len(first.cmds) + testing.expect(t, n_first > 0, "") + + // Replay into a second recorder: the second device must see exactly what + // the first was given, command for command. + ld2, _ := list_device_make() + defer list_device_destroy(ld2) + testing.expect_value(t, display_list_run(first, list_device_as_device(ld2)), Error.None) + + second := list_device_result(ld2) + testing.expect_value(t, len(second.cmds), n_first) + if len(second.cmds) != n_first { return } + for cmd, i in first.cmds { + testing.expectf(t, dl_cmd_eq(cmd, second.cmds[i]), + "command %d (%s) did not survive the replay", i, dl_kind(cmd)) + } + + // The second list is its own recording, not a view onto the first. + a := first.cmds[1].(Display_Fill_Path) + b := second.cmds[1].(Display_Fill_Path) + testing.expect(t, a.path != b.path, "a replay into a recorder copies again") + testing.expect(t, a.paint.soft_mask != b.paint.soft_mask, "and copies the mask again") + + ga := dl_find_group(first) + gb := dl_find_group(second) + testing.expect(t, ga != nil && gb != nil, "both lists recorded the group") + if ga == nil || gb == nil { return } + testing.expect(t, ga.soft_mask != nil, "begin_group recorded its soft mask") + testing.expect(t, ga.soft_mask != &gmask, + "the group's mask must be copied out of interpreter scratch") + testing.expect(t, ga.soft_mask != gb.soft_mask, "and copied again on replay") + if ga.soft_mask != nil && gb.soft_mask != nil { + testing.expect_value(t, gb.soft_mask^, gmask) + } + + // And a replay into a foreign device gets the same values. + probe: Dl_Probe + dl_probe_init(&probe) + defer dl_probe_destroy(&probe) + testing.expect_value(t, display_list_run(first, &probe.dev), Error.None) + + // The probe implements seven hooks; the commands for the ones it leaves + // nil (clip_text, stroke_text, ignore_text, image masks, shade, mask, + // layer) pass through the dev_* wrappers and are simply not seen. + testing.expect_value(t, len(probe.events), 10) + if len(probe.events) != 10 { return } + testing.expect_value(t, probe.events[0].kind, "fill") + testing.expect_value(t, probe.events[0].even_odd, true) + testing.expect(t, dl_path_eq(probe.events[0].path, a.path), "the probe saw the recorded path") + testing.expect(t, dl_paint_eq(probe.events[0].paint, paint), "the probe saw the recorded paint") + testing.expect_value(t, probe.events[1].kind, "stroke") + testing.expect_value(t, probe.events[1].stroke, stroke) + testing.expect_value(t, probe.events[1].ctm, Matrix{2, 0, 0, 2, 0, 0}) + testing.expect_value(t, probe.events[2].kind, "text") + testing.expect(t, dl_text_eq(probe.events[2].text, first.cmds[4].(Display_Fill_Text).text), "") + testing.expect_value(t, probe.events[3].kind, "image") + testing.expect_value(t, probe.events[3].ctm, Matrix{10, 0, 0, 10, 1, 1}) + testing.expect_value(t, probe.events[4].kind, "begin_group") + testing.expect_value(t, probe.events[5].kind, "fill") + testing.expect_value(t, probe.events[6].kind, "end_group") + testing.expect_value(t, probe.events[7].kind, "pop") + testing.expect_value(t, probe.events[8].kind, "pop") + testing.expect_value(t, probe.events[9].kind, "pop") +} + +// A replay honours the target's hints, exactly as the interpreter does: a text +// device has no more use for a decoded image the second time round. +@(test) +test_display_list_replay_honours_hints :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 1, 1) + img := Image{width = 2, height = 2} + + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_fill_image(dev, &img, MATRIX_IDENTITY, Paint{}) + + probe: Dl_Probe + dl_probe_init(&probe) + defer dl_probe_destroy(&probe) + + probe.dev.hints = {.Ignore_Image} + testing.expect_value(t, display_list_run(list_device_result(ld), &probe.dev), Error.None) + testing.expect_value(t, len(probe.events), 1) + testing.expect_value(t, probe.events[0].kind, "fill") + + clear(&probe.events) + probe.dev.hints = {} + _ = display_list_run(list_device_result(ld), &probe.dev) + testing.expect_value(t, len(probe.events), 2) +} + +@(test) +test_display_list_replay_tolerates_nil :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 1, 1) + dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, Paint{}) + + testing.expect_value(t, display_list_run(nil, nil), Error.Invalid_Argument) + testing.expect_value(t, display_list_run(list_device_result(ld), nil), Error.None) + + // A device with every hook nil is the interpreter's own tolerance case. + empty: Device + testing.expect_value(t, display_list_run(list_device_result(ld), &empty), Error.None) + + // Destroying the list before the device that owns it is a misuse, and one + // that must not take the allocator with it: the list is zeroed, so the + // device has to remember where it came from. + ld2, _ := list_device_make() + display_list_destroy(list_device_result(ld2)) + display_list_destroy(list_device_result(ld2)) + list_device_destroy(ld2) + + testing.expect_value(t, list_device_as_device(nil), (^Device)(nil)) + testing.expect_value(t, list_device_result(nil), (^Display_List)(nil)) + list_device_destroy(nil) + display_list_destroy(nil) +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +@(test) +test_display_list_bounds_covers_every_command :: proc(t: ^testing.T) { + testing.expect_value(t, display_list_bounds(nil), RECT_EMPTY) + + rect := path_make() + defer path_destroy(&rect) + path_rect(&rect, 10, 20, 20, 20) // 10,20 .. 30,40 + + line := path_make() + defer path_destroy(&line) + path_move_to(&line, 200, 100) + path_line_to(&line, 260, 100) + + font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} + txt := text_make() + defer text_destroy(&txt) + txt.font = &font + // Glyph space is 1000 units to the em, and the trm scales by 0.01, so the + // em is 10 device units and the 0.5-em advance is 5 of them. + append(&txt.items, Text_Item{gid = 1, trm = Matrix{0.01, 0, 0, 0.01, 400, 500}, advance = 0.5}) + + img := Image{width = 2, height = 2} + shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 2, 2}} + stroke := Stroke_State{line_width = 4, line_join = .Bevel, miter_limit = 10} + + // Each command alone, so that a bounds case that stopped contributing + // cannot hide inside the union of the others. + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY, Paint{}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_path(list_device_as_device(ld), &rect, false, Matrix{2, 0, 0, 2, 5, 5}, Paint{}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{25, 45, 65, 85}, 1e-9) + } + { + // A clip is unioned in too: a consumer still has to address it. + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_clip_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_stroke_path(list_device_as_device(ld), &line, &stroke, MATRIX_IDENTITY, Paint{}) + b := display_list_bounds(list_device_result(ld)) + testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroked line's ink") + testing.expect(t, dl_contains(Rect{170, 70, 290, 130}, b), "and not much more than that") + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY, Paint{}) + b := display_list_bounds(list_device_result(ld)) + testing.expect(t, dl_contains(b, Rect{400, 500, 405, 500}), "the glyph origin and its advance") + testing.expect(t, dl_contains(Rect{399, 485, 420, 515}, b), "roughly one em around the origin") + } + { + // Invisible text is the exception: it paints nothing, so it cannot + // enlarge the surface. + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_ignore_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY) + testing.expect_value(t, len(list_device_result(ld).cmds), 1) + testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_image(list_device_as_device(ld), &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{600, 700, 650, 750}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_image_mask(list_device_as_device(ld), &img, Matrix{4, 0, 0, 4, 0, 0}, Paint{}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{0, 0, 4, 4}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_clip_image_mask(list_device_as_device(ld), &img, Matrix{0, 3, -3, 0, 10, 10}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{7, 10, 10, 13}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_fill_shade(list_device_as_device(ld), &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{}) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{800, 900, 820, 920}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_begin_group(list_device_as_device(ld), Rect{1, 2, 3, 4}, false, false, .Normal, 1, nil) + dev_end_group(list_device_as_device(ld)) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{1, 2, 3, 4}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev_begin_mask(list_device_as_device(ld), Rect{5, 6, 7, 8}, true, Paint{}) + dev_end_mask(list_device_as_device(ld), nil, nil) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{5, 6, 7, 8}, 1e-9) + } + { + ld, _ := list_device_make() + defer list_device_destroy(ld) + _ = dev_begin_tile(list_device_as_device(ld), Rect{2, 2, 6, 6}, Rect{2, 2, 6, 6}, 4, 4, MATRIX_IDENTITY) + dev_end_tile(list_device_as_device(ld)) + dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{2, 2, 6, 6}, 1e-9) + } + + // And all of them together: the union, from the fill's top-left corner to + // the shading's bottom-right. + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY) + + dev_fill_path(dev, &rect, false, MATRIX_IDENTITY, Paint{}) + dev_stroke_path(dev, &line, &stroke, MATRIX_IDENTITY, Paint{}) + dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{}) + dev_fill_image(dev, &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{}) + dev_fill_shade(dev, &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{}) + + dl := list_device_result(ld) + b := display_list_bounds(dl) + testing.expect(t, !dl.unbounded, "every command here has a bound") + testing.expect(t, dl_contains(b, Rect{10, 20, 30, 40}), "the fill") + testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroke") + testing.expect(t, dl_contains(b, Rect{600, 700, 650, 750}), "the image") + testing.expect(t, dl_contains(b, Rect{800, 900, 820, 920}), "the shading") + dl_rect_near(t, b, Rect{10, 20, 820, 920}, 1) +} + +@(test) +test_display_list_bounds_flags_the_unbounded :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + + dl := list_device_result(ld) + testing.expect(t, !dl.unbounded, "") + + // A shading with no /BBox paints the whole clip: there is no box for it. + shade := Shading{kind = .Axial} + dev_fill_shade(dev, &shade, MATRIX_IDENTITY, Paint{}) + testing.expect(t, dl.unbounded, "an unbounded shading must be flagged") + dl_rect_near(t, display_list_bounds(dl), Rect{0, 0, 10, 10}, 1e-9) + + // So does an infinite group area, which is what an XObject with no /BBox + // reaches the device as. + ld2, _ := list_device_make() + defer list_device_destroy(ld2) + dev_begin_group(list_device_as_device(ld2), RECT_INFINITE, false, false, .Normal, 1, nil) + dev_end_group(list_device_as_device(ld2)) + dl2 := list_device_result(ld2) + testing.expect(t, dl2.unbounded, "an infinite area must be flagged") + testing.expect_value(t, display_list_bounds(dl2), RECT_EMPTY) +} + +// --------------------------------------------------------------------------- +// Robustness +// --------------------------------------------------------------------------- + +// Past its cap the recorder must stop growing without unbalancing: a clip +// whose push was refused must not have its pop recorded, or every replay pops +// a clip the target never pushed. +@(test) +test_display_list_overflow_stays_balanced :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + dl := list_device_result(ld) + dl.max_cmds = 2 + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // recorded + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // recorded, now full + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // refused + dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) // refused + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // refused + dev_end_group(dev) // must not be recorded + dev_pop_clip(dev) // matches the refused clip + dev_pop_clip(dev) // matches the recorded clip + + testing.expect(t, dl.overflow, "the cap must be reported") + dl_expect_kinds(t, dl, {"clip", "fill", "pop"}) + + depth := 0 + for cmd in dl.cmds { + #partial switch _ in cmd { + case Display_Clip_Path: depth += 1 + case Display_Pop_Clip: depth -= 1 + } + testing.expect(t, depth >= 0, "a replay must never pop more than it pushed") + } + testing.expect_value(t, depth, 0) +} + +@(test) +test_display_list_byte_cap :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + dl := list_device_result(ld) + dl.max_bytes = 1 + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + testing.expect_value(t, len(dl.cmds), 1) + testing.expect(t, dl.bytes > 1, "the copy must be counted") + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + testing.expect_value(t, len(dl.cmds), 1) + testing.expect(t, dl.overflow, "") +} + +// close is the last thing the interpreter does, including on the error path, +// so it must leave a list that replays into a balanced device even if the page +// died inside three groups and a clip. +@(test) +test_display_list_close_unwinds_what_was_left_open :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) + dev_begin_layer(dev, "oc") + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_close(dev) + + dl := list_device_result(ld) + testing.expect(t, dl.closed, "") + dl_expect_kinds(t, dl, {"clip", "begin_group", "begin_layer", "fill", + "end_layer", "end_group", "pop"}) + + // Replaying it leaves a counting device back at depth zero. + depth := 0 + for cmd in dl.cmds { + #partial switch _ in cmd { + case Display_Clip_Path: depth += 1 + case Display_Begin_Group: depth += 1 + case Display_Begin_Layer: depth += 1 + case Display_Pop_Clip: depth -= 1 + case Display_End_Group: depth -= 1 + case Display_End_Layer: depth -= 1 + } + } + testing.expect_value(t, depth, 0) +} + +// A content stream with more Q than q reaches the device as a stray pop. The +// recorder must swallow it rather than record a pop with no push. +@(test) +test_display_list_stray_pops_are_dropped :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + dev_pop_clip(dev) + dev_pop_clip(dev) + dev_end_group(dev) + dev_end_mask(dev, nil, nil) + dev_end_tile(dev) + dev_end_layer(dev) + testing.expect_value(t, len(list_device_result(ld).cmds), 0) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 1, 1) + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_pop_clip(dev) + dev_pop_clip(dev) + dl_expect_kinds(t, list_device_result(ld), {"clip", "pop"}) +} + +// Nothing the interpreter can hand a hook may crash it, including the nil an +// image that failed to load leaves behind. +@(test) +test_display_list_tolerates_nil_arguments :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + dev_fill_path(dev, nil, false, MATRIX_IDENTITY, Paint{}) + dev_stroke_path(dev, nil, nil, MATRIX_IDENTITY, Paint{}) + dev_clip_text(dev, nil, MATRIX_IDENTITY) + dev_clip_image_mask(dev, nil, MATRIX_IDENTITY) + dev_fill_text(dev, nil, MATRIX_IDENTITY, Paint{}) + dev_fill_image(dev, nil, MATRIX_IDENTITY, Paint{}) + dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, Paint{}) + dev_fill_shade(dev, nil, MATRIX_IDENTITY, Paint{}) + dev_ignore_text(dev, nil, MATRIX_IDENTITY) + testing.expect_value(t, len(list_device_result(ld).cmds), 0) + + // A clip that could not be recorded still has to swallow its pop. + dev_pop_clip(dev) + testing.expect_value(t, len(list_device_result(ld).cmds), 0) + + // A stroke with a nil Stroke_State records the default, not zeros: a zero + // line width means a hairline, but a zero miter limit means nothing. + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 1, 1) + dev_stroke_path(dev, &p, nil, MATRIX_IDENTITY, Paint{}) + cmd, ok := list_device_result(ld).cmds[0].(Display_Stroke_Path) + testing.expect(t, ok, "") + if ok { + testing.expect_value(t, cmd.stroke, STROKE_STATE_DEFAULT) + } +} + +// A page can nest deeper than the recorder tracks. Past the cap it stops +// recording, and the pops still have to line up. +@(test) +test_display_list_deep_nesting_is_capped :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + n := MAX_DISPLAY_SCOPES + 10 + for _ in 0 ..< n { + dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) + } + for _ in 0 ..< n { + dev_end_group(dev) + } + dev_close(dev) + + dl := list_device_result(ld) + testing.expect(t, dl.overflow, "the depth cap must be reported") + begins, ends := 0, 0 + for cmd in dl.cmds { + #partial switch _ in cmd { + case Display_Begin_Group: begins += 1 + case Display_End_Group: ends += 1 + } + } + testing.expect_value(t, begins, MAX_DISPLAY_SCOPES) + testing.expect_value(t, ends, MAX_DISPLAY_SCOPES) + testing.expect_value(t, len(dl.cmds), 2 * MAX_DISPLAY_SCOPES) +} + +// Non-finite geometry must never reach a consumer through the list. +@(test) +test_display_list_keeps_nan_out :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + inf := max(f64) * 10 + dev_begin_group(dev, Rect{0, 0, inf, inf}, false, false, .Normal, inf, nil) + dev_begin_tile(dev, RECT_EMPTY, RECT_EMPTY, inf, inf, MATRIX_IDENTITY) + + dl := list_device_result(ld) + g := dl.cmds[0].(Display_Begin_Group) + testing.expect(t, is_finite(g.alpha), "a non-finite alpha must be clamped") + testing.expect_value(t, g.alpha, 1) + testing.expect(t, dl.unbounded, "an area that is not finite has no bound") + testing.expect_value(t, g.area, RECT_INFINITE) + testing.expect(t, rect_is_finite(g.area) || g.area == RECT_INFINITE, + "a NaN corner must never reach a consumer") + + tile := dl.cmds[1].(Display_Begin_Tile) + testing.expect(t, is_finite(tile.xstep) && is_finite(tile.ystep), "steps must be finite") + + // path.odin filters NaN on construction, so the bounds of whatever does + // get recorded are finite too. + testing.expect(t, rect_is_finite(display_list_bounds(dl)), "bounds must be finite") +} + +// Replaying a list into the device that is recording it is something a caller +// can do by accident. It has to terminate: the walk is over a snapshot of the +// command count, not over an array growing under it. +@(test) +test_display_list_self_replay_terminates :: proc(t: ^testing.T) { + ld, _ := list_device_make() + defer list_device_destroy(ld) + dev := list_device_as_device(ld) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 4, 4) + + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_pop_clip(dev) + + dl := list_device_result(ld) + testing.expect_value(t, len(dl.cmds), 3) + testing.expect_value(t, display_list_run(dl, dev), Error.None) + testing.expect_value(t, len(dl.cmds), 6) + dl_expect_kinds(t, dl, {"fill", "clip", "pop", "fill", "clip", "pop"}) +} + +// Everything the list copied has to come back. The runner's own leak check +// reports a leak as a warning and still calls the suite successful, so the +// teardown gets an allocator it can be held to: one recording of every command +// that owns memory, destroyed, and nothing left behind. +@(test) +test_display_list_destroy_gives_everything_back :: proc(t: ^testing.T) { + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + defer mem.tracking_allocator_destroy(&track) + alloc := mem.tracking_allocator(&track) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} + txt := text_make() + defer text_destroy(&txt) + txt.font = &font + append(&txt.items, Text_Item{gid = 1, trm = MATRIX_IDENTITY, advance = 0.5}) + + img := Image{width = 2, height = 2} + shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}} + stroke := STROKE_STATE_DEFAULT + mask := Soft_Mask{luminosity = true} + paint := Paint{alpha = 1, soft_mask = &mask} + + { + ld, err := list_device_make(alloc) + testing.expect_value(t, err, Error.None) + dev := list_device_as_device(ld) + + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY) + dev_clip_text(dev, &txt, MATRIX_IDENTITY) + dev_clip_image_mask(dev, &img, MATRIX_IDENTITY) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + dev_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY, paint) + dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint) + dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint) + dev_ignore_text(dev, &txt, MATRIX_IDENTITY) + dev_fill_image(dev, &img, MATRIX_IDENTITY, paint) + dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint) + dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint) + dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) + dev_begin_layer(dev, "a layer name long enough to be its own allocation") + mask.luminosity = false + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) + dev_end_layer(dev) + dev_end_group(dev) + dev_close(dev) + + testing.expect(t, len(list_device_result(ld).cmds) > 15, "") + testing.expect(t, len(track.allocation_map) > 0, "the recording allocated") + + list_device_destroy(ld) + } + testing.expectf(t, len(track.allocation_map) == 0, + "%d allocations were not given back", len(track.allocation_map)) + testing.expect_value(t, len(track.bad_free_array), 0) + + // And the same for a list destroyed before the device that owns it. + { + ld, _ := list_device_make(alloc) + dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, paint) + display_list_destroy(list_device_result(ld)) + list_device_destroy(ld) + } + testing.expectf(t, len(track.allocation_map) == 0, + "%d allocations survived a list destroyed out of order", len(track.allocation_map)) + testing.expect_value(t, len(track.bad_free_array), 0) +} diff --git a/device_raster.odin b/device_raster.odin new file mode 100644 index 0000000..7ae1a14 --- /dev/null +++ b/device_raster.odin @@ -0,0 +1,2141 @@ +package pdf + +import "core:math" +import "core:mem" + +// The rasterising Device: drawing calls in, pixels out. +// +// Nothing here knows anything about PDF. Everything it needs -- the resolved +// paint, the CTM, the decoded image, the loaded shading -- arrives through the +// Device vtable, and everything it does is assembled out of the primitives +// that were written and frozen before it: `path_flatten` and `stroke_flatten` +// produce geometry, `raster_fill` turns geometry into coverage, `blend_span` +// and `blend_bitmap` turn coverage into pixels, `image_decode` and +// `shading_paint` supply the two sources of colour that are not a single +// value. +// +// --------------------------------------------------------------------------- +// COORDINATE SPACES +// +// Two, and the distinction matters at every boundary below. +// +// PAGE device space what the interpreter's `ctm` maps into, what `area` +// rectangles are given in, and what `base_clip` bounds. +// Every clip box and every mask lives here. +// TARGET space pixels of the bitmap currently being painted, which is +// the page bitmap until a transparency group or a soft +// mask redirects it. A layer's bitmap covers only its own +// box, so target pixel (0,0) is page pixel `org`. +// +// Draw calls translate their CTM by -org (`rdev_ctm`) and rasterise in target +// space; clip and mask lookups translate back by +org. Keeping the clip stack +// in page space is what lets a group be pushed and popped without rebasing +// every mask underneath it. +// +// --------------------------------------------------------------------------- +// THE CLIP STACK -- the one part of this file that is a design rather than an +// assembly of existing parts. +// +// A clip is two things at once: a rectangle outside which nothing is painted, +// and (sometimes) a coverage plane inside it. They are stored separately +// because they have wildly different costs: +// +// * `rd.scissor` is the running intersection of every rectangular clip. It +// is a Raster_Clip, it is what `raster_fill` is handed, and it is free: +// narrowing it turns a path 10^9 units across into a few pixels of work +// because `raster_fill` clamps its raster box to the clip and replaces the +// parts of every edge outside the box with vertical edges standing on the +// box walls. +// +// * `rd.masks` holds the coverage planes, one per non-rectangular clip, each +// an 8-bit Bitmap the size of the box it was rasterised into. They are NOT +// flattened together on push. Flattening would make the paint-time lookup +// O(1) instead of O(depth), but it would also rebuild and re-allocate the +// whole intersected plane on every push -- and the depth is 0 in the +// overwhelming majority of content and 1 in nearly all of the rest, so the +// lookup is already O(1) in practice and the pushes are where the cost +// would actually land. +// +// An axis-aligned rectangle under a rectilinear CTM takes the scissor path. +// `matrix_is_rectilinear` is deliberately tolerance-based, so a page with +// `/Rotate 90` -- whose cos is 6.1e-17 and not 0 -- takes it too, which is the +// case the predicate exists for. The rectangle is recognised after flattening, +// in device space, so `re W n` and a hand-written `m l l l h W n` are the same +// to it. The scissor is the rectangle rounded to the nearest pixel boundary, +// so a clip edge at a fractional coordinate is up to half a pixel off; the +// alternative is an 8-bit plane the size of the page for every `re W n` in +// every content stream, which is not a trade worth making. Anything else -- +// a rotated rectangle, a circle, a glyph, a stencil -- becomes a mask. +// +// Clips nest. Each stack entry records the scissor and the mask count that +// were in force BEFORE it, so pushing intersects and popping restores exactly, +// and a pop frees the masks its push added. The interpreter guarantees the +// pushes and pops balance; `pop_clip` on an empty stack is still a no-op +// rather than an underflow. +// +// --------------------------------------------------------------------------- +// TRANSPARENCY +// +// `begin_group` renders into a fresh bitmap covering the group's area and +// `end_group` composites it back through `blend_bitmap` with the group's +// alpha, blend mode AND soft mask. All three belong to the group's RESULT +// rather than to what is drawn inside it (11.6.6), which is why `begin_group` +// carries all three and the paints inside carry none of them. `begin_mask` +// does the same into a mask layer, and `end_mask` turns it into an alpha plane +// with `mask_from_bitmap`, honouring /TR, and installs it as the current soft +// mask. +// +// A soft mask is applied to a painting operation whose `Paint.soft_mask` is +// non-nil, by multiplying its coverage -- which is exactly what the clip masks +// do, through the same code. It is deliberately NOT pushed onto the clip +// stack: `device.odin` says every `clip_*` pushes one entry and `pop_clip` +// removes one, and `end_mask` is not a `clip_*`, so an interpreter written to +// that contract will not pop for it. Keying the application off +// `Paint.soft_mask` instead means "/SMask /None" stops the masking without the +// device having to model the graphics state at all. +// +// The rendered masks live in a small cache keyed by the CONTENTS of the +// Soft_Mask each one belongs to, because an ExtGState renders its mask once -- +// when it installs it -- and a `Q` can bring an older one back into force long +// afterwards. A device that kept only the newest would modulate everything +// after that `Q` with whichever mask happened to be installed last. `end_mask` +// is told which Soft_Mask it has just drawn, so a plane is matched by what it +// was rendered from and never claimed by whatever paints next -- a claim the +// inside of a transparency group destroys, every one of its paints carrying no +// mask by design. A paint whose mask is not in the cache is painted unmasked: +// showing content is a much better failure than erasing it with somebody +// else's mask. +// +// Two simplifications, both deliberate and both visible only in cases the +// corpus notes rank as rare: +// +// * A NON-ISOLATED group is given a copy of its backdrop as its initial +// contents, so its finished bitmap already IS "contents over backdrop", +// and it is put BACK onto the page rather than composited over it. +// Compositing it would blend the backdrop with itself: `C over C` under +// Normal gives ao = 2a - a^2, so a backdrop pixel at alpha 0.5 comes back +// at 0.75 -- wrong for every group nested inside another one and for every +// page rendered with a transparent background, and wrong whether or not +// the group draws anything. Replacing is the spec's group-removal step +// (11.4.6) composed with the composite it cancels against, and is exact +// while the group's own alpha is 1 and its blend mode Normal. There is no +// such cancellation with anything else, so those groups are rendered +// ISOLATED instead. Isolated and non-isolated differ only through blend +// modes used INSIDE the group, so the case that is approximated is a +// partially transparent, non-Normal, non-isolated group -- and the +// approximation is the one every renderer without a full backdrop-removal +// pass makes. +// +// * A KNOCKOUT group is rendered as a non-knockout one. Knockout requires +// each element to composite against the group's initial backdrop rather than +// against its siblings, which means either a per-element scratch layer or +// a per-pixel restore; both cost more than the feature is worth here. +// +// --------------------------------------------------------------------------- +// ROBUSTNESS +// +// Content streams are the most malformed data in a PDF. Nothing in this file +// panics, allocates on a number taken from a file without clamping it, or +// loops on anything but a bounded count. A layer whose bitmap cannot be +// allocated suppresses its contents and still accepts its `end_group`, so the +// stack stays balanced and the rest of the page draws. Every non-finite matrix +// and every singular CTM paints nothing rather than reaching the geometry. + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +// RDEV_MAX_LAYERS bounds the group and mask stack. Form XObject recursion is +// already capped at MAX_DEPTH by the interpreter and every layer costs a +// bitmap, so this is a backstop against a content stream that opens groups +// without closing them, not a limit any real file approaches. +@(private) +RDEV_MAX_LAYERS :: 32 + +// RDEV_MAX_TAPS bounds one filtered image sample in each axis, and +// RDEV_MAX_TAP_AREA their product. Sixteen samples across a footprint is well +// past the point where more of them change an 8-bit result; the area cap is +// what stops a 4000x4000 photograph squeezed into one pixel from reading every +// source pixel for that one output. +// +// The cap is only sound while the taps still COVER the footprint. Past 16x +// minification they no longer would -- the sampler walks them at a stride of +// `footprint / taps`, so at 32x it reads every second source pixel and never +// averages the ones between, which is point sampling with extra steps and +// aliases exactly as badly. A one-pixel checkerboard minified 32x came out +// solid black or solid white depending on phase. So past this factor the +// source is BOX-REDUCED by an integer amount first (`rdev_mip_for`), which +// brings the footprint back under the cap with every source pixel counted +// exactly once. +@(private) +RDEV_MAX_TAPS :: 16 +@(private) +RDEV_MAX_TAP_AREA :: 256 + +// RDEV_MAX_MIPS is how many reduced images are kept. A tiling pattern replays +// its cell thousands of times with the same picture at the same scale, so +// without a cache the reduction would be redone for every cell. +@(private) +RDEV_MAX_MIPS :: 2 + +// RDEV_MIP_BUDGET is the total source pixels one page may spend on reductions. +// Each costs one pass over the image, which is repaid many times over by the +// sampling it makes honest -- but a content stream that forces a fresh +// reduction thousands of times must still stop somewhere. Past it the sampler +// falls back to the strided taps, which alias but cost nothing extra. +@(private) +RDEV_MIP_BUDGET :: 1 << 26 + +// RDEV_MAX_CLIPS bounds the clip stack. Every other stack in this device is +// capped and this one was not: `gstate.odin` caps q/Q at 64, but a clip needs +// no `q` -- `re W n` repeated pushes one every time and nothing pops until the +// enclosing scope ends, so the only bound was the shared token fuel. +// `device_list.odin` caps the same stack with MAX_DISPLAY_CLIPS. +@(private) +RDEV_MAX_CLIPS :: 4096 + +// RDEV_MAX_CLIP_MASKS and RDEV_MAX_CLIP_MASK_BYTES bound the coverage planes, +// which are what a clip stack actually costs here: one 8-bit plane the size of +// the intersection of the scissor and the shape, so a shape whose bounds are +// the whole page costs width*height BYTES and narrows nothing for the next one. +// `rdev_mask_row` also walks every live plane for every run of pixels painted, +// so both the memory and the per-span cost are bounded here. Past either cap a +// non-rectangular clip degrades to its bounding box: content shows inside the +// box that the shape excluded, which is the direction every other failure in +// this file takes. +@(private) +RDEV_MAX_CLIP_MASKS :: 64 +@(private) +RDEV_MAX_CLIP_MASK_BYTES :: 1 << 25 + +// RDEV_MAX_SOFT_MASKS is how many rendered soft masks are kept at once. Real +// nesting is one or two; four leaves room and bounds the memory, each plane +// being one byte per pixel of the mask's own area. +@(private) +RDEV_MAX_SOFT_MASKS :: 4 + +// RDEV_INTERPOLATE_SCALE is the magnification past which an image is smoothed +// even without /Interpolate. A producer that stretches a 2x2 image across a +// page is describing a gradient, not four squares, and every viewer smooths +// it; below this the blocks are what the file asked for. +@(private) +RDEV_INTERPOLATE_SCALE :: 4.0 + +// RDEV_RECT_TOL is how far a flattened quadrilateral may be from axis-aligned +// and still be taken as a rectangle for the scissor fast path. Device pixels. +@(private) +RDEV_RECT_TOL :: 1.0 / 512 + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +// Rdev_Mask is one coverage plane in force over a region of PAGE device space. +// `outside` is the value the mask takes beyond `box`: 0 for a clip, and for a +// luminosity soft mask the transfer of the backdrop luminosity, which is what +// makes a mask whose form covers only part of the page erase the rest. +@(private) +Rdev_Mask :: struct { + box: Raster_Clip, + bmp: Bitmap, // n = 1, exactly as wide and tall as `box` + outside: u8, +} + +// Rdev_Clip is one entry of the clip stack: what to restore when it is popped. +@(private) +Rdev_Clip :: struct { + scissor: Raster_Clip, // the scissor in force BEFORE this entry + n_masks: int, // len(masks) BEFORE this entry +} + +// Rdev_Smask is one rendered soft mask and the ExtGState mask it belongs to. +// `end_mask` names the owner, so a plane is matched by what it was drawn from +// and never by guesswork. +// +// The key is the Soft_Mask's CONTENTS, not its address. Everything a render +// depends on is in there -- the form, the CTM it was installed under, the +// backdrop and the transfer -- so two equal masks have one plane between them; +// and a Soft_Mask lives in interpreter scratch, where the same address can +// carry a different mask later in the page, so the address is not an identity +// at all. +@(private) +Rdev_Smask :: struct { + plane: Rdev_Mask, + owner_val: Soft_Mask, + owned: bool, + live: bool, + // Whether any paint has actually gone through this plane. An ExtGState + // that installs a mask and then draws nothing is common, and those planes + // are the ones to evict first. + used: bool, + clock: u64, +} + +@(private) +Rdev_Layer_Kind :: enum u8 { Group, Mask } + +// Rdev_Layer is a transparency group or a soft-mask group: a bitmap that +// drawing is redirected into, plus everything needed to put the device back +// the way it was. +@(private) +Rdev_Layer :: struct { + kind: Rdev_Layer_Kind, + bmp: Bitmap, + box: Raster_Clip, // page device space + ok: bool, // false: allocation failed, contents suppressed + + prev_scissor: Raster_Clip, + prev_mask_base: int, + + // Group. + alpha: f64, + blend: Blend_Mode, + knockout: bool, + // The soft mask the group's RESULT is composited through (11.6.6), and + // whether the layer was seeded with a copy of its backdrop. + soft_mask: ^Soft_Mask, + seeded: bool, + + // Mask. `owner` is the Soft_Mask this plane belongs to, named by + // `end_mask` rather than guessed from whatever paints next. + luminosity: bool, + backdrop: [3]f64, + owner: ^Soft_Mask, +} + +// Rdev_Mip is one box-reduced copy of a decoded image, kept so that a tile +// replayed thousands of times reduces it once. `src` is the identity of the +// image it came from: `image_decode` hands back `&img.rgba`, whose address is +// stable for the life of the Document. +@(private) +Rdev_Mip :: struct { + src: ^Bitmap, + kx, ky: int, + bmp: Bitmap, +} + +@(private) +Rdev_Sampler :: enum u8 { Nearest, Bilinear, Box } + +@(private) +Raster_Device :: struct { + // The vtable handed to the interpreter. `dev.user` points back here. + dev: Device, + allocator: mem.Allocator, + + // Needed by `image_decode` and `shading_paint`, which are the only two + // things below the Device line that still have to reach the Document. + // The vtable carries no Document, so it is set here; see + // `raster_device_make`. + doc: ^Document, + + page: ^Bitmap, + base_clip: Raster_Clip, + + // Where painting currently lands, and its origin in page device space. + target: ^Bitmap, + org_x, org_y: int, + + scissor: Raster_Clip, + clips: [dynamic]Rdev_Clip, + masks: [dynamic]Rdev_Mask, + // Clip pushes refused because the stack was full. The matching pop is + // refused too, so the stack stays balanced. + clip_over: int, + // Bytes of live coverage plane, against RDEV_MAX_CLIP_MASK_BYTES. + mask_bytes: int, + // Masks below this index are hidden. A soft mask's own form is rendered + // outside the clip that was in force when the ExtGState installed it. + mask_base: int, + + smasks: [RDEV_MAX_SOFT_MASKS]Rdev_Smask, + smask_clock: u64, + smask_suspended: bool, + + mips: [RDEV_MAX_MIPS]Rdev_Mip, + mip_next: int, + mip_budget: int, + + layers: [dynamic]Rdev_Layer, + // Depth of failed layers. Non-zero means everything painted is dropped, + // while the hooks keep accepting calls so the stack still balances. + suppress: int, + + // Scratch. `fp` is the flattened geometry of whatever is being drawn, + // `scratch` one row of combined mask values, `pix` one row of image + // samples. All three are sized once per target and reused. + fp: Flat_Path, + scratch: [dynamic]u8, + pix: [dynamic]u8, + + // First error that stopped a paint. Diagnostic only: a device that cannot + // allocate still has to let the page finish. + err: Error, +} + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- + +// raster_device_make builds a device that paints into `bmp`, which it does NOT +// own, clipped to `base_clip`. +// +// `bmp` must be PREMULTIPLIED, like every bitmap below the Device line. A page +// bitmap cleared to an opaque background satisfies that trivially, since +// premultiplied and straight agree wherever alpha is 1 or 0; +// `bitmap_unpremultiply` at the end of `render_page` is what converts the +// result back for a caller. +// +// `doc` is optional only in the sense that the device survives without one: +// images and shadings need the Document to decode and to record repairs +// against, and are skipped when it is nil. +// +// The result is owned by the caller; free it with `raster_device_destroy`. +@(private) +raster_device_make :: proc(bmp: ^Bitmap, base_clip: Raster_Clip, + allocator := context.allocator, + doc: ^Document = nil) -> (^Raster_Device, Error) { + if bmp == nil || bmp.width <= 0 || bmp.height <= 0 || len(bmp.pixels) == 0 { + return nil, .Invalid_Argument + } + + rd, alloc_err := new(Raster_Device, allocator) + if alloc_err != nil || rd == nil { + return nil, .Out_Of_Memory + } + + rd.allocator = allocator + rd.doc = doc + rd.mip_budget = RDEV_MIP_BUDGET + rd.page = bmp + rd.target = bmp + rd.base_clip = rdev_clip_intersect(base_clip, Raster_Clip{0, 0, bmp.width, bmp.height}) + rd.scissor = rd.base_clip + + rd.clips = make([dynamic]Rdev_Clip, 0, 8, allocator) + rd.masks = make([dynamic]Rdev_Mask, 0, 4, allocator) + rd.layers = make([dynamic]Rdev_Layer, 0, 4, allocator) + rd.scratch = make([dynamic]u8, 0, 0, allocator) + rd.pix = make([dynamic]u8, 0, 0, allocator) + rd.fp = flat_path_make(allocator) + + rd.dev = Device{ + user = rd, + fill_path = rd_fill_path, + stroke_path = rd_stroke_path, + clip_path = rd_clip_path, + clip_stroke_path = rd_clip_stroke_path, + clip_text = rd_clip_text, + clip_image_mask = rd_clip_image_mask, + pop_clip = rd_pop_clip, + fill_text = rd_fill_text, + stroke_text = rd_stroke_text, + fill_image = rd_fill_image, + fill_image_mask = rd_fill_image_mask, + fill_shade = rd_fill_shade, + begin_mask = rd_begin_mask, + end_mask = rd_end_mask, + has_soft_mask = rd_has_soft_mask, + begin_group = rd_begin_group, + end_group = rd_end_group, + begin_tile = rd_begin_tile, + end_tile = rd_end_tile, + close = rd_close, + } + + if !rdev_fit_scratch(rd) { + raster_device_destroy(rd) + return nil, .Out_Of_Memory + } + return rd, .None +} + +@(private) +raster_device_destroy :: proc(rd: ^Raster_Device) { + if rd == nil { return } + + for i in 0 ..< len(rd.layers) { + bitmap_destroy(&rd.layers[i].bmp) + } + for i in 0 ..< len(rd.masks) { + bitmap_destroy(&rd.masks[i].bmp) + } + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + bitmap_destroy(&rd.smasks[i].plane.bmp) + } + for i in 0 ..< RDEV_MAX_MIPS { + bitmap_destroy(&rd.mips[i].bmp) + } + + delete(rd.layers) + delete(rd.masks) + delete(rd.clips) + delete(rd.scratch) + delete(rd.pix) + flat_path_destroy(&rd.fp) + + allocator := rd.allocator + rd^ = {} + free(rd, allocator) +} + +@(private) +raster_device_as_device :: proc(rd: ^Raster_Device) -> ^Device { + if rd == nil { return nil } + return &rd.dev +} + +@(private) +rdev_from :: proc(dev: ^Device) -> ^Raster_Device { + if dev == nil || dev.user == nil { return nil } + return (^Raster_Device)(dev.user) +} + +// rdev_ready reports whether a paint can proceed at all. +@(private) +rdev_ready :: proc(rd: ^Raster_Device) -> bool { + return rd != nil && rd.suppress == 0 && rd.target != nil && rd.target.width > 0 +} + +// --------------------------------------------------------------------------- +// Boxes and spaces +// --------------------------------------------------------------------------- + +@(private) +rdev_clip_intersect :: proc "contextless" (a, b: Raster_Clip) -> Raster_Clip { + r := Raster_Clip{max(a.x0, b.x0), max(a.y0, b.y0), min(a.x1, b.x1), min(a.y1, b.y1)} + if r.x1 <= r.x0 || r.y1 <= r.y0 { return Raster_Clip{} } + return r +} + +@(private) +rdev_clip_empty :: proc "contextless" (c: Raster_Clip) -> bool { + return c.x1 <= c.x0 || c.y1 <= c.y0 +} + +@(private) +rdev_clip_width :: proc "contextless" (c: Raster_Clip) -> int { + return max(0, c.x1 - c.x0) +} + +// rdev_box_outward is the smallest pixel box containing `r`, bounded by +// `bound`. `raster_floor_clamped` and `raster_ceil_clamped` do the work +// because they already refuse to let a huge or non-finite f64 reach an integer +// conversion, and a NaN there widens to the bound rather than collapsing -- +// drawing too much is a better failure than dropping the content. +@(private) +rdev_box_outward :: proc "contextless" (r: Rect, bound: Raster_Clip) -> Raster_Clip { + return Raster_Clip{ + raster_floor_clamped(r.x0, bound.x0, bound.x1), + raster_floor_clamped(r.y0, bound.y0, bound.y1), + raster_ceil_clamped (r.x1, bound.x0, bound.x1), + raster_ceil_clamped (r.y1, bound.y0, bound.y1), + } +} + +// rdev_box_round rounds each edge to the nearest pixel boundary. This is the +// scissor for a rectangular clip, and the half-pixel it can be wrong by is the +// price of not allocating a coverage plane for every `re W n`. +@(private) +rdev_box_round :: proc "contextless" (r: Rect, bound: Raster_Clip) -> Raster_Clip { + if !rect_is_finite(r) { return rdev_box_outward(r, bound) } + return Raster_Clip{ + raster_floor_clamped(r.x0 + 0.5, bound.x0, bound.x1), + raster_floor_clamped(r.y0 + 0.5, bound.y0, bound.y1), + raster_floor_clamped(r.x1 + 0.5, bound.x0, bound.x1), + raster_floor_clamped(r.y1 + 0.5, bound.y0, bound.y1), + } +} + +// rdev_ctm folds the current layer's origin into an interpreter CTM, so that +// geometry comes out in target pixels. +@(private) +rdev_ctm :: proc(rd: ^Raster_Device, ctm: Matrix) -> Matrix { + return matrix_mul(ctm, translate_matrix(f64(-rd.org_x), f64(-rd.org_y))) +} + +// rdev_scissor is the current clip in TARGET pixels, clamped to the target. +@(private) +rdev_scissor :: proc(rd: ^Raster_Device) -> Raster_Clip { + s := rd.scissor + t := Raster_Clip{ + s.x0 - rd.org_x, s.y0 - rd.org_y, + s.x1 - rd.org_x, s.y1 - rd.org_y, + } + return rdev_clip_intersect(t, Raster_Clip{0, 0, rd.target.width, rd.target.height}) +} + +// rdev_cull is the region a path about to be flattened can put ink in: the +// scissor, in the layer-local coordinates `path_flatten` works in, with a +// pixel of margin so that nothing is decided by a rounding at the edge. +// +// Passing it is what bounds the cost of ONE painting operator. `path_flatten` +// caps a whole flattening at MAX_FLATTEN_POINTS and one curve at +// MAX_FLATTEN_DEPTH, but a content stream may spend that million points on +// every fill, stroke and clip it does, and the token budget buys tens of +// thousands of them: sixteen curves with control points ten trillion units +// away exhaust the point cap between them, and 800 painting operators over +// such a path -- 150 KB of content, and nothing of it visible -- measured 79 +// seconds. With the clip in hand the flattener stops subdividing what cannot +// reach it, which is exact rather than an approximation -- see +// `flatten_reaches`. +@(private) +rdev_cull :: proc(rd: ^Raster_Device) -> Maybe(Rect) { + if rd == nil || rd.target == nil { return nil } + sc := rdev_scissor(rd) + return Rect{f64(sc.x0) - 1, f64(sc.y0) - 1, f64(sc.x1) + 1, f64(sc.y1) + 1} +} + +// rdev_fit_scratch sizes the per-row scratch to the current target. A span can +// never be wider than the target, so this is sized once per retarget rather +// than per operation. +@(private) +rdev_fit_scratch :: proc(rd: ^Raster_Device) -> bool { + if rd.target == nil { return true } + w := rd.target.width + if len(rd.scratch) < w { + if resize(&rd.scratch, w) != nil { return false } + } + if len(rd.pix) < w * 4 { + if resize(&rd.pix, w * 4) != nil { return false } + } + return len(rd.scratch) >= w && len(rd.pix) >= w * 4 +} + +// rdev_retarget points painting at the topmost layer that has a bitmap, or at +// the page. The layer array is the owner, so the target is recomputed from it +// rather than remembered -- a `[dynamic]` reallocates, and a saved interior +// pointer would dangle the first time a fifth group was pushed. +@(private) +rdev_retarget :: proc(rd: ^Raster_Device) { + rd.target = rd.page + rd.org_x, rd.org_y = 0, 0 + for i := len(rd.layers) - 1; i >= 0; i -= 1 { + if rd.layers[i].ok { + rd.target = &rd.layers[i].bmp + rd.org_x = rd.layers[i].box.x0 + rd.org_y = rd.layers[i].box.y0 + break + } + } + _ = rdev_fit_scratch(rd) +} + +// --------------------------------------------------------------------------- +// Masks +// --------------------------------------------------------------------------- + +// rdev_smask_lookup returns the rendered plane for `sm`, or nil. +// +// The owner is recorded by `end_mask`, which is told which Soft_Mask it drew, +// so nothing here depends on what paints next. It used to: the plane was +// claimed by the first paint to arrive afterwards, and ANY paint arriving first +// without a mask destroyed the claim -- which is exactly what the inside of a +// transparency group is, every one of whose paints has `soft_mask == nil` by +// design (11.6.6). One group early on therefore disabled an ExtGState mask for +// the rest of the page. +@(private) +rdev_smask_lookup :: proc(rd: ^Raster_Device, sm: ^Soft_Mask) -> ^Rdev_Mask { + if rd.smask_suspended || sm == nil { return nil } + + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + if rd.smasks[i].live && rd.smasks[i].owned && rd.smasks[i].owner_val == sm^ { + rd.smask_clock += 1 + rd.smasks[i].clock = rd.smask_clock + rd.smasks[i].used = true + return &rd.smasks[i].plane + } + } + return nil +} + +// rd_has_soft_mask lets the interpreter skip re-rendering a mask this device +// already holds. An ExtGState naming an /SMask is applied once per `gs`, and +// every one of those used to rasterise the mask's whole form again. +@(private) +rd_has_soft_mask :: proc(dev: ^Device, mask: ^Soft_Mask) -> bool { + rd := rdev_from(dev) + if rd == nil { return false } + return rdev_smask_lookup(rd, mask) != nil +} + +@(private) +rdev_smask_for :: proc(rd: ^Raster_Device, paint: Paint) -> ^Rdev_Mask { + return rdev_smask_lookup(rd, paint.soft_mask) +} + +// rdev_smask_pinned reports whether an OPEN transparency group still needs +// this plane. A group's result is composited through its mask at `end_group` +// (11.6.6), which can be a whole content stream after the mask was installed, +// and until then nothing has painted through the plane -- which is exactly +// what makes it the first slot the scan below would otherwise give away. An +// ExtGState mask over a group whose contents install masks of their own is +// ordinary illustration output, and it lost the outer mask every time. +@(private) +rdev_smask_pinned :: proc(rd: ^Raster_Device, i: int) -> bool { + if !rd.smasks[i].live || !rd.smasks[i].owned { return false } + for &l in rd.layers { + if l.kind == .Group && l.soft_mask != nil && rd.smasks[i].owner_val == l.soft_mask^ { + return true + } + } + return false +} + +// rdev_smask_install takes ownership of a freshly rendered plane, evicting the +// least recently used slot when every one of them is taken. +@(private) +rdev_smask_install :: proc(rd: ^Raster_Device, owner: ^Soft_Mask, plane: Rdev_Mask) { + slot := -1 + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + // A plane an open group is still going to composite through is not + // available at any price -- unless every slot holds one, which takes + // RDEV_MAX_SOFT_MASKS nested masked groups and falls through to the + // same unmasked degradation as any other miss. + if rdev_smask_pinned(rd, i) { continue } + // A slot nothing ever painted through goes first, ahead of any mask + // that has been used at least once. + if !rd.smasks[i].live || !rd.smasks[i].used { + slot = i + break + } + if slot < 0 || rd.smasks[i].clock < rd.smasks[slot].clock { + slot = i + } + } + if slot < 0 { slot = 0 } + + bitmap_destroy(&rd.smasks[slot].plane.bmp) + rd.smask_clock += 1 + rd.smasks[slot] = Rdev_Smask{ + plane = plane, + owned = owner != nil, + owner_val = owner != nil ? owner^ : Soft_Mask{}, + live = true, + clock = rd.smask_clock, + } +} + +@(private) +rdev_smask_drop_all :: proc(rd: ^Raster_Device) { + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + bitmap_destroy(&rd.smasks[i].plane.bmp) + rd.smasks[i] = {} + } +} + +@(private) +rdev_has_masks :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask) -> bool { + return sm != nil || len(rd.masks) > rd.mask_base +} + +// rdev_mask_apply multiplies one mask's row into `buf`, which is indexed from +// page pixel `px0` on page row `py`. +@(private) +rdev_mask_apply :: proc(m: ^Rdev_Mask, buf: []u8, px0, py: int) { + row: []u8 + if py >= m.box.y0 && py < m.box.y1 { + row = bitmap_row(&m.bmp, py - m.box.y0) + } + for i in 0 ..< len(buf) { + if buf[i] == 0 { continue } + v := m.outside + x := px0 + i + if row != nil && x >= m.box.x0 && x < m.box.x1 { + j := x - m.box.x0 + if j < len(row) { v = row[j] } + } + switch v { + case 255: + case 0: + buf[i] = 0 + case: + buf[i] = u8((int(buf[i]) * int(v) + 127) / 255) + } + } +} + +// rdev_mask_window materialises one plane as a bitmap covering exactly `box`, +// which is what `blend_bitmap` wants: a mask aligned with the source it +// modulates. The plane's `outside` value fills whatever `box` reaches beyond +// it, so a luminosity mask whose form covers only part of the page still erases +// the rest. The result is owned by the caller. +@(private) +rdev_mask_window :: proc(rd: ^Raster_Device, m: ^Rdev_Mask, box: Raster_Clip) -> (Bitmap, bool) { + if m == nil || rdev_clip_empty(box) { return {}, false } + out, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, 1, rd.allocator) + if err != .None { + // Without the plane the group is composited unmasked. Showing content + // is the better failure, but it is still a failure worth recording. + rd.err = err + return {}, false + } + for y in 0 ..< out.height { + row := bitmap_row(&out, y) + if row == nil { continue } + for i in 0 ..< len(row) { row[i] = 255 } + rdev_mask_apply(m, row, box.x0, box.y0 + y) + } + return out, true +} + +// rdev_mask_row builds the combined mask for one run of target pixels, or +// returns nil when nothing masks it -- which is the common case and the one +// worth not paying for. The slice is scratch and is only valid until the next +// call. +@(private) +rdev_mask_row :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask, y, x0, count: int) -> []u8 { + if count <= 0 || !rdev_has_masks(rd, sm) { return nil } + if len(rd.scratch) < count { return nil } + + buf := rd.scratch[:count] + for i in 0 ..< count { buf[i] = 255 } + + px0 := x0 + rd.org_x + py := y + rd.org_y + for i in rd.mask_base ..< len(rd.masks) { + rdev_mask_apply(&rd.masks[i], buf, px0, py) + } + if sm != nil { + rdev_mask_apply(sm, buf, px0, py) + } + return buf +} + +// --------------------------------------------------------------------------- +// Clip stack +// --------------------------------------------------------------------------- + +// rdev_push_entry reserves one stack slot, or reports that the stack is full. +// A refused push is counted so that the pop it is paired with is refused too: +// popping somebody else's entry would restore the wrong scissor for the rest +// of the page. +@(private) +rdev_push_entry :: proc(rd: ^Raster_Device) -> bool { + if len(rd.clips) >= RDEV_MAX_CLIPS { + rd.clip_over += 1 + return false + } + append(&rd.clips, Rdev_Clip{scissor = rd.scissor, n_masks = len(rd.masks)}) + return true +} + +@(private) +rdev_push_clip_box :: proc(rd: ^Raster_Device, box: Raster_Clip) { + if !rdev_push_entry(rd) { return } + rd.scissor = rdev_clip_intersect(rd.scissor, box) +} + +// rdev_mask_budget_ok reports whether one more coverage plane of `box` fits. +// The caller degrades to a box clip when it does not. +@(private) +rdev_mask_budget_ok :: proc(rd: ^Raster_Device, box: Raster_Clip) -> bool { + if len(rd.masks) >= RDEV_MAX_CLIP_MASKS { return false } + w := box.x1 - box.x0 + h := box.y1 - box.y0 + if w <= 0 || h <= 0 { return true } + if w > RDEV_MAX_CLIP_MASK_BYTES / h { return false } + return rd.mask_bytes + w * h <= RDEV_MAX_CLIP_MASK_BYTES +} + +@(private) +rdev_push_clip_mask :: proc(rd: ^Raster_Device, m: Rdev_Mask) { + m := m + if !rdev_push_entry(rd) { + bitmap_destroy(&m.bmp) + return + } + if m.outside == 0 { + rd.scissor = rdev_clip_intersect(rd.scissor, m.box) + } + rd.mask_bytes += len(m.bmp.pixels) + append(&rd.masks, m) +} + +@(private) +rdev_pop_entry :: proc(rd: ^Raster_Device) { + if rd.clip_over > 0 { + rd.clip_over -= 1 + return + } + if len(rd.clips) == 0 { return } + e := pop(&rd.clips) + rd.scissor = e.scissor + for len(rd.masks) > e.n_masks { + m := pop(&rd.masks) + rd.mask_bytes = max(0, rd.mask_bytes - len(m.bmp.pixels)) + bitmap_destroy(&m.bmp) + } + rd.mask_base = min(rd.mask_base, len(rd.masks)) +} + +// --------------------------------------------------------------------------- +// Painting: solid colour +// --------------------------------------------------------------------------- + +@(private) +Rdev_Solid_Span :: struct { + rd: ^Raster_Device, + rgba: [4]f64, + mode: Blend_Mode, + sm: ^Rdev_Mask, +} + +@(private) +rdev_span_solid :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Rdev_Solid_Span)(user) + m := rdev_mask_row(c.rd, c.sm, y, x0, len(coverage)) + blend_span(c.rd.target, y, x0, coverage, c.rgba, c.mode, m) +} + +// rdev_paint_rgba is the paint's colour and constant alpha, clamped. A /None +// separation marks nothing at all on the page (ISO 32000-2, 8.6.6.4), which is +// spelled here as zero alpha so every caller gets it for free. +@(private) +rdev_paint_rgba :: proc(paint: Paint) -> [4]f64 { + if paint.cs != nil && paint.cs.is_none { + return {0, 0, 0, 0} + } + return { + blend_clamp01(paint.rgb[0]), + blend_clamp01(paint.rgb[1]), + blend_clamp01(paint.rgb[2]), + blend_clamp01(paint.alpha), + } +} + +// rdev_fill_flat is where every solid-colour paint in this file ends up: +// coverage from `raster_fill`, modulated by the clip masks and the soft mask, +// composited by `blend_span`. +@(private) +rdev_fill_flat :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool, paint: Paint) { + rgba := rdev_paint_rgba(paint) + if rgba[3] <= 0 { return } + + sc := rdev_scissor(rd) + if rdev_clip_empty(sc) { return } + + ctx := Rdev_Solid_Span{ + rd = rd, + rgba = rgba, + mode = paint.blend, + sm = rdev_smask_for(rd, paint), + } + // With a mask in force the span needs a scratch row, and painting unmasked + // because there was none would show what should have been hidden. The row + // is sized to the page once and no span can be wider than that, so this is + // a backstop rather than a path the device is expected to take. + if rdev_has_masks(rd, ctx.sm) && len(rd.scratch) < rdev_clip_width(sc) { + rd.err = .Out_Of_Memory + return + } + if err := raster_fill(fp, even_odd, sc, rdev_span_solid, &ctx, rd.allocator); err != .None { + rd.err = err + } +} + +// --------------------------------------------------------------------------- +// Building a clip mask +// --------------------------------------------------------------------------- + +@(private) +Rdev_Mask_Span :: struct { + rd: ^Raster_Device, + box: Raster_Clip, // page device space + bmp: ^Bitmap, +} + +@(private) +rdev_span_mask :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Rdev_Mask_Span)(user) + py := y + c.rd.org_y + if py < c.box.y0 || py >= c.box.y1 { return } + row := bitmap_row(c.bmp, py - c.box.y0) + if row == nil { return } + px0 := x0 + c.rd.org_x + for i in 0 ..< len(coverage) { + j := px0 + i - c.box.x0 + if j < 0 || j >= len(row) { continue } + // `max` rather than assignment: `raster_fill` never overlaps its own + // spans, but a stencil clip rasterises the same box twice. + row[j] = max(row[j], coverage[i]) + } +} + +// rdev_mask_alloc makes an empty coverage plane for `box`, in page space. +@(private) +rdev_mask_alloc :: proc(rd: ^Raster_Device, box: Raster_Clip) -> (Rdev_Mask, bool) { + if rdev_clip_empty(box) { return {}, false } + bmp, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, 1, rd.allocator) + if err != .None { + rd.err = err + return {}, false + } + return Rdev_Mask{box = box, bmp = bmp}, true +} + +// rdev_flat_is_rect reports whether a flattened path is one axis-aligned +// rectangle, and gives it back. This is the test behind the scissor fast path: +// it runs on device-space points, so `re` under any rectilinear CTM -- a +// scale, a flip, a `/Rotate 90` page -- is recognised, and so is a rectangle +// written out as four `l` operators. +@(private) +rdev_flat_is_rect :: proc(fp: ^Flat_Path) -> (Rect, bool) { + if fp == nil || len(fp.subpaths) != 1 { return {}, false } + pts := flat_path_slice(fp, 0) + // Four corners, or five with the first repeated by an explicit close. + if len(pts) == 5 { + if pts[4] != pts[0] { return {}, false } + pts = pts[:4] + } + if len(pts) != 4 { return {}, false } + + for p in pts { + if !is_finite(p.x) || !is_finite(p.y) { return {}, false } + } + // Consecutive corners must alternate between sharing an x and sharing a y. + for i in 0 ..< 4 { + a := pts[i] + b := pts[(i + 1) & 3] + dx := abs(a.x - b.x) + dy := abs(a.y - b.y) + if dx > RDEV_RECT_TOL && dy > RDEV_RECT_TOL { return {}, false } + } + r := rect_normalize(Rect{pts[0].x, pts[0].y, pts[2].x, pts[2].y}) + // The diagonal alone does not prove it is a rectangle: a degenerate + // "bowtie" shares both corners. Requiring the other two on the box, and + // requiring some area, does. + for p in pts { + near_x := abs(p.x - r.x0) <= RDEV_RECT_TOL || abs(p.x - r.x1) <= RDEV_RECT_TOL + near_y := abs(p.y - r.y0) <= RDEV_RECT_TOL || abs(p.y - r.y1) <= RDEV_RECT_TOL + if !near_x || !near_y { return {}, false } + } + if pts[0].x == pts[2].x || pts[0].y == pts[2].y { return {}, false } + return r, true +} + +// rdev_push_clip_flat pushes one clip entry for an already-flattened shape: +// a scissor when it is an axis-aligned rectangle, a coverage plane otherwise. +@(private) +rdev_push_clip_flat :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool) { + if r, is_rect := rdev_flat_is_rect(fp); is_rect { + page_r := Rect{ + r.x0 + f64(rd.org_x), r.y0 + f64(rd.org_y), + r.x1 + f64(rd.org_x), r.y1 + f64(rd.org_y), + } + rdev_push_clip_box(rd, rdev_box_round(page_r, rd.base_clip)) + return + } + + b := flat_path_bounds(fp) + page_b := Rect{ + b.x0 + f64(rd.org_x), b.y0 + f64(rd.org_y), + b.x1 + f64(rd.org_x), b.y1 + f64(rd.org_y), + } + box := rdev_clip_intersect(rd.scissor, rdev_box_outward(page_b, rd.base_clip)) + if !rdev_mask_budget_ok(rd, box) { + // Out of plane budget. The bounding box still hides everything outside + // the shape's extent, which is the most that can be kept for free. + rdev_push_clip_box(rd, box) + return + } + m, ok := rdev_mask_alloc(rd, box) + if !ok { + // No plane: clip everything away rather than nothing. A clip that + // fails open would leak the whole page through a shape. + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + + ctx := Rdev_Mask_Span{rd = rd, box = box, bmp = &m.bmp} + sc := rdev_clip_intersect(rdev_scissor(rd), Raster_Clip{ + box.x0 - rd.org_x, box.y0 - rd.org_y, + box.x1 - rd.org_x, box.y1 - rd.org_y, + }) + if !rdev_clip_empty(sc) { + if err := raster_fill(fp, even_odd, sc, rdev_span_mask, &ctx, rd.allocator); err != .None { + rd.err = err + } + } + rdev_push_clip_mask(rd, m) +} + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +// rdev_flatten flattens into the device's scratch path, through the clip, and +// records the damage when the result had to be truncated. MAX_FLATTEN_POINTS +// draws part of a shape no viewer could have drawn all of, which is a repair +// in the same sense every other clamp in the library is: the caller reading +// `doc.repairs` is entitled to know that what came out is not what the file +// asked for. +@(private) +rdev_flatten :: proc(rd: ^Raster_Device, path: ^Path, m: Matrix) { + flat_path_clear(&rd.fp) + path_flatten(&rd.fp, path, m, FLATNESS, rdev_cull(rd)) + if len(rd.fp.points) >= MAX_FLATTEN_POINTS { + add_repair(rd.doc, .Content_Syntax, 0) + } +} + +@(private) +rd_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || path == nil { return } + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { return } + + rdev_flatten(rd, path, m) + if paint.pattern != nil { + rdev_fill_pattern(rd, &rd.fp, even_odd, ctm, paint) + return + } + rdev_fill_flat(rd, &rd.fp, even_odd, paint) +} + +@(private) +rd_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || path == nil { return } + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { return } + + flat_path_clear(&rd.fp) + if err := stroke_flatten(&rd.fp, path, stroke, m, FLATNESS, rdev_cull(rd)); err != .None { + rd.err = err + return + } + // A stroke outline is always filled nonzero: the two sides of the pen + // overlap at every join that doubles back, and even-odd would punch holes + // in exactly those places. + if paint.pattern != nil { + rdev_fill_pattern(rd, &rd.fp, false, ctm, paint) + return + } + rdev_fill_flat(rd, &rd.fp, false, paint) +} + +@(private) +rd_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { + rd := rdev_from(dev) + if rd == nil { return } + if rd.suppress > 0 || rd.target == nil || path == nil { + rdev_push_entry(rd) + return + } + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + rdev_flatten(rd, path, m) + rdev_push_clip_flat(rd, &rd.fp, even_odd) +} + +@(private) +rd_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { + rd := rdev_from(dev) + if rd == nil { return } + if rd.suppress > 0 || rd.target == nil || path == nil { + rdev_push_entry(rd) + return + } + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + flat_path_clear(&rd.fp) + if err := stroke_flatten(&rd.fp, path, stroke, m, FLATNESS, rdev_cull(rd)); err != .None { + rd.err = err + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + rdev_push_clip_flat(rd, &rd.fp, false) +} + +@(private) +rd_pop_clip :: proc(dev: ^Device) { + rd := rdev_from(dev) + if rd == nil { return } + rdev_pop_entry(rd) +} + +// --------------------------------------------------------------------------- +// Text +// +// Every glyph of one showing operator goes into a single Flat_Path and is +// filled once. `Text_Item.trm` maps the glyph outline that `font_glyph_path` +// produces straight to device space -- it already folds in the font matrix, +// the text matrix, the size, the horizontal scale, the rise and the CTM -- so +// the `ctm` argument is only needed for the layer offset. +// --------------------------------------------------------------------------- + +// rdev_text_outlines accumulates the glyph outlines of one showing operator +// into the scratch flat path. +// +// `drew` is whether any outline came out. `unknown` is whether any glyph's +// shape could not be determined at all -- a font with no usable program, or a +// Type3 font, whose glyphs are content streams the interpreter runs rather +// than outlines the device can have. Only the clip hook needs the distinction, +// and it needs it badly: clipping to a shape nobody knows must reveal, not +// hide. +@(private) +rdev_text_outlines :: proc(rd: ^Raster_Device, text: ^Text) -> (drew, unknown: bool) { + flat_path_clear(&rd.fp) + if text == nil || text.font == nil { return false, false } + if font_is_type3(text.font) { return false, len(text.items) > 0 } + + glyph := path_make(rd.allocator, 32) + defer path_destroy(&glyph) + + for &it in text.items { + m := rdev_ctm(rd, it.trm) + if !matrix_is_finite(m) { continue } + path_clear(&glyph) + if font_glyph_path(text.font, it.gid, &glyph) != .None { + unknown = true + continue + } + if path_is_empty(&glyph) { continue } + path_flatten(&rd.fp, &glyph, m) + drew = true + } + return +} + +@(private) +rd_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) { return } + if drew, _ := rdev_text_outlines(rd, text); !drew { return } + if paint.pattern != nil { + rdev_fill_pattern(rd, &rd.fp, false, ctm, paint) + return + } + rdev_fill_flat(rd, &rd.fp, false, paint) +} + +@(private) +rd_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || text == nil || text.font == nil { return } + if font_is_type3(text.font) { return } + + glyph := path_make(rd.allocator, 32) + defer path_destroy(&glyph) + outline := flat_path_make(rd.allocator) + defer flat_path_destroy(&outline) + + // `stroke_flatten` wants the path in the space its width is measured in + // and the matrix that takes it to device space. Here the path is in glyph + // units and `it.trm` is that matrix, but the width is a user-space + // quantity, so it is converted by the ratio of the two matrices' + // expansions first -- which is also what keeps the pen elliptical under an + // anisotropic text matrix, exactly as it is for a path. Dashes are + // dropped: an array measured in user space cannot be walked along a path + // measured in thousandths of an em. + user_scale := matrix_expansion(ctm) + if !is_finite(user_scale) { return } + + for &it in text.items { + m := rdev_ctm(rd, it.trm) + if !matrix_is_finite(m) { continue } + glyph_scale := matrix_expansion(m) + if !(glyph_scale > 0) || !is_finite(glyph_scale) { continue } + + path_clear(&glyph) + if font_glyph_path(text.font, it.gid, &glyph) != .None { continue } + if path_is_empty(&glyph) { continue } + + st := stroke != nil ? stroke^ : STROKE_STATE_DEFAULT + st.line_width = st.line_width * user_scale / glyph_scale + st.dash_count = 0 + if !is_finite(st.line_width) { continue } + if stroke_flatten(&outline, &glyph, &st, m) != .None { continue } + } + if len(outline.points) == 0 { return } + rdev_fill_flat(rd, &outline, false, paint) +} + +@(private) +rd_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + rd := rdev_from(dev) + if rd == nil { return } + if rd.suppress > 0 || rd.target == nil { + rdev_push_entry(rd) + return + } + // `ctm` is not used: `Text_Item.trm` already maps glyph space to device + // space, and applying the CTM again would square it. + _ = ctm + drew, unknown := rdev_text_outlines(rd, text) + if drew { + rdev_push_clip_flat(rd, &rd.fp, false) + return + } + if unknown { + // A font that would not load, or a Type3 one: the shape is not + // knowable here, so the clip lets everything through. Blanking the + // rest of the page over a broken font is a far worse failure than + // showing what the text would have covered. + rdev_push_entry(rd) + return + } + // Genuinely empty text in render mode 7 clips everything away, which is + // what the operator means and is used deliberately to hide content. + rdev_push_clip_box(rd, Raster_Clip{}) +} + +// --------------------------------------------------------------------------- +// Images +// +// An image always covers the unit square of the current transform, so the +// quadrilateral that square maps to is rasterised -- which gives the edges the +// same analytic anti-aliasing every other shape gets, and bounds the whole +// operation by the clip for free -- and each covered pixel is mapped back +// through the inverse to be sampled. +// +// WHICH FILTER +// +// minified a box filter: the footprint of one device pixel in image space +// is sampled on a grid of up to 16x16 taps spread evenly across +// it. Point-sampling a 4000-pixel photograph into 200 pixels +// throws away 99.75% of it and aliases badly enough to look like +// a decoding bug, and the tap cap is what keeps the cost of that +// bounded by the OUTPUT size rather than the input's. +// magnified bilinear when the image asks for it with /Interpolate, or when +// it is blown up past RDEV_INTERPOLATE_SCALE -- a producer +// stretching a 2x2 image across a page means a gradient, and +// every viewer smooths it. +// otherwise nearest, which is what a magnified image without /Interpolate +// is specified to look like. +// +// Filtering is done on PREMULTIPLIED values and converted back to straight +// afterwards. Averaging straight RGBA would pull the colour of transparent +// pixels -- which `bitmap_unpremultiply` sets to zero and which is therefore +// black -- into every soft edge. +// --------------------------------------------------------------------------- + +@(private) +Rdev_Image_Span :: struct { + rd: ^Raster_Device, + src: ^Bitmap, + m: Matrix, // target device space -> image pixel space + alpha: f64, + mode: Blend_Mode, + sm: ^Rdev_Mask, + kind: Rdev_Sampler, + fx, fy: f64, // device-pixel footprint, in image pixels + tx, ty: int, // box taps per axis + rgba: [4]f64, // the fill colour, for a stencil mask +} + +// rdev_texel reads one image pixel as PREMULTIPLIED components on a 0..255 +// scale, clamping the coordinates to the edge. +@(private) +rdev_texel :: proc(src: ^Bitmap, x, y: int) -> [4]f64 { + cx := clamp(x, 0, src.width - 1) + cy := clamp(y, 0, src.height - 1) + off, ok := bitmap_pixel_offset(src, cx, cy) + if !ok { return {} } + a := f64(src.pixels[off + 3]) + return { + f64(src.pixels[off]) * a / 255, + f64(src.pixels[off + 1]) * a / 255, + f64(src.pixels[off + 2]) * a / 255, + a, + } +} + +@(private) +rdev_texel_gray :: proc(src: ^Bitmap, x, y: int) -> f64 { + cx := clamp(x, 0, src.width - 1) + cy := clamp(y, 0, src.height - 1) + off, ok := bitmap_pixel_offset(src, cx, cy) + if !ok { return 0 } + return f64(src.pixels[off]) +} + +@(private) +rdev_unpremul :: proc(acc: [4]f64) -> [4]u8 { + a := acc[3] + if !(a > 0.5) { return {} } + inv := 1 / a + return { + u8(clamp(acc[0] * inv * 255 + 0.5, 0, 255)), + u8(clamp(acc[1] * inv * 255 + 0.5, 0, 255)), + u8(clamp(acc[2] * inv * 255 + 0.5, 0, 255)), + u8(clamp(a + 0.5, 0, 255)), + } +} + +// rdev_box_reduce shrinks `src` by the integer factors (kx, ky), averaging +// every source pixel of each block exactly once. Colour is averaged +// PREMULTIPLIED and handed back straight, which is the same convention +// `rdev_texel` and `rdev_unpremul` meet on, so a reduced image samples +// identically to the original. +@(private) +rdev_box_reduce :: proc(src: ^Bitmap, kx, ky: int, allocator: mem.Allocator) -> (Bitmap, bool) { + if src == nil || kx < 1 || ky < 1 { return {}, false } + if src.n != 1 && src.n != 4 { return {}, false } + w := (src.width + kx - 1) / kx + h := (src.height + ky - 1) / ky + if w <= 0 || h <= 0 { return {}, false } + + out, err := bitmap_make(w, h, src.n, allocator) + if err != .None { return {}, false } + out.premultiplied = src.premultiplied + + for j in 0 ..< h { + row := bitmap_row(&out, j) + if row == nil { continue } + y0 := j * ky + y1 := min(y0 + ky, src.height) + for i in 0 ..< w { + x0 := i * kx + x1 := min(x0 + kx, src.width) + n := (x1 - x0) * (y1 - y0) + if n <= 0 { continue } + if src.n == 1 { + acc := 0.0 + for y in y0 ..< y1 { + for x in x0 ..< x1 { acc += rdev_texel_gray(src, x, y) } + } + row[i] = u8(clamp(acc / f64(n) + 0.5, 0, 255)) + continue + } + acc: [4]f64 + for y in y0 ..< y1 { + for x in x0 ..< x1 { + t := rdev_texel(src, x, y) + acc[0] += t[0]; acc[1] += t[1]; acc[2] += t[2]; acc[3] += t[3] + } + } + inv := 1 / f64(n) + px := rdev_unpremul({acc[0] * inv, acc[1] * inv, acc[2] * inv, acc[3] * inv}) + off := i * 4 + row[off] = px[0]; row[off + 1] = px[1]; row[off + 2] = px[2]; row[off + 3] = px[3] + } + } + return out, true +} + +// rdev_mip_for gives back `src` reduced by (kx, ky), building it if the cache +// does not already hold it, or nil when the page's reduction budget is spent. +@(private) +rdev_mip_for :: proc(rd: ^Raster_Device, src: ^Bitmap, kx, ky: int) -> ^Bitmap { + for i in 0 ..< RDEV_MAX_MIPS { + m := &rd.mips[i] + if m.src == src && m.kx == kx && m.ky == ky && m.bmp.width > 0 { + return &m.bmp + } + } + cost := src.width * src.height + if cost > rd.mip_budget { return nil } + + red, ok := rdev_box_reduce(src, kx, ky, rd.allocator) + if !ok { return nil } + rd.mip_budget -= cost + + slot := rd.mip_next + rd.mip_next = (rd.mip_next + 1) % RDEV_MAX_MIPS + bitmap_destroy(&rd.mips[slot].bmp) + rd.mips[slot] = Rdev_Mip{src = src, kx = kx, ky = ky, bmp = red} + return &rd.mips[slot].bmp +} + +@(private) +rdev_sample_rgba :: proc(c: ^Rdev_Image_Span, px, py: f64) -> [4]u8 { + src := c.src + switch c.kind { + case .Nearest: + t := rdev_texel(src, int(math.floor(px)), int(math.floor(py))) + return rdev_unpremul(t) + + case .Bilinear: + fx := px - 0.5 + fy := py - 0.5 + x0 := int(math.floor(fx)) + y0 := int(math.floor(fy)) + u := fx - f64(x0) + v := fy - f64(y0) + p00 := rdev_texel(src, x0, y0) + p10 := rdev_texel(src, x0 + 1, y0) + p01 := rdev_texel(src, x0, y0 + 1) + p11 := rdev_texel(src, x0 + 1, y0 + 1) + acc: [4]f64 + for i in 0 ..< 4 { + top := p00[i] + (p10[i] - p00[i]) * u + bot := p01[i] + (p11[i] - p01[i]) * u + acc[i] = top + (bot - top) * v + } + return rdev_unpremul(acc) + + case .Box: + acc: [4]f64 + sx := c.fx / f64(c.tx) + sy := c.fy / f64(c.ty) + oy := py - c.fy * 0.5 + sy * 0.5 + for j in 0 ..< c.ty { + yy := int(math.floor(oy + f64(j) * sy)) + ox := px - c.fx * 0.5 + sx * 0.5 + for i in 0 ..< c.tx { + t := rdev_texel(src, int(math.floor(ox + f64(i) * sx)), yy) + acc[0] += t[0]; acc[1] += t[1]; acc[2] += t[2]; acc[3] += t[3] + } + } + n := f64(c.tx * c.ty) + return rdev_unpremul({acc[0] / n, acc[1] / n, acc[2] / n, acc[3] / n}) + } + return {} +} + +@(private) +rdev_sample_gray :: proc(c: ^Rdev_Image_Span, px, py: f64) -> u8 { + src := c.src + switch c.kind { + case .Nearest: + return u8(clamp(rdev_texel_gray(src, int(math.floor(px)), int(math.floor(py))), 0, 255)) + + case .Bilinear: + fx := px - 0.5 + fy := py - 0.5 + x0 := int(math.floor(fx)) + y0 := int(math.floor(fy)) + u := fx - f64(x0) + v := fy - f64(y0) + top := rdev_texel_gray(src, x0, y0) + + (rdev_texel_gray(src, x0 + 1, y0) - rdev_texel_gray(src, x0, y0)) * u + bot := rdev_texel_gray(src, x0, y0 + 1) + + (rdev_texel_gray(src, x0 + 1, y0 + 1) - rdev_texel_gray(src, x0, y0 + 1)) * u + return u8(clamp(top + (bot - top) * v + 0.5, 0, 255)) + + case .Box: + acc := 0.0 + sx := c.fx / f64(c.tx) + sy := c.fy / f64(c.ty) + oy := py - c.fy * 0.5 + sy * 0.5 + for j in 0 ..< c.ty { + yy := int(math.floor(oy + f64(j) * sy)) + ox := px - c.fx * 0.5 + sx * 0.5 + for i in 0 ..< c.tx { + acc += rdev_texel_gray(src, int(math.floor(ox + f64(i) * sx)), yy) + } + } + return u8(clamp(acc / f64(c.tx * c.ty) + 0.5, 0, 255)) + } + return 0 +} + +@(private) +rdev_span_image :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Rdev_Image_Span)(user) + rd := c.rd + n := len(coverage) + if n <= 0 || len(rd.pix) < n * 4 { return } + + buf := rd.pix[:n * 4] + p := transform_point(c.m, Point{f64(x0) + 0.5, f64(y) + 0.5}) + for i in 0 ..< n { + s := rdev_sample_rgba(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b) + buf[i * 4] = s[0] + buf[i * 4 + 1] = s[1] + buf[i * 4 + 2] = s[2] + buf[i * 4 + 3] = s[3] + } + msk := rdev_mask_row(rd, c.sm, y, x0, n) + blend_span_rgba8(rd.target, y, x0, buf, c.alpha, c.mode, coverage, msk) +} + +@(private) +rdev_span_stencil :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Rdev_Image_Span)(user) + rd := c.rd + n := len(coverage) + if n <= 0 || len(rd.pix) < n { return } + + cov := rd.pix[:n] + p := transform_point(c.m, Point{f64(x0) + 0.5, f64(y) + 0.5}) + for i in 0 ..< n { + s := rdev_sample_gray(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b) + cov[i] = u8((int(coverage[i]) * int(s) + 127) / 255) + } + msk := rdev_mask_row(rd, c.sm, y, x0, n) + blend_span(rd.target, y, x0, cov, c.rgba, c.mode, msk) +} + +// rdev_flat_unit_quad replaces the flattened path with the unit square under +// `m`. Every image covers exactly that square (ISO 32000-2, 8.9.5.2). +@(private) +rdev_flat_unit_quad :: proc(fp: ^Flat_Path, m: Matrix) { + flat_path_clear(fp) + append(&fp.points, + transform_point(m, Point{0, 0}), + transform_point(m, Point{1, 0}), + transform_point(m, Point{1, 1}), + transform_point(m, Point{0, 1})) + append(&fp.subpaths, Flat_Subpath{start = 0, count = 4, closed = true}) +} + +// rdev_image_span sets up the sampler for one image draw, or reports false +// when the transform makes it invisible. +@(private) +rdev_image_span :: proc(rd: ^Raster_Device, img: ^Image, src: ^Bitmap, + m: Matrix, paint: Paint) -> (Rdev_Image_Span, bool) { + if src == nil || src.width <= 0 || src.height <= 0 { return {}, false } + + inv, ok := matrix_invert(m) + if !ok { return {}, false } + + // Unit square to image pixels: x right, y DOWN from the top row, which is + // the opposite of the unit square's y. + unit_to_img := Matrix{f64(src.width), 0, 0, -f64(src.height), 0, f64(src.height)} + dev_to_img := matrix_mul(inv, unit_to_img) + if !matrix_is_finite(dev_to_img) { return {}, false } + + c := Rdev_Image_Span{ + rd = rd, + src = src, + m = dev_to_img, + alpha = blend_clamp01(paint.alpha), + mode = paint.blend, + sm = rdev_smask_for(rd, paint), + rgba = rdev_paint_rgba(paint), + } + + // The footprint of one device pixel, summed over both device axes: an + // image rotated 30 degrees is minified along neither axis alone and along + // both together. + c.fx = clamp(abs(dev_to_img.a) + abs(dev_to_img.c), 0, 1e9) + c.fy = clamp(abs(dev_to_img.b) + abs(dev_to_img.d), 0, 1e9) + if !is_finite(c.fx) || !is_finite(c.fy) { return {}, false } + + // Past RDEV_MAX_TAPS the taps would no longer cover the footprint, so the + // source is reduced by an integer factor until they do. Everything below + // then sees a smaller image and a proportionally smaller footprint. + if c.fx > RDEV_MAX_TAPS || c.fy > RDEV_MAX_TAPS { + kx := max(1, int(math.ceil(c.fx / RDEV_MAX_TAPS))) + ky := max(1, int(math.ceil(c.fy / RDEV_MAX_TAPS))) + if red := rdev_mip_for(rd, src, kx, ky); red != nil { + c.src = red + c.m = matrix_mul(c.m, Matrix{1 / f64(kx), 0, 0, 1 / f64(ky), 0, 0}) + c.fx /= f64(kx) + c.fy /= f64(ky) + } + } + + mag := 1 / max(c.fx, c.fy, 1e-12) + switch { + case c.fx > 1.0001 || c.fy > 1.0001: + c.kind = .Box + case img != nil && img.interpolate, mag >= RDEV_INTERPOLATE_SCALE: + c.kind = .Bilinear + case: + c.kind = .Nearest + } + + c.tx = clamp(int(math.ceil(c.fx)), 1, RDEV_MAX_TAPS) + c.ty = clamp(int(math.ceil(c.fy)), 1, RDEV_MAX_TAPS) + for c.tx * c.ty > RDEV_MAX_TAP_AREA { + if c.tx >= c.ty { c.tx -= 1 } else { c.ty -= 1 } + } + return c, true +} + +@(private) +rd_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || img == nil || rd.doc == nil { return } + if img.is_mask { + rd_fill_image_mask(dev, img, ctm, paint) + return + } + if blend_clamp01(paint.alpha) <= 0 { return } + + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { return } + sc := rdev_scissor(rd) + if rdev_clip_empty(sc) { return } + + src, err := image_decode(rd.doc, img) + if err != .None || src == nil { rd.err = err; return } + + c, ok := rdev_image_span(rd, img, src, m, paint) + if !ok || src.n != 4 { return } + if rdev_has_masks(rd, c.sm) && len(rd.scratch) < rdev_clip_width(sc) { + rd.err = .Out_Of_Memory + return + } + + rdev_flat_unit_quad(&rd.fp, m) + if e := raster_fill(&rd.fp, false, sc, rdev_span_image, &c, rd.allocator); e != .None { + rd.err = e + } +} + +@(private) +rd_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || img == nil || rd.doc == nil { return } + + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { return } + sc := rdev_scissor(rd) + if rdev_clip_empty(sc) { return } + + src, err := image_decode(rd.doc, img) + if err != .None || src == nil { rd.err = err; return } + + c, ok := rdev_image_span(rd, img, src, m, paint) + if !ok || src.n != 1 { return } + if c.rgba[3] <= 0 { return } + if rdev_has_masks(rd, c.sm) && len(rd.scratch) < rdev_clip_width(sc) { + rd.err = .Out_Of_Memory + return + } + + rdev_flat_unit_quad(&rd.fp, m) + if e := raster_fill(&rd.fp, false, sc, rdev_span_stencil, &c, rd.allocator); e != .None { + rd.err = e + } +} + +@(private) +rd_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { + rd := rdev_from(dev) + if rd == nil { return } + if rd.suppress > 0 || rd.target == nil || img == nil || rd.doc == nil { + rdev_push_entry(rd) + return + } + m := rdev_ctm(rd, ctm) + src, err := image_decode(rd.doc, img) + if !matrix_is_finite(m) || err != .None || src == nil || src.n != 1 { + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + + rdev_flat_unit_quad(&rd.fp, m) + b := flat_path_bounds(&rd.fp) + page_b := Rect{ + b.x0 + f64(rd.org_x), b.y0 + f64(rd.org_y), + b.x1 + f64(rd.org_x), b.y1 + f64(rd.org_y), + } + box := rdev_clip_intersect(rd.scissor, rdev_box_outward(page_b, rd.base_clip)) + if !rdev_mask_budget_ok(rd, box) { + rdev_push_clip_box(rd, box) + return + } + mask, ok := rdev_mask_alloc(rd, box) + if !ok { + rdev_push_clip_box(rd, Raster_Clip{}) + return + } + + // The stencil is rasterised into the plane the same way a path clip is, + // then multiplied by its own coverage: `rdev_span_stencil` composites, so + // it cannot be reused here. + ctx := Rdev_Mask_Span{rd = rd, box = box, bmp = &mask.bmp} + sc := rdev_clip_intersect(rdev_scissor(rd), Raster_Clip{ + box.x0 - rd.org_x, box.y0 - rd.org_y, + box.x1 - rd.org_x, box.y1 - rd.org_y, + }) + if !rdev_clip_empty(sc) { + if e := raster_fill(&rd.fp, false, sc, rdev_span_mask, &ctx, rd.allocator); e != .None { + rd.err = e + } + c, span_ok := rdev_image_span(rd, img, src, m, Paint{alpha = 1}) + if span_ok { + rdev_mask_multiply_stencil(&mask, &c, rd) + } + } + rdev_push_clip_mask(rd, mask) +} + +// rdev_mask_multiply_stencil multiplies an already-rasterised coverage plane +// by the stencil's own samples, so the clip is the intersection of the unit +// square's shape and the bits that are set inside it. +@(private) +rdev_mask_multiply_stencil :: proc(mask: ^Rdev_Mask, c: ^Rdev_Image_Span, rd: ^Raster_Device) { + for j in 0 ..< mask.bmp.height { + row := bitmap_row(&mask.bmp, j) + if row == nil { continue } + // The plane is in page space; the sampler wants target space. + dy := f64(mask.box.y0 + j - rd.org_y) + 0.5 + dx := f64(mask.box.x0 - rd.org_x) + 0.5 + p := transform_point(c.m, Point{dx, dy}) + for i in 0 ..< len(row) { + if row[i] == 0 { continue } + s := rdev_sample_gray(c, p.x + f64(i) * c.m.a, p.y + f64(i) * c.m.b) + row[i] = u8((int(row[i]) * int(s) + 127) / 255) + } + } +} + +// --------------------------------------------------------------------------- +// Shadings and patterns +// --------------------------------------------------------------------------- + +// rdev_full_mask materialises the combined clip and soft mask as a plane the +// size of the whole target, which is the shape `shading_paint` takes. +// +// It is only built when something actually masks the paint. A shading under a +// non-rectangular clip is the one place in this file where a page-sized +// temporary is allocated, and it is bounded by the target rather than by +// anything a file controls. +@(private) +rdev_full_mask :: proc(rd: ^Raster_Device, sm: ^Rdev_Mask, sc: Raster_Clip) -> (Bitmap, bool) { + if !rdev_has_masks(rd, sm) || rdev_clip_empty(sc) { return {}, false } + + out, err := bitmap_make(rd.target.width, rd.target.height, 1, rd.allocator) + if err != .None { + rd.err = err + return {}, false + } + w := sc.x1 - sc.x0 + for y in sc.y0 ..< sc.y1 { + row := bitmap_row(&out, y) + if row == nil { continue } + buf := rdev_mask_row(rd, sm, y, sc.x0, w) + if buf == nil { + for x in sc.x0 ..< sc.x1 { row[x] = 255 } + continue + } + copy(row[sc.x0:sc.x1], buf) + } + return out, true +} + +@(private) +rd_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { + rd := rdev_from(dev) + if !rdev_ready(rd) || shade == nil || rd.doc == nil { return } + + m := rdev_ctm(rd, ctm) + if !matrix_is_finite(m) { return } + sc := rdev_scissor(rd) + if rdev_clip_empty(sc) { return } + + sm := rdev_smask_for(rd, paint) + mb, have := rdev_full_mask(rd, sm, sc) + defer if have { bitmap_destroy(&mb) } + + if err := shading_paint(rd.doc, shade, rd.target, m, sc, + paint.alpha, paint.blend, have ? &mb : nil); err != .None { + rd.err = err + } +} + +// rdev_fill_pattern paints a shape whose Paint names a pattern. +// +// The interpreter is expected to resolve patterns itself -- it is the only +// thing that knows the default user space a pattern's /Matrix is relative to +// (ISO 32000-2, 8.7.3.1), and tiling needs it to replay the cell's content +// stream. This is the fallback for a Paint that reaches the device with a +// pattern still on it: the shape becomes a clip, the shading is painted +// through it, and the CTM of the fill stands in for the default-space CTM, +// which is exactly right in the common case where the pattern is used without +// an intervening `cm` and wrong by that `cm` otherwise. A tiling pattern +// cannot be approximated this way at all and paints nothing. +@(private) +rdev_fill_pattern :: proc(rd: ^Raster_Device, fp: ^Flat_Path, even_odd: bool, + ctm: Matrix, paint: Paint) { + p := paint.pattern + if p == nil || rd.doc == nil || p.kind != .Shading { return } + if blend_clamp01(paint.alpha) <= 0 { return } + + sm := rdev_smask_for(rd, paint) + rdev_push_clip_flat(rd, fp, even_odd) + defer rdev_pop_entry(rd) + + sc := rdev_scissor(rd) + if rdev_clip_empty(sc) { return } + + mb, have := rdev_full_mask(rd, sm, sc) + defer if have { bitmap_destroy(&mb) } + + if err := pattern_paint_shading(rd.doc, p, rd.target, rdev_ctm(rd, ctm), sc, + paint.alpha, paint.blend, have ? &mb : nil); err != .None { + rd.err = err + } +} + +// --------------------------------------------------------------------------- +// Layers: transparency groups and soft masks +// --------------------------------------------------------------------------- + +// rdev_backdrop_copy seeds a non-isolated group with what is already on the +// page underneath it, converting between component counts as it goes. +// +// The layer is PREMULTIPLIED like every bitmap below the Device line, so the +// straight colour `blend_read_straight` gives back has to be multiplied by its +// alpha again before it is stored. Writing it straight is invisible wherever +// the backdrop is opaque -- premultiplied and straight agree at alpha 1 -- and +// wrong everywhere else, which is the same set of cases the composite below +// used to get wrong. +@(private) +rdev_backdrop_copy :: proc(dst, src: ^Bitmap, sx, sy: int) { + dn := dst.n + sn := src.n + for y in 0 ..< dst.height { + drow := bitmap_row(dst, y) + srow := bitmap_row(src, sy + y) + if drow == nil { continue } + for x in 0 ..< dst.width { + doff := x * dn + if doff + dn > len(drow) { break } + if srow == nil { continue } + soff := (sx + x) * sn + if sx + x < 0 || soff + sn > len(srow) { continue } + c, a := blend_read_straight(srow, soff, sn) + cs := blend_source_for(dn, c) + blend_write_premul(drow, doff, dn, [3]f64{cs[0] * a, cs[1] * a, cs[2] * a}, a) + } + } +} + +@(private) +rdev_layer_push :: proc(rd: ^Raster_Device, layer: Rdev_Layer, area: Rect, isolated: bool) { + l := layer + l.prev_scissor = rd.scissor + l.prev_mask_base = rd.mask_base + l.ok = false + + // A mask's form is positioned by the CTM the ExtGState installed it under + // and is not subject to the clip in force where it is used, so it is + // bounded by the page rather than by the current scissor. + bound := l.kind == .Mask ? rd.base_clip : rd.scissor + box := rdev_clip_intersect(bound, rdev_box_outward(area, rd.base_clip)) + + if len(rd.layers) >= RDEV_MAX_LAYERS || rdev_clip_empty(box) || rd.target == nil { + append(&rd.layers, l) + rd.suppress += 1 + return + } + + // A group always carries alpha even when the page does not, or its + // contents would composite back as if the uncovered part were opaque. + n := l.kind == .Mask ? 4 : (rd.target.n <= 2 ? 2 : 4) + bmp, err := bitmap_make(box.x1 - box.x0, box.y1 - box.y0, n, rd.allocator) + if err != .None { + rd.err = err + append(&rd.layers, l) + rd.suppress += 1 + return + } + bmp.premultiplied = true + + l.bmp = bmp + l.box = box + l.ok = true + + // A non-isolated group starts with a copy of its backdrop. That is only + // sound while the group's own alpha is 1 and its blend mode Normal -- + // otherwise the backdrop would be counted twice -- and `rdev_layer_finish` + // must then put the layer BACK rather than composite it over, or the + // backdrop's own alpha is counted twice instead: `C over C` with Normal + // takes a pixel at alpha 0.5 to 0.75. See the note there. + if l.kind == .Group && !isolated && !l.knockout && + l.blend == .Normal && l.alpha >= 1 { + rdev_backdrop_copy(&l.bmp, rd.target, box.x0 - rd.org_x, box.y0 - rd.org_y) + l.seeded = true + } + + append(&rd.layers, l) + if l.kind == .Mask { + rd.scissor = box + rd.mask_base = len(rd.masks) + rd.smask_suspended = true + } + rdev_retarget(rd) +} + +// rdev_layer_finish pops the topmost layer and does whatever that layer is +// for. It switches on the layer's own recorded kind rather than on which hook +// called it, so a content stream that closes a group with the wrong operator +// unwinds instead of corrupting the stack. +@(private) +rdev_layer_finish :: proc(rd: ^Raster_Device, transfer: ^Function) { + if len(rd.layers) == 0 { return } + l := pop(&rd.layers) + + rd.scissor = l.prev_scissor + rd.mask_base = min(l.prev_mask_base, len(rd.masks)) + if !l.ok { + rd.suppress = max(0, rd.suppress - 1) + if l.kind == .Mask { + // A mask that could not be rendered masks nothing: the paint that + // was going to use it finds no plane for its Soft_Mask and is + // drawn unmasked. Showing content is the better failure. + rd.smask_suspended = false + } + rdev_retarget(rd) + return + } + rdev_retarget(rd) + + switch l.kind { + case .Group: + // 11.6.6: the outer soft mask applies to the group's RESULT. The + // interpreter cleared it for the contents, so if it were not applied + // here it would not be applied anywhere -- which is how a gradient + // fade over a transparency group used to render as a solid shape. + win, has_win := rdev_mask_window(rd, rdev_smask_lookup(rd, l.soft_mask), l.box) + defer if has_win { bitmap_destroy(&win) } + if l.seeded { + // The layer already CONTAINS the backdrop, so compositing it over + // the backdrop again would blend the backdrop with itself. Putting + // it back is what "contents over backdrop" means, and interpolating + // towards the backdrop is exactly what the mask asks for -- Normal + // at alpha 1 is linear in the source alpha, so the two agree. + blend_bitmap_restore(rd.target, &l.bmp, + l.box.x0 - rd.org_x, l.box.y0 - rd.org_y, + has_win ? &win : nil) + } else { + blend_bitmap(rd.target, &l.bmp, + l.box.x0 - rd.org_x, l.box.y0 - rd.org_y, + l.alpha, l.blend, has_win ? &win : nil) + } + bitmap_destroy(&l.bmp) + + case .Mask: + plane, err := mask_from_bitmap(&l.bmp, l.luminosity, l.backdrop, transfer, rd.allocator) + bitmap_destroy(&l.bmp) + rd.smask_suspended = false + if err != .None { + rd.err = err + return + } + // Outside the mask's own area the value is what an unpainted pixel + // inside it would have been: the backdrop's luminosity for a + // luminosity mask, zero alpha for an alpha one, both through /TR. + lut := blend_transfer_table(transfer) + rdev_smask_install(rd, l.owner, Rdev_Mask{ + box = l.box, + bmp = plane, + outside = l.luminosity ? lut[blend_quantize(blend_lum(l.backdrop))] : lut[0], + }) + } +} + +@(private) +rd_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, + blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { + rd := rdev_from(dev) + if rd == nil { return } + rdev_layer_push(rd, Rdev_Layer{ + kind = .Group, + alpha = blend_clamp01(alpha), + blend = blend, + knockout = knockout, + soft_mask = soft_mask, + }, area, isolated) +} + +@(private) +rd_end_group :: proc(dev: ^Device) { + rd := rdev_from(dev) + if rd == nil { return } + rdev_layer_finish(rd, nil) +} + +@(private) +rd_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { + rd := rdev_from(dev) + if rd == nil { return } + bd := [3]f64{ + blend_clamp01(backdrop.rgb[0]), + blend_clamp01(backdrop.rgb[1]), + blend_clamp01(backdrop.rgb[2]), + } + rdev_layer_push(rd, Rdev_Layer{ + kind = .Mask, + alpha = 1, + luminosity = luminosity, + backdrop = bd, + }, area, true) + // Even when the layer failed, the mask's form must not be masked by + // whatever was in force where the ExtGState appeared. + rd.smask_suspended = true +} + +@(private) +rd_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { + rd := rdev_from(dev) + if rd == nil { return } + if len(rd.layers) > 0 { + rd.layers[len(rd.layers) - 1].owner = mask + } + rdev_layer_finish(rd, transfer) +} + +// --------------------------------------------------------------------------- +// Tiling patterns +// +// The device clips to the area and answers with the number of cells that meet +// it, which is `device.odin`'s "a device that cannot tile returns a count and +// lets the interpreter place each copy". Replaying the cell is strictly better +// than stamping a pre-rendered one: a rotated or skewed pattern matrix would +// otherwise resample an already-rasterised tile and show seams between the +// copies, and the interpreter is the only thing that can run a content stream +// at all. +// --------------------------------------------------------------------------- + +@(private) +rd_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { + rd := rdev_from(dev) + if rd == nil { return 0 } + + rdev_push_clip_box(rd, rdev_box_outward(area, rd.base_clip)) + if rd.suppress > 0 { return 0 } + + if !rect_is_finite(area) || !rect_is_finite(view) || !matrix_is_finite(ctm) { return 0 } + if !(xstep > 0) || !(ystep > 0) || !is_finite(xstep) || !is_finite(ystep) { return 0 } + + inv, ok := matrix_invert(ctm) + if !ok { return 0 } + pa := transform_rect(inv, area) + if !rect_is_finite(pa) { return 0 } + + i0, i1, okx := tile_range(pa.x0, pa.x1, view.x0, view.x1, xstep) + j0, j1, oky := tile_range(pa.y0, pa.y1, view.y0, view.y1, ystep) + if !okx || !oky { return 0 } + + nx := i1 - i0 + 1 + ny := j1 - j0 + 1 + if nx <= 0 || ny <= 0 { return 0 } + if f64(nx) * f64(ny) > f64(MAX_PATTERN_TILES) { return MAX_PATTERN_TILES } + return nx * ny +} + +@(private) +rd_end_tile :: proc(dev: ^Device) { + rd := rdev_from(dev) + if rd == nil { return } + rdev_pop_entry(rd) +} + +// --------------------------------------------------------------------------- +// Teardown +// --------------------------------------------------------------------------- + +// rd_close unwinds anything the content stream left open. A page that ends +// inside a group is malformed and common; compositing what it drew is much +// better than dropping it. +@(private) +rd_close :: proc(dev: ^Device) { + rd := rdev_from(dev) + if rd == nil { return } + for len(rd.layers) > 0 { + rdev_layer_finish(rd, nil) + } + for len(rd.clips) > 0 { + rdev_pop_entry(rd) + } + rdev_smask_drop_all(rd) + rd.smask_suspended = false + rd.scissor = rd.base_clip + rdev_retarget(rd) +} diff --git a/device_raster_test.odin b/device_raster_test.odin new file mode 100644 index 0000000..29d906c --- /dev/null +++ b/device_raster_test.odin @@ -0,0 +1,2965 @@ +package pdf + +import "core:fmt" +import "core:testing" +import "core:time" + +// The fixtures `cf_open`, `cf_stream` and `cf_close` live in function_test.odin. + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +@(private = "file") +WHITE :: [4]u8{255, 255, 255, 255} + +@(private = "file") +rt_bitmap :: proc(t: ^testing.T, w, h: int, fill := WHITE, loc := #caller_location) -> Bitmap { + bmp, err := bitmap_make(w, h, 4) + testing.expectf(t, err == .None, "bitmap_make: %v", err, loc = loc) + bitmap_fill(&bmp, fill) + // Opaque and fully transparent pixels are the same premultiplied as + // straight, which is what makes a cleared page bitmap a legal target. + bmp.premultiplied = true + return bmp +} + +@(private = "file") +rt_dev :: proc(t: ^testing.T, bmp: ^Bitmap, doc: ^Document = nil, + loc := #caller_location) -> ^Raster_Device { + rd, err := raster_device_make(bmp, Raster_Clip{0, 0, bmp.width, bmp.height}, + context.allocator, doc) + testing.expectf(t, err == .None, "raster_device_make: %v", err, loc = loc) + return rd +} + +@(private = "file") +rt_px :: proc(bmp: ^Bitmap, x, y: int) -> [4]u8 { + off, ok := bitmap_pixel_offset(bmp, x, y) + if !ok { return {} } + return {bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2], bmp.pixels[off + 3]} +} + +@(private = "file") +rt_expect :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, what: string, + loc := #caller_location) { + got := rt_px(bmp, x, y) + if got != want { + testing.expectf(t, false, "%s at (%d,%d): got %v, want %v", what, x, y, got, want, + loc = loc) + } +} + +@(private = "file") +rt_expect_near :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, tol: int, + what: string, loc := #caller_location) { + got := rt_px(bmp, x, y) + for i in 0 ..< 4 { + if abs(int(got[i]) - int(want[i])) > tol { + testing.expectf(t, false, "%s at (%d,%d): got %v, want %v +-%d", + what, x, y, got, want, tol, loc = loc) + return + } + } +} + +// rt_smask is the most recently installed soft-mask plane, or nil. +@(private = "file") +rt_smask :: proc(rd: ^Raster_Device) -> ^Rdev_Smask { + best: ^Rdev_Smask + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + if !rd.smasks[i].live { continue } + if best == nil || rd.smasks[i].clock > best.clock { best = &rd.smasks[i] } + } + return best +} + +// rt_mask makes a Soft_Mask distinguishable from every other one built here. +// The device keys its plane cache on a mask's CONTENTS -- everything a render +// depends on is in there -- so two all-zero Soft_Masks are one mask to it, and +// a test that wants two different masks has to build two different masks. +@(private = "file") +rt_mask :: proc(n: int) -> Soft_Mask { + return Soft_Mask{ctm = translate_matrix(f64(n), 0)} +} + +@(private = "file") +rt_paint :: proc(r, g, b: f64, alpha := 1.0) -> Paint { + return Paint{rgb = {r, g, b}, alpha = alpha, blend = .Normal} +} + +@(private = "file") +RED :: [4]u8{255, 0, 0, 255} +@(private = "file") +GREEN :: [4]u8{0, 255, 0, 255} +@(private = "file") +BLUE :: [4]u8{0, 0, 255, 255} +@(private = "file") +BLACK :: [4]u8{0, 0, 0, 255} + +// rt_rect fills one axis-aligned rectangle in device space. +@(private = "file") +rt_rect :: proc(rd: ^Raster_Device, x, y, w, h: f64, paint: Paint, + ctm := MATRIX_IDENTITY, even_odd := false) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, x, y, w, h) + dev_fill_path(raster_device_as_device(rd), &p, even_odd, ctm, paint) +} + +// rt_ink counts pixels that differ from the background. +@(private = "file") +rt_ink :: proc(bmp: ^Bitmap, background := WHITE) -> int { + n := 0 + for y in 0 ..< bmp.height { + for x in 0 ..< bmp.width { + if rt_px(bmp, x, y) != background { n += 1 } + } + } + return n +} + +// --------------------------------------------------------------------------- +// Fills +// --------------------------------------------------------------------------- + +// A pixel-aligned rectangle must land on exactly the pixels it covers, with +// full-strength colour inside and nothing at all outside. Everything else in +// this file is built on that being exact. +@(test) +test_rdev_fill_rect_exact :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_rect(rd, 2, 3, 8, 4, rt_paint(1, 0, 0)) + + rt_expect(t, &bmp, 2, 3, RED, "top-left corner of the fill") + rt_expect(t, &bmp, 9, 6, RED, "bottom-right corner of the fill") + rt_expect(t, &bmp, 5, 4, RED, "inside") + rt_expect(t, &bmp, 1, 3, WHITE, "one pixel left of the fill") + rt_expect(t, &bmp, 10, 3, WHITE, "one pixel right of the fill") + rt_expect(t, &bmp, 2, 2, WHITE, "one pixel above the fill") + rt_expect(t, &bmp, 2, 7, WHITE, "one pixel below the fill") + testing.expect_value(t, rt_ink(&bmp), 8 * 4) +} + +// Constant alpha is the paint's, not the coverage's: a fully covered pixel at +// alpha 0.5 is half the colour, and the result is opaque because the backdrop +// was. +@(test) +test_rdev_fill_alpha :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_rect(rd, 0, 0, 4, 4, rt_paint(1, 0, 0, 0.5)) + rt_expect(t, &bmp, 1, 1, {255, 128, 128, 255}, "red at half alpha over white") + rt_expect(t, &bmp, 5, 5, WHITE, "outside") +} + +// Half a pixel of coverage is half the ink. The rasteriser's coverage is +// analytic in x, so a fill that stops mid-pixel is a real test of the whole +// coverage-to-pixel path and not just of the geometry. +@(test) +test_rdev_fill_partial_coverage :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_rect(rd, 1, 0, 1.5, 4, rt_paint(0, 0, 0)) + rt_expect(t, &bmp, 1, 1, BLACK, "the fully covered column") + rt_expect_near(t, &bmp, 2, 1, {128, 128, 128, 255}, 2, "the half-covered column") + rt_expect(t, &bmp, 3, 1, WHITE, "past the fill") +} + +// Even-odd is not a corner case: `f*` means it and Type1 counters depend on +// it. A rectangle inside a rectangle is filled as a frame by even-odd and +// solid by nonzero. +@(test) +test_rdev_fill_rules_differ :: proc(t: ^testing.T) { + for even_odd in ([2]bool{false, true}) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := path_make() + defer path_destroy(&p) + path_rect(&p, 2, 2, 12, 12) + path_rect(&p, 5, 5, 6, 6) + dev_fill_path(raster_device_as_device(rd), &p, even_odd, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 3, 3, BLACK, "the outer ring is filled either way") + if even_odd { + rt_expect(t, &bmp, 8, 8, WHITE, "even-odd leaves the inner rectangle open") + } else { + rt_expect(t, &bmp, 8, 8, BLACK, "nonzero fills it, both windings being the same") + } + } +} + +// --------------------------------------------------------------------------- +// Strokes +// --------------------------------------------------------------------------- + +// A stroke of width 4 centred on x = 8 covers columns 6..9 and nothing else. +@(test) +test_rdev_stroke_width :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 8, 2) + path_line_to(&p, 8, 14) + + st := STROKE_STATE_DEFAULT + st.line_width = 4 + dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + for x in 6 ..< 10 { + rt_expect(t, &bmp, x, 8, BLACK, "inside the pen") + } + rt_expect(t, &bmp, 5, 8, WHITE, "left of the pen") + rt_expect(t, &bmp, 10, 8, WHITE, "right of the pen") + // Butt caps: the ends are square at y = 2 and y = 14 exactly. + rt_expect(t, &bmp, 7, 2, BLACK, "the first row of the stroke") + rt_expect(t, &bmp, 7, 1, WHITE, "above the butt cap") + rt_expect(t, &bmp, 7, 13, BLACK, "the last row of the stroke") + rt_expect(t, &bmp, 7, 14, WHITE, "below the butt cap") + testing.expect_value(t, rt_ink(&bmp), 4 * 12) +} + +// A zero line width is one device pixel, whatever the CTM does (8.4.3.2). +// Thin table rules are written that way and they must not vanish. +@(test) +test_rdev_stroke_hairline :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 8.5, 0) + path_line_to(&p, 8.5, 16) + + st := STROKE_STATE_DEFAULT + st.line_width = 0 + dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 8, 8, BLACK, "the hairline column") + testing.expect_value(t, rt_ink(&bmp), 16) +} + +// --------------------------------------------------------------------------- +// Clipping +// --------------------------------------------------------------------------- + +@(private = "file") +rt_clip_rect :: proc(rd: ^Raster_Device, x, y, w, h: f64, ctm := MATRIX_IDENTITY) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, x, y, w, h) + dev_clip_path(raster_device_as_device(rd), &p, false, ctm) +} + +// Clips nest by intersection, and a rectangular one under an axis-aligned CTM +// costs no coverage plane at all. +@(test) +test_rdev_nested_rect_clips :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_clip_rect(rd, 2, 2, 10, 10) + rt_clip_rect(rd, 6, 0, 14, 8) + testing.expect_value(t, len(rd.masks), 0) + testing.expect_value(t, rd.scissor, Raster_Clip{6, 2, 12, 8}) + + rt_rect(rd, 0, 0, 20, 20, rt_paint(1, 0, 0)) + rt_expect(t, &bmp, 6, 2, RED, "the top-left of the intersection") + rt_expect(t, &bmp, 11, 7, RED, "the bottom-right of the intersection") + rt_expect(t, &bmp, 5, 4, WHITE, "left of the intersection") + rt_expect(t, &bmp, 12, 4, WHITE, "right of the intersection") + rt_expect(t, &bmp, 8, 1, WHITE, "above the intersection") + rt_expect(t, &bmp, 8, 8, WHITE, "below the intersection") + testing.expect_value(t, rt_ink(&bmp), 6 * 6) + + dev_pop_clip(raster_device_as_device(rd)) + testing.expect_value(t, rd.scissor, Raster_Clip{2, 2, 12, 12}) + dev_pop_clip(raster_device_as_device(rd)) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 20, 20}) +} + +// A page with /Rotate 90 puts a cos(pi/2) of 6.1e-17 into the CTM. That is +// precisely why `matrix_is_rectilinear` is tolerance-based, and it has to keep +// the scissor path or every rotated page in existence allocates a plane. +@(test) +test_rdev_rotated_page_clip_is_a_scissor :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + // 90 degrees about the origin, then back into view: (x,y) -> (20-y, x). + ctm := matrix_mul(rotate_matrix(90), translate_matrix(20, 0)) + rt_clip_rect(rd, 2, 3, 6, 5, ctm) + + testing.expect_value(t, len(rd.masks), 0) + testing.expect_value(t, rd.scissor, Raster_Clip{12, 2, 17, 8}) + + rt_rect(rd, 0, 0, 20, 20, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 5 * 6) + rt_expect(t, &bmp, 12, 2, BLACK, "inside the rotated clip") + rt_expect(t, &bmp, 11, 2, WHITE, "outside it") +} + +// Anything that is not an axis-aligned rectangle becomes a coverage plane. +// A triangle is the smallest shape that proves the plane is consulted rather +// than just its bounding box. +@(test) +test_rdev_non_rectilinear_clip_uses_a_mask :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 16, 0) + path_line_to(&p, 0, 16) + path_close(&p) + dev_clip_path(raster_device_as_device(rd), &p, false, MATRIX_IDENTITY) + + testing.expect_value(t, len(rd.masks), 1) + testing.expect_value(t, rd.masks[0].box, Raster_Clip{0, 0, 16, 16}) + + rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 0, 0)) + rt_expect(t, &bmp, 1, 1, RED, "well inside the triangle") + rt_expect(t, &bmp, 14, 14, WHITE, "well outside it, inside its bounding box") + rt_expect(t, &bmp, 12, 12, WHITE, "the far side of the hypotenuse, x + y > 16") + rt_expect(t, &bmp, 12, 2, RED, "and the near side of it, x + y < 16") + + // Roughly half the box, which a bounding-box clip would not be. + ink := rt_ink(&bmp) + testing.expectf(t, ink > 100 && ink < 156, "triangle ink was %d, want about 128", ink) + + dev_pop_clip(raster_device_as_device(rd)) + testing.expect_value(t, len(rd.masks), 0) +} + +// A rotated rectangle is a rectangle to the eye and not to the scissor. +@(test) +test_rdev_rotated_rect_clip_uses_a_mask :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + ctm := matrix_mul(rotate_matrix(30), translate_matrix(16, 4)) + rt_clip_rect(rd, -6, -6, 12, 12, ctm) + testing.expect_value(t, len(rd.masks), 1) + + rt_rect(rd, 0, 0, 32, 32, rt_paint(0, 0, 0)) + rt_expect(t, &bmp, 16, 4, BLACK, "the centre of the rotated square") + rt_expect(t, &bmp, 0, 0, WHITE, "a corner of the page it cannot reach") +} + +// Nesting a mask inside a scissor and a scissor inside a mask must both come +// out as the intersection, and popping must give back exactly what was there. +@(test) +test_rdev_mixed_clip_nesting :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + rt_clip_rect(rd, 0, 0, 16, 8) + + tri := path_make() + defer path_destroy(&tri) + path_move_to(&tri, 0, 0) + path_line_to(&tri, 16, 0) + path_line_to(&tri, 0, 16) + path_close(&tri) + dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) + + rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 0, 0)) + rt_expect(t, &bmp, 1, 1, RED, "inside both") + rt_expect(t, &bmp, 1, 9, WHITE, "inside the triangle, below the rectangle") + rt_expect(t, &bmp, 14, 6, WHITE, "inside the rectangle, outside the triangle") + + dev_pop_clip(dev) + dev_pop_clip(dev) + testing.expect_value(t, len(rd.masks), 0) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) +} + +// An empty clip is a legal way to hide content and must hide all of it. +@(test) +test_rdev_empty_clip_paints_nothing :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_clip_rect(rd, 4, 4, 0, 0) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// --------------------------------------------------------------------------- +// Bounding +// --------------------------------------------------------------------------- + +// "A shape 10^9 units across that touches one pixel costs one pixel." The +// assertion with teeth is the ink count: the rasteriser clips every edge to +// the clip box and replaces the parts outside with walls, so a path this size +// must neither hang nor paint anything it should not. +@(test) +test_rdev_huge_shape_is_bounded_by_the_clip :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 64, 64) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_clip_rect(rd, 10, 10, 1, 1) + rt_rect(rd, -1e9, -1e9, 2e9, 2e9, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 1) + rt_expect(t, &bmp, 10, 10, BLACK, "the one pixel the clip allows") +} + +// The same path entirely outside the page must cost nothing and paint nothing. +@(test) +test_rdev_shape_outside_the_clip_paints_nothing :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + rt_rect(rd, 1e9, 1e9, 10, 10, rt_paint(0, 0, 0)) + rt_rect(rd, -1e9, 4, 10, 10, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// --------------------------------------------------------------------------- +// Transparency groups +// --------------------------------------------------------------------------- + +// A group's alpha applies to the group as a whole. Two overlapping opaque +// rectangles inside a 50% group are 50% everywhere, including where they +// overlap -- which is the entire reason groups exist. Painting them at 50% +// each without a group would darken the overlap to 64. +@(test) +test_rdev_group_alpha :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 20, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{0, 0, 20, 16}, true, false, .Normal, 0.5, nil) + rt_rect(rd, 2, 2, 8, 8, rt_paint(1, 0, 0)) + rt_rect(rd, 6, 2, 8, 8, rt_paint(1, 0, 0)) + dev_end_group(dev) + + rt_expect(t, &bmp, 3, 3, {255, 128, 128, 255}, "the first rectangle at the group's alpha") + rt_expect(t, &bmp, 7, 3, {255, 128, 128, 255}, "the overlap composites once, not twice") + rt_expect(t, &bmp, 12, 3, {255, 128, 128, 255}, "the second rectangle") + rt_expect(t, &bmp, 1, 1, WHITE, "outside both") + testing.expect_value(t, len(rd.layers), 0) +} + +// The group's bitmap covers only its area, so content outside it is dropped +// rather than composited from somewhere else in the buffer. +@(test) +test_rdev_group_area_bounds_it :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{4, 4, 12, 12}, true, false, .Normal, 1, nil) + testing.expect_value(t, len(rd.layers), 1) + testing.expect_value(t, rd.layers[0].box, Raster_Clip{4, 4, 12, 12}) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 1)) + dev_end_group(dev) + + rt_expect(t, &bmp, 4, 4, BLUE, "the group's own corner") + rt_expect(t, &bmp, 11, 11, BLUE, "its far corner") + rt_expect(t, &bmp, 3, 4, WHITE, "outside the group's area") + rt_expect(t, &bmp, 12, 12, WHITE, "past its far corner") + testing.expect_value(t, rt_ink(&bmp), 8 * 8) +} + +// A group inherits the clip in force where it was opened. +@(test) +test_rdev_group_inherits_the_clip :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + rt_clip_rect(rd, 0, 0, 8, 16) + dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Normal, 1, nil) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + dev_end_group(dev) + dev_pop_clip(dev) + + rt_expect(t, &bmp, 7, 8, BLACK, "inside the clip") + rt_expect(t, &bmp, 8, 8, WHITE, "outside it") + testing.expect_value(t, rt_ink(&bmp), 8 * 16) +} + +// A group blends as a unit: Multiply applied to the group, not to each of its +// pieces, so overlapping ink does not multiply twice. +@(test) +test_rdev_group_blend_mode :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16, {128, 128, 128, 255}) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Multiply, 1, nil) + rt_rect(rd, 0, 0, 8, 16, rt_paint(0.5, 0.5, 0.5)) + rt_rect(rd, 4, 0, 8, 16, rt_paint(0.5, 0.5, 0.5)) + dev_end_group(dev) + + // 128/255 squared, quantised: the same everywhere the group has ink. + want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) + rt_expect(t, &bmp, 1, 1, {want, want, want, 255}, "the left rectangle multiplied once") + rt_expect(t, &bmp, 6, 1, {want, want, want, 255}, "the overlap multiplied once, not twice") + rt_expect(t, &bmp, 14, 1, {128, 128, 128, 255}, "untouched backdrop") +} + +// A non-isolated group at alpha 1 in Normal mode is seeded with its backdrop, +// so a Multiply INSIDE it sees the page underneath. An isolated one does not. +@(test) +test_rdev_non_isolated_group_sees_the_backdrop :: proc(t: ^testing.T) { + for isolated in ([2]bool{false, true}) { + bmp := rt_bitmap(t, 8, 8, {128, 128, 128, 255}) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{0, 0, 8, 8}, isolated, false, .Normal, 1, nil) + p := rt_paint(0.5, 0.5, 0.5) + p.blend = .Multiply + rt_rect(rd, 0, 0, 8, 8, p) + dev_end_group(dev) + + got := rt_px(&bmp, 4, 4) + if isolated { + // Nothing to multiply with, so the source passes through. + rt_expect(t, &bmp, 4, 4, {128, 128, 128, 255}, "isolated group has no backdrop") + } else { + want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) + testing.expectf(t, got[0] == want, + "non-isolated group: got %v, want %d", got, want) + } + } +} + +// --------------------------------------------------------------------------- +// Soft masks +// --------------------------------------------------------------------------- + +// A luminosity mask: what the mask's form paints white shows, what it leaves +// bare is masked out by the black backdrop. +@(test) +test_rdev_soft_mask_luminosity :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 8, 16, rt_paint(1, 1, 1)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + + installed := rt_smask(rd) + testing.expect(t, installed != nil, "end_mask must install a mask") + testing.expect_value(t, installed.plane.outside, u8(0)) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + + rt_expect(t, &bmp, 3, 3, RED, "under the white part of the mask") + rt_expect(t, &bmp, 12, 3, WHITE, "under the part the mask left black") + testing.expect_value(t, rt_ink(&bmp), 8 * 16) +} + +// An alpha mask takes the group's alpha and ignores its colour: black at half +// alpha masks to half, exactly as white at half alpha would. +@(test) +test_rdev_soft_mask_alpha :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 8, 8}, false, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 4, 8, rt_paint(0, 0, 0, 0.5)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 8, 8, p) + + rt_expect_near(t, &bmp, 1, 1, {255, 127, 127, 255}, 2, "half-alpha region of the mask") + rt_expect(t, &bmp, 6, 1, WHITE, "the region the mask never covered") +} + +// The mask multiplies coverage rather than replacing it, so a half-covered +// pixel under a half mask is a quarter of the ink. +@(test) +test_rdev_soft_mask_multiplies_coverage :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 8, 8}, false, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0, 0.5)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + + p := rt_paint(0, 0, 0) + p.soft_mask = &sm + // Half a pixel wide, so coverage is 0.5 and the mask another 0.5. + rt_rect(rd, 2, 0, 0.5, 8, p) + rt_expect_near(t, &bmp, 2, 4, {191, 191, 191, 255}, 3, "a quarter of full black") +} + +// The mask applies only to paints that carry one. That is what makes +// "/SMask /None", and the `Q` that restores it, work without the device +// modelling the graphics state. +@(test) +test_rdev_soft_mask_only_applies_when_the_paint_has_one :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 8, 8}, true, rt_paint(0, 0, 0)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) // an empty mask: everything is masked out + + masked := rt_paint(1, 0, 0) + masked.soft_mask = &sm + rt_rect(rd, 0, 0, 4, 8, masked) + testing.expect_value(t, rt_ink(&bmp), 0) + + rt_rect(rd, 0, 0, 4, 8, rt_paint(1, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 4 * 8) +} + +// The mask's own form is not subject to the clip that was in force where the +// ExtGState installed it, nor to any mask already in place. +@(test) +test_rdev_soft_mask_form_ignores_the_current_clip :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + rt_clip_rect(rd, 0, 0, 4, 16) + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + testing.expect(t, rd.smask_suspended, "a mask must not mask itself") + rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 1, 1)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + testing.expect(t, !rd.smask_suspended, "end_mask re-arms masking") + dev_pop_clip(dev) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + // The mask covered the whole page, not just the clip that was in force. + testing.expect_value(t, rt_ink(&bmp), 16 * 16) +} + +// --------------------------------------------------------------------------- +// Images +// --------------------------------------------------------------------------- + +// A 2x2 picture: red and green on the top row, blue and black on the bottom. +@(private = "file") +RT_RGB2 :: "\xff\x00\x00\x00\xff\x00" + "\x00\x00\xff\x00\x00\x00" + +@(private = "file") +rt_image :: proc(t: ^testing.T, doc: ^Document, dict_src, payload: string, + loc := #caller_location) -> ^Image { + img, err := image_load(doc, cf_stream(doc, dict_src, transmute([]byte)payload), nil) + testing.expectf(t, err == .None, "image_load: %v", err, loc = loc) + return img +} + +// The upright placement: the unit square flipped in y, which is what a page's +// base matrix does to every image on it. +@(private = "file") +rt_place :: proc(size: f64) -> Matrix { + return Matrix{size, 0, 0, -size, 0, size} +} + +// An image covers the unit square of the CTM. Under a plain scale its four +// source pixels land in the four quadrants, top row first. +@(test) +test_rdev_image_scaled :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_image(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 0, 0, RED, "image pixel (0,0) is the top-left") + rt_expect(t, &bmp, 7, 0, GREEN, "image pixel (1,0) is the top-right") + rt_expect(t, &bmp, 0, 7, BLUE, "image pixel (0,1) is the bottom-left") + rt_expect(t, &bmp, 7, 7, BLACK, "image pixel (1,1) is the bottom-right") + testing.expect_value(t, rt_ink(&bmp), 8 * 8) +} + +// Without the y flip the image is upside down, which is the whole difference +// between PDF's y-up user space and the y-down bitmap. +@(test) +test_rdev_image_flipped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_image(raster_device_as_device(rd), img, scale_matrix(8, 8), rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 0, 7, RED, "the top row is now at the bottom") + rt_expect(t, &bmp, 7, 7, GREEN, "") + rt_expect(t, &bmp, 0, 0, BLUE, "and the bottom row at the top") + rt_expect(t, &bmp, 7, 0, BLACK, "") +} + +// Rotated by 90 degrees the four pixels move round one quadrant, and the +// magnification is deliberately kept under the smoothing threshold so the +// corners are exact source pixels rather than blends. +@(test) +test_rdev_image_rotated :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + + bmp := rt_bitmap(t, 6, 6) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + // (x, y) -> (6 - y, x) after the upright placement. + ctm := matrix_mul(rt_place(6), matrix_mul(rotate_matrix(90), translate_matrix(6, 0))) + dev_fill_image(raster_device_as_device(rd), img, ctm, rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 5, 0, RED, "the top-left pixel rotated to the top-right") + rt_expect(t, &bmp, 5, 5, GREEN, "") + rt_expect(t, &bmp, 0, 0, BLUE, "") + rt_expect(t, &bmp, 0, 5, BLACK, "") + testing.expect_value(t, rt_ink(&bmp), 6 * 6) +} + +// Minification averages. A source of alternating white and black rows squeezed +// four to one must come out mid-grey; point-sampling it would come out solid +// white or solid black, which is what makes a downscaled photograph look +// broken. +@(test) +test_rdev_image_minified_is_averaged :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + // 4x4 DeviceGray, rows 255 0 255 0. + rows := "\xff\xff\xff\xff" + "\x00\x00\x00\x00" + "\xff\xff\xff\xff" + "\x00\x00\x00\x00" + img := rt_image(t, doc, + "<< /Width 4 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", rows) + + bmp := rt_bitmap(t, 2, 2) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_image(raster_device_as_device(rd), img, rt_place(2), rt_paint(0, 0, 0)) + + for y in 0 ..< 2 { + for x in 0 ..< 2 { + rt_expect_near(t, &bmp, x, y, {128, 128, 128, 255}, 1, "averaged 2x2 block") + } + } +} + +// /Interpolate asks for smoothing at any magnification; without it a modest +// blow-up stays blocky, which is what the format specifies. +@(test) +test_rdev_image_interpolate_flag :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + for interpolate in ([2]bool{false, true}) { + src := "<< /Width 2 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray >>" + if interpolate { + src = "<< /Width 2 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray /Interpolate true >>" + } + img := rt_image(t, doc, src, "\x00\xff") + + bmp := rt_bitmap(t, 6, 2) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_image(raster_device_as_device(rd), img, Matrix{6, 0, 0, -2, 0, 2}, + rt_paint(0, 0, 0)) + + // The pixel straddling the two source samples: a hard edge without + // interpolation, a blend with it. + got := rt_px(&bmp, 3, 0) + if interpolate { + testing.expectf(t, got[0] > 128 && got[0] < 255, + "/Interpolate must smooth the join, got %v", got) + } else { + testing.expectf(t, got[0] == 255, "no /Interpolate must keep it hard, got %v", got) + } + } +} + +// An image mask is a stencil painted in the current fill colour. +@(test) +test_rdev_image_mask_paints_the_fill_colour :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + // 2x2, one bit per pixel, one byte per row. A zero sample paints. + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_image_mask(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 1)) + + rt_expect(t, &bmp, 0, 0, BLUE, "sample 0 paints in the fill colour") + rt_expect(t, &bmp, 7, 0, WHITE, "sample 1 paints nothing") + rt_expect(t, &bmp, 0, 7, WHITE, "") + rt_expect(t, &bmp, 7, 7, BLUE, "") +} + +// The same stencil used as a clip: what it would have painted is what shows. +@(test) +test_rdev_clip_image_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_clip_image_mask(dev, img, rt_place(8)) + testing.expect_value(t, len(rd.masks), 1) + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0)) + dev_pop_clip(dev) + + rt_expect(t, &bmp, 0, 0, RED, "the stencil's set quadrant") + rt_expect(t, &bmp, 7, 0, WHITE, "its clear quadrant") + rt_expect(t, &bmp, 7, 7, RED, "") + testing.expect_value(t, len(rd.masks), 0) +} + +// An image is clipped like anything else, and its constant alpha applies. +@(test) +test_rdev_image_clipped_and_alpha :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", "\xff\x00\x00") + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + rt_clip_rect(rd, 0, 0, 4, 8) + dev_fill_image(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 0, 0.5)) + + rt_expect(t, &bmp, 1, 1, {255, 128, 128, 255}, "half-alpha image inside the clip") + rt_expect(t, &bmp, 5, 1, WHITE, "outside the clip") +} + +// An image under a degenerate CTM paints nothing rather than dividing by zero. +@(test) +test_rdev_image_degenerate_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_fill_image(dev, img, scale_matrix(0, 8), rt_paint(0, 0, 0)) + dev_fill_image(dev, img, Matrix{}, rt_paint(0, 0, 0)) + inf := max(f64) + dev_fill_image(dev, img, Matrix{inf * 2, 0, 0, 8, 0, 0}, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// --------------------------------------------------------------------------- +// Shadings and patterns +// --------------------------------------------------------------------------- + +@(private = "file") +RT_AXIAL :: `<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 16 0] + /Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> + /Extend [true true] >>` + +// `sh` fills the current clip with the shading. The ramp must run the right +// way and must stop at the clip. +@(test) +test_rdev_fill_shade :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) + testing.expectf(t, err == .None, "shading_load: %v", err) + + bmp := rt_bitmap(t, 16, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + dev_fill_shade(raster_device_as_device(rd), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + left := rt_px(&bmp, 0, 4) + right := rt_px(&bmp, 15, 4) + testing.expectf(t, left[0] < 24, "the dark end of the ramp: %v", left) + testing.expectf(t, right[0] > 231, "the light end of the ramp: %v", right) + prev := -1 + for x in 0 ..< 16 { + v := int(rt_px(&bmp, x, 4)[0]) + testing.expectf(t, v > prev, "the ramp must increase at x=%d: %d after %d", x, v, prev) + prev = v + } +} + +// A shading is bounded by the clip, including by a non-rectangular one, which +// is the path that materialises a full-target mask plane. +@(test) +test_rdev_fill_shade_through_a_mask_clip :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) + testing.expectf(t, err == .None, "shading_load: %v", err) + + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + tri := path_make() + defer path_destroy(&tri) + path_move_to(&tri, 0, 0) + path_line_to(&tri, 16, 0) + path_line_to(&tri, 0, 16) + path_close(&tri) + dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) + testing.expect_value(t, len(rd.masks), 1) + + dev_fill_shade(dev, sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + dev_pop_clip(dev) + + testing.expect(t, rt_px(&bmp, 12, 2) != WHITE, "inside the clip the shading paints") + rt_expect(t, &bmp, 12, 12, WHITE, "outside it nothing does") +} + +// A shading pattern named on a fill paints through the shape. The interpreter +// normally resolves patterns itself; this is the device's fallback for one +// that arrives on a Paint. +@(test) +test_rdev_shading_pattern_fill :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + src := "<< /PatternType 2 /Shading " + RT_AXIAL + " >>" + pat, err := pattern_load(doc, cf_parse(doc, src), nil) + testing.expectf(t, err == .None, "pattern_load: %v", err) + + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + p := rt_paint(0, 0, 0) + p.pattern = pat + rt_rect(rd, 4, 4, 8, 8, p) + + testing.expect(t, rt_px(&bmp, 5, 5) != WHITE, "the shape is painted with the shading") + testing.expect(t, rt_px(&bmp, 10, 5)[0] > rt_px(&bmp, 5, 5)[0], "and the ramp runs across it") + rt_expect(t, &bmp, 2, 5, WHITE, "outside the shape nothing is painted") + rt_expect(t, &bmp, 13, 5, WHITE, "") + testing.expect_value(t, len(rd.clips), 0) + testing.expect_value(t, len(rd.masks), 0) +} + +// A tiling pattern cannot be replayed by the device, so it paints nothing -- +// but it must also leave the clip stack exactly as it found it. +@(test) +test_rdev_tiling_pattern_on_a_paint_is_left_alone :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + src := `<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 4 4] + /XStep 4 /YStep 4 /Resources << >> /Length 0 >>` + pat, err := pattern_load(doc, cf_stream(doc, src, nil), nil) + testing.expectf(t, err == .None, "pattern_load: %v", err) + + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + p := rt_paint(0, 0, 0) + p.pattern = pat + rt_rect(rd, 0, 0, 16, 16, p) + testing.expect_value(t, rt_ink(&bmp), 0) + testing.expect_value(t, len(rd.clips), 0) +} + +// begin_tile reports how many cells meet the area and clips to it; end_tile +// gives the clip back. +@(test) +test_rdev_begin_tile_counts_cells :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // Cells of 4 covering 0..16 meet indices -1..4 in each axis: 6 x 6. + n := dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 4, 4, MATRIX_IDENTITY) + testing.expect_value(t, n, 36) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) + dev_end_tile(dev) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 32, 32}) + + // A degenerate step is not a lattice at all. + testing.expect_value(t, + dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 0, 4, MATRIX_IDENTITY), 0) + dev_end_tile(dev) + + // A singular matrix collapses every cell. + testing.expect_value(t, + dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 4, 4, Matrix{}), 0) + dev_end_tile(dev) + + // A step small enough to make one fill cost a million content streams is + // capped rather than reported honestly. + testing.expect_value(t, + dev_begin_tile(dev, Rect{0, 0, 32, 32}, Rect{0, 0, 1, 1}, 1e-6, 1e-6, MATRIX_IDENTITY), + MAX_PATTERN_TILES) + dev_end_tile(dev) + testing.expect_value(t, len(rd.clips), 0) +} + + +// --------------------------------------------------------------------------- +// Text +// +// A standard-14 font has metrics but no program, so it draws nothing by +// design. Testing the text hooks needs a font that really has an outline, and +// the smallest one that does is a TrueType with a single square glyph. +// --------------------------------------------------------------------------- + +// RT_GLYF is one simple glyph: a square from (100,0) to (900,700) in a +// 1000-unit em. One contour, four on-curve points, 16-bit deltas. +@(private = "file") +RT_GLYF := [?]u8{ + 0x00, 0x01, // numberOfContours + 0x00, 0x64, 0x00, 0x00, 0x03, 0x84, 0x02, 0xBC, // xMin yMin xMax yMax + 0x00, 0x03, // endPtsOfContours[0] + 0x00, 0x00, // instructionLength + 0x01, 0x01, 0x01, 0x01, // flags: on-curve, long deltas + 0x00, 0x64, 0x03, 0x20, 0x00, 0x00, 0xFC, 0xE0, // x: 100 800 0 -800 + 0x00, 0x00, 0x00, 0x00, 0x02, 0xBC, 0x00, 0x00, // y: 0 0 700 0 +} + +@(private = "file") +rt_be16 :: proc(b: []byte, off: int, v: u16) { + b[off] = u8(v >> 8) + b[off + 1] = u8(v) +} + +@(private = "file") +rt_be32 :: proc(b: []byte, off: int, v: u32) { + b[off] = u8(v >> 24); b[off + 1] = u8(v >> 16) + b[off + 2] = u8(v >> 8); b[off + 3] = u8(v) +} + +// rt_square_ttf assembles head, maxp, loca and glyf around RT_GLYF. Glyph 1 is +// the square; glyph 0 is the empty .notdef every font must have. +@(private = "file") +rt_square_ttf :: proc(doc: ^Document) -> []byte { + head := make([]byte, 54, doc.allocator) + rt_be16(head, 18, 1000) // unitsPerEm; indexToLocFormat stays 0 + + maxp := make([]byte, 32, doc.allocator) + rt_be32(maxp, 0, 0x00010000) + rt_be16(maxp, 4, 2) + + loca := make([]byte, 6, doc.allocator) + rt_be16(loca, 0, 0) + rt_be16(loca, 2, 0) + rt_be16(loca, 4, u16(len(RT_GLYF) / 2)) + + tags := [4]string{"head", "maxp", "loca", "glyf"} + datas := [4][]byte{head, maxp, loca, RT_GLYF[:]} + + total := 12 + 16 * len(tags) + for d in datas { total += (len(d) + 3) & ~int(3) } + out := make([]byte, total, doc.allocator) + rt_be32(out, 0, 0x00010000) + rt_be16(out, 4, u16(len(tags))) + + pos := 12 + 16 * len(tags) + for tag, i in tags { + rec := 12 + 16 * i + copy(out[rec:rec + 4], transmute([]byte)tag) + rt_be32(out, rec + 8, u32(pos)) + rt_be32(out, rec + 12, u32(len(datas[i]))) + copy(out[pos:], datas[i]) + pos += (len(datas[i]) + 3) & ~int(3) + } + return out +} + +@(private = "file") +rt_square_font :: proc(t: ^testing.T, doc: ^Document, loc := #caller_location) -> ^Font { + d, _ := to_dict(cf_parse(doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Sq /FirstChar 65 /LastChar 65 /Widths [800] >>")) + desc, _ := to_dict(cf_parse(doc, "<< /Flags 4 >>")) + dict_set(desc, "FontFile2", cf_stream(doc, "<< >>", rt_square_ttf(doc))) + dict_set(d, "FontDescriptor", desc) + + f, err := font_load(doc, d) + testing.expectf(t, err == .None, "font_load: %v", err, loc = loc) + return f +} + +// rt_square_text is the square glyph at 24pt with its origin at (4, 26) and +// the page's y flip, so its ink is x 6.4..25.6 and y 9.2..26. +@(private = "file") +rt_square_text :: proc(f: ^Font, place := MATRIX_IDENTITY) -> Text { + txt := text_make() + txt.font = f + base := matrix_mul(scale_matrix(24, -24), translate_matrix(4, 26)) + trm := matrix_mul(font_matrix(f), matrix_mul(place, base)) + append(&txt.items, Text_Item{gid = 1, code = 65, trm = trm, advance = 0.8}) + return txt +} + +// RT_TILT rotates the glyph 20 degrees about its origin, which is what makes +// its outline stop being an axis-aligned rectangle. Applied before the +// placement, so the ink stays inside a 32x32 page. +@(private = "file") +rt_tilt :: proc() -> Matrix { + return matrix_mul(rotate_matrix(20), translate_matrix(-0.08, -0.1)) +} + +// The glyph lands where `trm` puts it, to the pixel, and nowhere else. `trm` +// is glyph space to device space complete, so the CTM argument is not applied +// a second time. +@(test) +test_rdev_fill_text :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + font := rt_square_font(t, doc) + + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + txt := rt_square_text(font) + defer text_destroy(&txt) + dev_fill_text(raster_device_as_device(rd), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 10, 15, BLACK, "inside the glyph's square") + rt_expect(t, &bmp, 24, 25, BLACK, "its bottom-right corner") + rt_expect(t, &bmp, 5, 15, WHITE, "left of it") + rt_expect(t, &bmp, 26, 15, WHITE, "right of it") + rt_expect(t, &bmp, 10, 8, WHITE, "above it") + rt_expect(t, &bmp, 10, 26, WHITE, "below the baseline") + + ink := rt_ink(&bmp) + testing.expectf(t, ink > 300 && ink < 400, "the square covers about 340 pixels, got %d", ink) +} + +// Stroked text outlines the glyph instead of filling it: the middle stays +// white and the edges do not. +@(test) +test_rdev_stroke_text :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + font := rt_square_font(t, doc) + + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + txt := rt_square_text(font) + defer text_destroy(&txt) + st := STROKE_STATE_DEFAULT + st.line_width = 2 + dev_stroke_text(raster_device_as_device(rd), &txt, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + rt_expect(t, &bmp, 16, 17, WHITE, "the inside of a stroked glyph is not filled") + testing.expect(t, rt_px(&bmp, 16, 25) != WHITE, "its bottom edge is inked") + testing.expect(t, rt_px(&bmp, 16, 10) != WHITE, "its top edge is inked") + testing.expect(t, rt_px(&bmp, 7, 17) != WHITE, "its left edge is inked") + + // The pen is 2 units wide in user space; a fill would be four times the ink. + ink := rt_ink(&bmp) + testing.expectf(t, ink > 60 && ink < 200, "a stroked square outline, got %d pixels", ink) +} + +// A glyph used as a clip covers the same pixels as filling it does. The +// square glyph is deliberately tilted first: an upright one really is an +// axis-aligned rectangle and takes the scissor path, which is correct and is +// asserted below, but it would not exercise the coverage plane. +@(test) +test_rdev_clip_text :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + font := rt_square_font(t, doc) + + filled := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&filled) + clipped := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&clipped) + + txt := rt_square_text(font, rt_tilt()) + defer text_destroy(&txt) + + rd1 := rt_dev(t, &filled, doc) + defer raster_device_destroy(rd1) + dev_fill_text(raster_device_as_device(rd1), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + testing.expect(t, rt_ink(&filled) > 250, "the tilted glyph must land on the page") + + rd2 := rt_dev(t, &clipped, doc) + defer raster_device_destroy(rd2) + dev2 := raster_device_as_device(rd2) + dev_clip_text(dev2, &txt, MATRIX_IDENTITY) + testing.expect_value(t, len(rd2.masks), 1) + rt_rect(rd2, 0, 0, 32, 32, rt_paint(0, 0, 0)) + dev_pop_clip(dev2) + testing.expect_value(t, len(rd2.masks), 0) + + diff := 0 + for y in 0 ..< 32 { + for x in 0 ..< 32 { + if (rt_px(&filled, x, y) != WHITE) != (rt_px(&clipped, x, y) != WHITE) { + diff += 1 + } + } + } + testing.expectf(t, diff == 0, "%d pixels differ between filling and clipping the glyph", diff) + + // An upright rectangular glyph is a rectangle, and costs no plane. + upright := rt_square_text(font) + defer text_destroy(&upright) + rd3 := rt_dev(t, &clipped, doc) + defer raster_device_destroy(rd3) + dev_clip_text(raster_device_as_device(rd3), &upright, MATRIX_IDENTITY) + testing.expect_value(t, len(rd3.masks), 0) + testing.expect_value(t, rd3.scissor, Raster_Clip{6, 9, 26, 26}) +} + +// A text object with no font, no items, or a font whose glyphs are content +// streams must not draw and must not crash. +@(test) +test_rdev_text_degenerate :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + empty := text_make() + defer text_destroy(&empty) + dev_fill_text(dev, &empty, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + dev_stroke_text(dev, &empty, nil, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + dev_fill_text(dev, nil, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + dev_ignore_text(dev, &empty, MATRIX_IDENTITY) + testing.expect_value(t, rt_ink(&bmp), 0) + + // A clip from a text object with nothing in it hides everything, which is + // what render mode 7 with no drawable glyph means. + dev_clip_text(dev, &empty, MATRIX_IDENTITY) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 0) + dev_pop_clip(dev) + testing.expect_value(t, len(rd.clips), 0) +} + +// --------------------------------------------------------------------------- +// Colour, blend modes and unusual targets +// --------------------------------------------------------------------------- + +// The blend mode on the Paint reaches `blend_over` unchanged. +@(test) +test_rdev_blend_mode_on_a_fill :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8, {128, 128, 128, 255}) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := rt_paint(0.5, 0.5, 0.5) + p.blend = .Multiply + rt_rect(rd, 0, 0, 4, 8, p) + + want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) + rt_expect(t, &bmp, 1, 1, {want, want, want, 255}, "Multiply against the backdrop") + rt_expect(t, &bmp, 6, 1, {128, 128, 128, 255}, "untouched") +} + +// A /None separation marks nothing at all on the page (8.6.6.4). +@(test) +test_rdev_none_colorspace_paints_nothing :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + cs := Colorspace{kind = .Separation, n = 1, is_none = true} + p := rt_paint(0, 0, 0) + p.cs = &cs + rt_rect(rd, 0, 0, 8, 8, p) + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// A greyscale target has no alpha channel at all. Colour is reduced to +// luminosity by `blend_source_for`, and a group over it still has to work. +@(test) +test_rdev_grey_target :: proc(t: ^testing.T) { + bmp, err := bitmap_make(8, 8, 1) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + bitmap_clear(&bmp, 255) + + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 0.5, nil) + rt_rect(rd, 0, 0, 4, 8, rt_paint(0, 0, 0)) + dev_end_group(dev) + + off, _ := bitmap_pixel_offset(&bmp, 1, 1) + testing.expectf(t, bmp.pixels[off] == 128, "black at half through a group: got %d", + bmp.pixels[off]) + off2, _ := bitmap_pixel_offset(&bmp, 6, 1) + testing.expect_value(t, bmp.pixels[off2], u8(255)) +} + +// A soft mask's /TR is honoured, including outside the mask's own area, where +// the value is the transfer of the backdrop's luminosity. +@(test) +test_rdev_soft_mask_transfer_function :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + // An inverting transfer: 1 - x. + fn, err := function_load(doc, cf_parse(doc, + "<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>")) + testing.expectf(t, err == .None, "function_load: %v", err) + + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 16, 8}, true, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 1, 1)) + sm: Soft_Mask + dev_end_mask(dev, &sm, fn) + + installed := rt_smask(rd) + testing.expect(t, installed != nil, "end_mask must install a mask") + testing.expect_value(t, installed.plane.outside, u8(255)) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + + rt_expect(t, &bmp, 3, 3, WHITE, "the inverted white part of the mask hides") + rt_expect(t, &bmp, 12, 3, RED, "the inverted black part shows") + rt_expect(t, &bmp, 3, 12, RED, "and outside the mask's area the backdrop inverts to 1") +} + +// --------------------------------------------------------------------------- +// Robustness +// --------------------------------------------------------------------------- + +// Nothing may be assumed about the arguments. Every hook has to survive nil +// geometry, nil resources and a device with no Document. +@(test) +test_rdev_nil_arguments :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + st := STROKE_STATE_DEFAULT + paint := rt_paint(0, 0, 0) + + dev_fill_path(dev, nil, false, MATRIX_IDENTITY, paint) + dev_stroke_path(dev, nil, &st, MATRIX_IDENTITY, paint) + dev_fill_image(dev, nil, MATRIX_IDENTITY, paint) + dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, paint) + dev_fill_shade(dev, nil, MATRIX_IDENTITY, paint) + dev_fill_text(dev, nil, MATRIX_IDENTITY, paint) + dev_stroke_text(dev, nil, &st, MATRIX_IDENTITY, paint) + + // Clip hooks must still push, so the interpreter's pops stay balanced. + dev_clip_path(dev, nil, false, MATRIX_IDENTITY) + dev_clip_stroke_path(dev, nil, &st, MATRIX_IDENTITY) + dev_clip_image_mask(dev, nil, MATRIX_IDENTITY) + testing.expect_value(t, len(rd.clips), 3) + dev_pop_clip(dev) + dev_pop_clip(dev) + dev_pop_clip(dev) + testing.expect_value(t, len(rd.clips), 0) + + testing.expect_value(t, rt_ink(&bmp), 0) + testing.expect(t, raster_device_as_device(nil) == nil, "no device from no device") + raster_device_destroy(nil) +} + +// A NaN or an infinity in the CTM must not reach the geometry, and a clip +// built from one must hide rather than reveal. +@(test) +test_rdev_non_finite_matrices :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + nan := math_nan() + inf := max(f64) * 2 + for m in ([3]Matrix{{nan, 0, 0, 1, 0, 0}, {1, 0, 0, 1, inf, 0}, {inf, inf, inf, inf, inf, inf}}) { + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0), m) + } + testing.expect_value(t, rt_ink(&bmp), 0) + + rt_clip_rect(rd, 0, 0, 8, 8, Matrix{nan, 0, 0, 1, 0, 0}) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 0) + dev_pop_clip(dev) + + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 64) +} + +@(private = "file") +math_nan :: proc() -> f64 { + z := 0.0 + return z / z +} + +// More pops than pushes must not underflow, and the clip must still be the +// page afterwards. +@(test) +test_rdev_unbalanced_pops :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + for _ in 0 ..< 5 { dev_pop_clip(dev) } + testing.expect_value(t, len(rd.clips), 0) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 8, 8}) + + dev_end_group(dev) + dev_end_mask(dev, nil, nil) + dev_end_tile(dev) + testing.expect_value(t, len(rd.layers), 0) + + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 64) +} + +// A content stream that opens groups without closing them must be bounded. +// Past the cap the layers are recorded but paint nothing, and every one of +// them still accepts its end_group. +@(test) +test_rdev_layer_depth_is_capped :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + n := RDEV_MAX_LAYERS + 8 + for _ in 0 ..< n { + dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) + } + testing.expect_value(t, len(rd.layers), n) + testing.expect_value(t, rd.suppress, 8) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + for _ in 0 ..< n { + dev_end_group(dev) + } + testing.expect_value(t, len(rd.layers), 0) + testing.expect_value(t, rd.suppress, 0) + + // Everything drawn inside the suppressed layers is dropped; the page is + // still usable afterwards. + testing.expect_value(t, rt_ink(&bmp), 0) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 64) +} + +// A group whose area is empty, or entirely off the page, costs no bitmap and +// still balances. +@(test) +test_rdev_degenerate_group_areas :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + for area in ([3]Rect{{4, 4, 4, 4}, {100, 100, 200, 200}, RECT_EMPTY}) { + dev_begin_group(dev, area, true, false, .Normal, 1, nil) + testing.expect(t, !rd.layers[0].ok, "an empty group needs no bitmap") + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + dev_end_group(dev) + testing.expect_value(t, len(rd.layers), 0) + testing.expect_value(t, rt_ink(&bmp), 0) + } + + // RECT_INFINITE means "the whole clip", which is a real group. + dev_begin_group(dev, RECT_INFINITE, true, false, .Normal, 1, nil) + testing.expect(t, rd.layers[0].ok, "an infinite area is the clip, not nothing") + testing.expect_value(t, rd.layers[0].box, Raster_Clip{0, 0, 8, 8}) + dev_end_group(dev) +} + +// close() unwinds whatever the content stream left open: the group's ink still +// reaches the page and the clip goes back to the whole of it. +@(test) +test_rdev_close_unwinds :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + rt_clip_rect(rd, 0, 0, 4, 8) + dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) + dev_begin_mask(dev, Rect{0, 0, 8, 8}, true, rt_paint(0, 0, 0)) + + dev_close(dev) + + testing.expect_value(t, len(rd.layers), 0) + testing.expect_value(t, len(rd.clips), 0) + testing.expect_value(t, len(rd.masks), 0) + testing.expect(t, rt_smask(rd) == nil, "close drops every soft mask") + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 8, 8}) + testing.expect_value(t, rt_ink(&bmp), 4 * 8) +} + +// The device refuses a bitmap it cannot paint into rather than taking one. +@(test) +test_rdev_make_rejects_bad_targets :: proc(t: ^testing.T) { + rd, err := raster_device_make(nil, Raster_Clip{0, 0, 8, 8}) + testing.expect_value(t, err, Error.Invalid_Argument) + testing.expect(t, rd == nil, "no device from no bitmap") + + empty: Bitmap + rd2, err2 := raster_device_make(&empty, Raster_Clip{0, 0, 8, 8}) + testing.expect_value(t, err2, Error.Invalid_Argument) + testing.expect(t, rd2 == nil, "no device from an unallocated bitmap") +} + +// The base clip is the page, whatever the caller asks for, and a clip pushed +// outside it cannot widen it. +@(test) +test_rdev_base_clip_bounds_everything :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd, err := raster_device_make(&bmp, Raster_Clip{-100, -100, 1000, 4}) + testing.expect_value(t, err, Error.None) + defer raster_device_destroy(rd) + + testing.expect_value(t, rd.base_clip, Raster_Clip{0, 0, 16, 4}) + rt_clip_rect(rd, -50, -50, 500, 500) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 4}) + + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp), 16 * 4) +} + +// Images and shadings need the Document. Without one they are skipped, not +// crashed through. +@(test) +test_rdev_without_a_document :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + sh, _ := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) + + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) // deliberately no Document + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_fill_image(dev, img, rt_place(8), rt_paint(0, 0, 0)) + dev_fill_shade(dev, sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + dev_clip_image_mask(dev, img, rt_place(8)) + dev_pop_clip(dev) + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// --------------------------------------------------------------------------- +// The clip stack's exact behaviour +// --------------------------------------------------------------------------- + +// A rectangular clip becomes a scissor rounded to the NEAREST pixel boundary. +// Rounding outward instead would leak up to a pixel of content past every clip +// edge in every content stream, which is exactly the artefact this pins. +@(test) +test_rdev_rect_clip_rounds_to_the_nearest_pixel :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + // 2.6 .. 9.4 across, 1.4 .. 6.6 down. + rt_clip_rect(rd, 2.6, 1.4, 6.8, 5.2) + testing.expect_value(t, rd.scissor, Raster_Clip{3, 1, 9, 7}) + + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + rt_expect(t, &bmp, 3, 1, BLACK, "the first pixel the clip keeps") + rt_expect(t, &bmp, 2, 1, WHITE, "the pixel the edge at 2.6 mostly misses") + rt_expect(t, &bmp, 8, 6, BLACK, "the last pixel it keeps") + rt_expect(t, &bmp, 9, 6, WHITE, "and the one past the edge at 9.4") + testing.expect_value(t, rt_ink(&bmp), 6 * 6) +} + +// A coverage plane really is coverage: the pixel the hypotenuse cuts in half +// gets half the ink, not all of it and not none. +@(test) +test_rdev_clip_plane_is_anti_aliased :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + tri := path_make() + defer path_destroy(&tri) + path_move_to(&tri, 0, 0) + path_line_to(&tri, 16, 0) + path_line_to(&tri, 0, 16) + path_close(&tri) + dev_clip_path(raster_device_as_device(rd), &tri, false, MATRIX_IDENTITY) + + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + + // Pixel (7,8) is cut corner to corner by x + y = 16: half in, half out. + got := rt_px(&bmp, 7, 8) + testing.expectf(t, got[0] > 100 && got[0] < 155, + "the half-covered clip edge should be mid-grey, got %v", got) + rt_expect(t, &bmp, 6, 8, BLACK, "the pixel fully inside it") + rt_expect(t, &bmp, 8, 8, WHITE, "the pixel fully outside it") +} + +// The group's bitmap starts at the group's own corner, and its contents have +// to be shifted into it. Getting that wrong moves everything inside a group by +// the group's origin, which is the kind of bug that only shows on pages whose +// groups are not anchored at (0,0). +@(test) +test_rdev_group_content_keeps_its_place :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{4, 4, 12, 12}, true, false, .Normal, 1, nil) + rt_rect(rd, 4, 4, 4, 4, rt_paint(0, 0, 1)) + dev_end_group(dev) + + rt_expect(t, &bmp, 4, 4, BLUE, "the group's contents land where the page put them") + rt_expect(t, &bmp, 7, 7, BLUE, "") + rt_expect(t, &bmp, 8, 8, WHITE, "not displaced by the group's own origin") + testing.expect_value(t, rt_ink(&bmp), 16) +} + +// A soft mask's form is not clipped by a coverage plane that happens to be in +// force where the ExtGState installed it, any more than by a scissor. +@(test) +test_rdev_soft_mask_form_ignores_a_clip_plane :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + tri := path_make() + defer path_destroy(&tri) + path_move_to(&tri, 0, 0) + path_line_to(&tri, 16, 0) + path_line_to(&tri, 0, 16) + path_close(&tri) + dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) + testing.expect_value(t, len(rd.masks), 1) + + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + testing.expect_value(t, rd.mask_base, 1) + rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 1, 1)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + testing.expect_value(t, rd.mask_base, 0) + dev_pop_clip(dev) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + testing.expect_value(t, rt_ink(&bmp), 16 * 16) +} + +// A clip built from text whose glyphs cannot be outlined at all -- a font with +// no embedded program, or a Type3 one -- has to let content through. The +// alternative is a blank page every time a font fails to load. +@(test) +test_rdev_clip_text_without_outlines_reveals :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + // Standard-14: metrics, but no program and so no outlines. + fd, _ := to_dict(cf_parse(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")) + font, err := font_load(doc, fd) + testing.expectf(t, err == .None, "font_load: %v", err) + + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + txt := text_make() + defer text_destroy(&txt) + txt.font = font + append(&txt.items, Text_Item{gid = 43, trm = scale_matrix(0.012, -0.012)}) + + dev_clip_text(dev, &txt, MATRIX_IDENTITY) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + dev_pop_clip(dev) + testing.expect_value(t, rt_ink(&bmp), 16 * 16) + + // Text with no items at all still means "clip everything". + empty := text_make() + defer text_destroy(&empty) + empty.font = font + bmp2 := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp2) + rd2 := rt_dev(t, &bmp2, doc) + defer raster_device_destroy(rd2) + dev_clip_text(raster_device_as_device(rd2), &empty, MATRIX_IDENTITY) + rt_rect(rd2, 0, 0, 16, 16, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&bmp2), 0) +} + +// A long random sequence of hook calls -- including non-finite matrices, +// unbalanced pops and nested layers -- must leave the device consistent and +// must not crash, leak or hang. Content streams are the most malformed data in +// a PDF and this is the shape of what they do. +@(test) +test_rdev_random_operations_stay_consistent :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + img := rt_image(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) + stencil := rt_image(t, doc, + "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") + sh, _ := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) + + bmp := rt_bitmap(t, 24, 24) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // A tiny xorshift, so the sequence is the same on every machine and a + // failure is reproducible. + state: u32 = 0x1234_5678 + next :: proc(s: ^u32) -> u32 { + s^ ~= s^ << 13 + s^ ~= s^ >> 17 + s^ ~= s^ << 5 + return s^ + } + coord :: proc(s: ^u32) -> f64 { + switch next(s) % 8 { + case 0: return 1e9 + case 1: return -1e9 + case 2: return f64(next(s) % 32) * 1e30 // finite but vast + } + return f64(next(s) % 40) - 8 + } + + sm: Soft_Mask + open_clips := 0 + open_layers := 0 + st := STROKE_STATE_DEFAULT + + for i in 0 ..< 4000 { + p := rt_paint(f64(next(&state) % 8) / 7, 0.4, 0.9, f64(next(&state) % 4) / 3) + p.blend = Blend_Mode(next(&state) % 16) + if next(&state) & 1 == 0 { p.soft_mask = &sm } + + ctm := MATRIX_IDENTITY + if next(&state) % 4 == 0 { + ctm = Matrix{coord(&state), coord(&state), coord(&state), + coord(&state), coord(&state), coord(&state)} + } + + switch next(&state) % 12 { + case 0: + rt_rect(rd, coord(&state), coord(&state), coord(&state), coord(&state), p, ctm) + case 1: + path := path_make() + defer path_destroy(&path) + path_move_to(&path, coord(&state), coord(&state)) + path_curve_to(&path, coord(&state), coord(&state), coord(&state), + coord(&state), coord(&state), coord(&state)) + st.line_width = f64(next(&state) % 6) + dev_stroke_path(dev, &path, &st, ctm, p) + case 2: + rt_clip_rect(rd, coord(&state), coord(&state), coord(&state), coord(&state), ctm) + open_clips += 1 + case 3: + if open_clips > 0 { + dev_pop_clip(dev) + open_clips -= 1 + } + case 4: + dev_fill_image(dev, img, ctm, p) + case 5: + dev_fill_image_mask(dev, stencil, ctm, p) + case 6: + dev_fill_shade(dev, sh, ctm, p) + case 7: + dev_begin_group(dev, Rect{coord(&state), coord(&state), coord(&state), + coord(&state)}, i & 1 == 0, i & 2 == 0, + p.blend, p.alpha, nil) + open_layers += 1 + case 8: + if open_layers > 0 { + dev_end_group(dev) + open_layers -= 1 + } + case 9: + dev_begin_mask(dev, Rect{coord(&state), coord(&state), coord(&state), + coord(&state)}, i & 1 == 0, p) + open_layers += 1 + case 10: + if open_layers > 0 { + dev_end_mask(dev, nil, nil) + open_layers -= 1 + } + case 11: + dev_clip_image_mask(dev, stencil, ctm) + open_clips += 1 + } + } + + for open_clips > 0 { dev_pop_clip(dev); open_clips -= 1 } + for open_layers > 0 { dev_end_group(dev); open_layers -= 1 } + dev_close(dev) + + testing.expect_value(t, len(rd.clips), 0) + testing.expect_value(t, len(rd.masks), 0) + testing.expect_value(t, len(rd.layers), 0) + testing.expect_value(t, rd.suppress, 0) + testing.expect_value(t, rd.scissor, rd.base_clip) + + // Whatever happened, the device is still usable. + fresh := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&fresh) + rd2 := rt_dev(t, &fresh, doc) + defer raster_device_destroy(rd2) + rt_rect(rd2, 0, 0, 8, 8, rt_paint(0, 0, 0)) + testing.expect_value(t, rt_ink(&fresh), 64) +} + +// A group's bitmap is smaller than the clip that surrounds it, so the scissor +// handed to the rasteriser has to be clamped to the bitmap and not just +// translated into it. Left unclamped, a run can come out wider than the +// per-row scratch and the soft mask silently stops being applied -- which is +// the failure this pins, and it only appears when a masked paint spills past +// a group narrower than the clip. +@(test) +test_rdev_group_narrower_than_the_clip_still_masks :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 4, 16, rt_paint(1, 1, 1)) + sm: Soft_Mask + dev_end_mask(dev, &sm, nil) + + dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) + testing.expect_value(t, rd.layers[0].box, Raster_Clip{0, 0, 8, 8}) + testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + dev_end_group(dev) + + rt_expect(t, &bmp, 1, 1, RED, "inside the group, under the lit part of the mask") + rt_expect(t, &bmp, 5, 1, WHITE, "inside the group, under the dark part of the mask") + rt_expect(t, &bmp, 9, 1, WHITE, "outside the group entirely") + testing.expect_value(t, rt_ink(&bmp), 4 * 8) +} + +// A stroke outline is FILLED, and it must be filled nonzero. Two overlapping +// strokes are one solid shape; even-odd would cancel them and punch a hole +// through exactly the place where a join doubles back or two dashes land on +// the same ink. +@(test) +test_rdev_stroke_outline_is_nonzero :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 12) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 2, 4) + path_line_to(&p, 14, 4) + path_move_to(&p, 2, 6) + path_line_to(&p, 14, 6) + + st := STROKE_STATE_DEFAULT + st.line_width = 6 + dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + // Each pen covers six rows; together they cover y 1..8 solidly, and the + // rows they share are the ones even-odd would erase. + for y in 1 ..< 9 { + rt_expect(t, &bmp, 8, y, BLACK, "the two overlapping strokes are one solid shape") + } + rt_expect(t, &bmp, 8, 0, WHITE, "above them") + rt_expect(t, &bmp, 8, 9, WHITE, "below them") + testing.expect_value(t, rt_ink(&bmp), 12 * 8) +} + +// A clip built from a stroke is the same outline, filled the same way: the +// overlap of two strokes is inside the clip, not punched out of it. +@(test) +test_rdev_clip_stroke_path :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 12) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 2, 4) + path_line_to(&p, 14, 4) + path_move_to(&p, 2, 6) + path_line_to(&p, 14, 6) + + st := STROKE_STATE_DEFAULT + st.line_width = 6 + dev_clip_stroke_path(dev, &p, &st, MATRIX_IDENTITY) + testing.expect_value(t, len(rd.masks), 1) + + rt_rect(rd, 0, 0, 16, 12, rt_paint(0, 0, 0)) + dev_pop_clip(dev) + + for y in 1 ..< 9 { + rt_expect(t, &bmp, 8, y, BLACK, "the whole stroked region clips in") + } + rt_expect(t, &bmp, 8, 0, WHITE, "above it") + rt_expect(t, &bmp, 8, 9, WHITE, "below it") + rt_expect(t, &bmp, 1, 4, WHITE, "left of it") +} + +// Two overlapping glyphs are one shape. Filling a text run even-odd would +// knock a hole through every ligature, every accent that touches its letter +// and every piece of tightly-kerned script. +@(test) +test_rdev_overlapping_glyphs_fill_nonzero :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + font := rt_square_font(t, doc) + + bmp := rt_bitmap(t, 32, 32) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + txt := text_make() + defer text_destroy(&txt) + txt.font = font + fm := font_matrix(font) + trm1 := matrix_mul(fm, matrix_mul(scale_matrix(24, -24), translate_matrix(4, 26))) + trm2 := matrix_mul(fm, matrix_mul(scale_matrix(24, -24), translate_matrix(10, 26))) + append(&txt.items, Text_Item{gid = 1, trm = trm1}) + append(&txt.items, Text_Item{gid = 1, trm = trm2}) + + dev_fill_text(raster_device_as_device(rd), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + + // The first square spans x 6.4..25.6, the second 12.4..31.6. + rt_expect(t, &bmp, 8, 15, BLACK, "only the first glyph") + rt_expect(t, &bmp, 20, 15, BLACK, "where the two overlap") + rt_expect(t, &bmp, 29, 15, BLACK, "only the second glyph") + rt_expect(t, &bmp, 5, 15, WHITE, "before both") +} + +// A group's alpha comes out of a content stream, so it can be anything. Above +// 1 it saturates; a NaN paints nothing rather than reaching the arithmetic. +@(test) +test_rdev_group_alpha_out_of_range :: proc(t: ^testing.T) { + for alpha, i in ([2]f64{2.0, math_nan()}) { + bmp := rt_bitmap(t, 8, 8) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, alpha, nil) + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0)) + dev_end_group(dev) + + if i == 0 { + rt_expect(t, &bmp, 4, 4, RED, "an alpha above 1 saturates at opaque") + } else { + testing.expect_value(t, rt_ink(&bmp), 0) + } + } +} + +// A soft mask whose group could not be rendered leaves nothing behind. If the +// previous mask survived, every later paint would be modulated by a mask that +// the graphics state has already replaced. +@(test) +test_rdev_failed_mask_replaces_the_previous_one :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // A mask whose form paints nothing: black everywhere, so it hides all. + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + sm := rt_mask(1) + dev_end_mask(dev, &sm, nil) + testing.expect(t, rt_smask(rd) != nil, "") + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + testing.expect_value(t, rt_ink(&bmp), 0) + + // Now one that cannot be rendered at all. + other := rt_mask(2) + dev_begin_mask(dev, RECT_EMPTY, true, rt_paint(0, 0, 0)) + testing.expect(t, !rd.layers[0].ok, "an empty mask area needs no bitmap") + dev_end_mask(dev, &other, nil) + testing.expect(t, rdev_smask_lookup(rd, &other) == nil, + "a mask that could not be rendered installs no plane") + + // The paint that was going to use it carries a mask nothing rendered, so + // it must not pick up the one that came before. + p2 := rt_paint(1, 0, 0) + p2.soft_mask = &other + rt_rect(rd, 0, 0, 16, 16, p2) + testing.expect_value(t, rt_ink(&bmp), 16 * 16) +} + +// The same thing again, with a mask that came before and was never painted +// through: a mask that failed to render must not hand its predecessor's plane +// to the next paint that comes along. +@(test) +test_rdev_failed_mask_does_not_donate_an_unused_one :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // Rendered, but nothing has been painted through it yet. + first := rt_mask(1) + rt_half_mask(rd, 0, 8, &first) + testing.expect(t, rdev_smask_lookup(rd, &first) != nil, "the plane is installed") + + dev_begin_mask(dev, RECT_EMPTY, true, rt_paint(0, 0, 0)) + sm := rt_mask(2) + dev_end_mask(dev, &sm, nil) + + p := rt_paint(1, 0, 0) + p.soft_mask = &sm + rt_rect(rd, 0, 0, 16, 16, p) + rt_expect(t, &bmp, 2, 2, RED, "") + rt_expect(t, &bmp, 12, 2, RED, "the other mask's plane is not handed to this paint") + testing.expect_value(t, rt_ink(&bmp), 16 * 16) +} + +// A shading is painted with the paint's alpha and blend mode, not at full +// strength. +@(test) +test_rdev_shade_honours_alpha :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) + testing.expectf(t, err == .None, "shading_load: %v", err) + + opaque := rt_bitmap(t, 16, 8) + defer bitmap_destroy(&opaque) + rd1 := rt_dev(t, &opaque, doc) + defer raster_device_destroy(rd1) + dev_fill_shade(raster_device_as_device(rd1), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0, 1)) + + half := rt_bitmap(t, 16, 8) + defer bitmap_destroy(&half) + rd2 := rt_dev(t, &half, doc) + defer raster_device_destroy(rd2) + dev_fill_shade(raster_device_as_device(rd2), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0, 0.5)) + + a := int(rt_px(&opaque, 0, 4)[0]) + b := int(rt_px(&half, 0, 4)[0]) + testing.expectf(t, a < 24, "the opaque dark end: %d", a) + testing.expectf(t, b > 110 && b < 145, "half of it over white should be mid-grey: %d", b) +} + +// --------------------------------------------------------------------------- +// Shape, not speed +// --------------------------------------------------------------------------- + +// Not a benchmark: a guard against something here being accidentally +// quadratic in the size of the page or of an image. A real page of work -- +// five thousand fills and one photograph minified into half of it -- runs in +// well under half a second, so the budget is a factor of thirty above what it +// costs and only a change of algorithmic shape can breach it. +@(test) +test_rdev_page_sized_workload :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp, err := bitmap_make(1275, 1650, 4) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, WHITE) + bmp.premultiplied = true + + rd, derr := raster_device_make(&bmp, Raster_Clip{0, 0, bmp.width, bmp.height}, + context.allocator, doc) + testing.expect_value(t, derr, Error.None) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + start := time.now() + + p := path_make() + defer path_destroy(&p) + for i in 0 ..< 5000 { + path_clear(&p) + path_rect(&p, f64((i * 37) % 1200), f64((i * 53) % 1600), 40, 12) + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0.1, 0.2, 0.3)) + } + + px := make([]byte, 3 * 1024 * 1024, doc.allocator) + for i in 0 ..< len(px) { px[i] = u8(i * 7) } + img := rt_image(t, doc, + "<< /Width 1024 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", + string(px)) + dev_fill_image(dev, img, Matrix{600, 0, 0, -800, 100, 900}, rt_paint(0, 0, 0)) + + dev_close(dev) + elapsed := time.duration_seconds(time.since(start)) + testing.expectf(t, elapsed < 15, "a page of work took %.2fs", elapsed) + testing.expect(t, rt_ink(&bmp) > 100_000, "and it has to have actually drawn") +} + +// --------------------------------------------------------------------------- +// The soft-mask cache +// --------------------------------------------------------------------------- + +// rt_half_mask renders a luminosity mask lit over `x0 ..< x1` and black +// everywhere else, belonging to `owner` -- which `end_mask` names, exactly as +// the interpreter does. +@(private = "file") +rt_half_mask :: proc(rd: ^Raster_Device, x0, x1: f64, owner: ^Soft_Mask) { + dev := raster_device_as_device(rd) + dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) + rt_rect(rd, x0, 0, x1 - x0, 16, rt_paint(1, 1, 1)) + dev_end_mask(dev, owner, nil) +} + +// An ExtGState renders its soft mask once, when it installs it, so a `Q` that +// restores an outer mask never draws it again -- the device has to still have +// it. Holding only the newest would modulate everything after the `Q` with the +// inner mask instead, which is the failure this pins. +@(test) +test_rdev_soft_mask_cache_survives_a_restore :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + outer, inner := rt_mask(1), rt_mask(2) + po := rt_paint(1, 0, 0); po.soft_mask = &outer + pi := rt_paint(0, 1, 0); pi.soft_mask = &inner + + // The outer mask lights the left half. + rt_half_mask(rd, 0, 8, &outer) + rt_rect(rd, 0, 0, 16, 4, po) + + // Inside a `q`, an inner mask lights the right half. + rt_half_mask(rd, 8, 16, &inner) + rt_rect(rd, 0, 4, 16, 4, pi) + + // After the `Q` the outer mask is in force again, and nothing re-rendered it. + rt_rect(rd, 0, 8, 16, 4, po) + rt_rect(rd, 0, 12, 16, 4, pi) + + rt_expect(t, &bmp, 2, 1, RED, "the outer mask lights the left half") + rt_expect(t, &bmp, 12, 1, WHITE, "") + rt_expect(t, &bmp, 2, 5, WHITE, "the inner mask lights the right half") + rt_expect(t, &bmp, 12, 5, GREEN, "") + rt_expect(t, &bmp, 2, 9, RED, "and the outer one is still there after the restore") + rt_expect(t, &bmp, 12, 9, WHITE, "") + rt_expect(t, &bmp, 2, 13, WHITE, "as is the inner one") + rt_expect(t, &bmp, 12, 13, GREEN, "") +} + +// The cache is bounded. Past its size the least recently used mask goes, and +// a paint that wanted it is drawn unmasked rather than through the wrong one. +@(test) +test_rdev_soft_mask_cache_is_bounded :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + owners: [RDEV_MAX_SOFT_MASKS + 1]Soft_Mask + for i in 0 ..< len(owners) { + owners[i] = rt_mask(i + 1) + p := rt_paint(0, 0, 0) + p.soft_mask = &owners[i] + // Every one of them lights only the left half. + rt_half_mask(rd, 0, 8, &owners[i]) + rt_rect(rd, 0, 0, 1, 1, p) // use it + } + + live := 0 + for i in 0 ..< RDEV_MAX_SOFT_MASKS { + if rd.smasks[i].live { live += 1 } + } + testing.expect_value(t, live, RDEV_MAX_SOFT_MASKS) + + // The first mask has been evicted, so its paint is unmasked and covers the + // whole row; the last is still there and covers only the left half. + first := rt_paint(1, 0, 0) + first.soft_mask = &owners[0] + rt_rect(rd, 0, 4, 16, 2, first) + rt_expect(t, &bmp, 2, 5, RED, "an evicted mask leaves the paint unmasked") + rt_expect(t, &bmp, 12, 5, RED, "") + + last := rt_paint(0, 0, 1) + last.soft_mask = &owners[len(owners) - 1] + rt_rect(rd, 0, 8, 16, 2, last) + rt_expect(t, &bmp, 2, 9, BLUE, "the newest is still cached") + rt_expect(t, &bmp, 12, 9, WHITE, "") +} + +// A mask that was rendered and never painted through is dead weight, so it is +// the first thing evicted -- ahead of any mask that has been used at least +// once. An ExtGState that installs a mask and then draws nothing is common +// enough (a `q /GS gs Q` around content that turned out to be empty) that +// letting those push out the masks still in force would empty the cache. +@(test) +test_rdev_soft_mask_cache_evicts_unused_planes_first :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + + owners: [RDEV_MAX_SOFT_MASKS]Soft_Mask + for i in 0 ..< len(owners) { + owners[i] = rt_mask(i + 1) + p := rt_paint(0, 0, 0) + p.soft_mask = &owners[i] + rt_half_mask(rd, 0, 8, &owners[i]) + rt_rect(rd, 0, 0, 1, 1, p) // use it + } + + // Two more masks, of the opposite shape, that nothing ever paints through. + unused := [2]Soft_Mask{rt_mask(101), rt_mask(102)} + rt_half_mask(rd, 8, 16, &unused[0]) + rt_half_mask(rd, 8, 16, &unused[1]) + + // The second-oldest used mask must still be there: only the oldest was + // evicted, and the second unused plane reused the first one's slot. + p := rt_paint(1, 0, 0) + p.soft_mask = &owners[1] + rt_rect(rd, 0, 4, 16, 4, p) + rt_expect(t, &bmp, 2, 5, RED, "the mask it was rendered with lights the left half") + rt_expect(t, &bmp, 12, 5, WHITE, "and not the right, which is what an unused plane would have done") +} + +// --------------------------------------------------------------------------- +// Transparency groups: what the outer compositing parameters apply to +// --------------------------------------------------------------------------- + +// rt_group_res builds the resources these tests share: an ExtGState whose +// /SMask is a 50% luminosity group, a plain half-alpha ExtGState, and the same +// drawing as a form with and without /Group. +@(private = "file") +rt_group_res :: proc(doc: ^Document) -> ^Dict { + res := new_dict(doc.allocator) + + smd := new_dict(doc.allocator) + dict_set(smd, "S", Name("Luminosity")) + dict_set(smd, "G", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + + "/Group << /S /Transparency /CS /DeviceGray >> >>", + transmute([]byte)string("0.5 g 0 0 20 20 re f"))) + gm := new_dict(doc.allocator) + dict_set(gm, "Type", Name("ExtGState")) + dict_set(gm, "SMask", smd) + + gs := new_dict(doc.allocator) + dict_set(gs, "GM", gm) + dict_set(gs, "HA", cf_parse(doc, "<< /Type /ExtGState /ca 0.5 >>")) + dict_set(res, "ExtGState", gs) + + xo := new_dict(doc.allocator) + dict_set(xo, "Grp", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + + "/Group << /S /Transparency /CS /DeviceGray >> >>", + transmute([]byte)string("0 0 20 20 re f"))) + dict_set(xo, "Plain", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] >>", + transmute([]byte)string("0 0 20 20 re f"))) + // The left half as a group, the right half as a plain form, so a paint + // through the mask on one side cannot be confused with the other. + dict_set(xo, "GrpL", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 10 20] " + + "/Group << /S /Transparency /CS /DeviceGray >> >>", + transmute([]byte)string("0 0 10 20 re f"))) + dict_set(xo, "Right", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [10 0 20 20] >>", + transmute([]byte)string("10 0 10 20 re f"))) + dict_set(xo, "Zero", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] >>", + transmute([]byte)string("0 0 20 20 re f"))) + // A transparency group whose whole content is another transparency group. + // A form with no /Resources of its own inherits the invoking stream's, so + // /Grp resolves here (8.10.1). + dict_set(xo, "Nest", cf_stream(doc, + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + + "/Group << /S /Transparency /CS /DeviceGray >> >>", + transmute([]byte)string("/Grp Do"))) + dict_set(res, "XObject", xo) + return res +} + +@(private = "file") +rt_run :: proc(t: ^testing.T, doc: ^Document, bmp: ^Bitmap, res: ^Dict, src: string, + loc := #caller_location) { + rd := rt_dev(t, bmp, doc, loc) + defer raster_device_destroy(rd) + err := run_content(doc, raster_device_as_device(rd), transmute([]byte)src, res, + MATRIX_IDENTITY) + testing.expectf(t, err == .None, "run_content: %v", err, loc = loc) +} + +// 11.6.6: the outer alpha, blend mode AND soft mask apply to a transparency +// group's RESULT. `content_do_form` clears all three for the group's contents, +// so a mask that `begin_group` did not carry was applied nowhere at all -- the +// gradient fade every drop shadow is built from rendered as a solid shape. +// Alpha was already carried, which is what isolates the missing parameter. +@(test) +test_rdev_soft_mask_applies_to_a_group_result :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + res := rt_group_res(doc) + + Case :: struct { src: string, want: [4]u8, what: string } + cases := []Case{ + {"/GM gs 0 0 20 20 re f", {127, 127, 127, 255}, "a bare fill under the mask"}, + {"/GM gs /Plain Do", {127, 127, 127, 255}, "a form with no /Group"}, + {"/GM gs /Grp Do", {127, 127, 127, 255}, "a transparency group"}, + {"0 0 20 20 re f", {0, 0, 0, 255}, "no mask at all"}, + {"/HA gs /Grp Do", {128, 128, 128, 255}, "a group's own alpha"}, + } + for c in cases { + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, c.src) + rt_expect_near(t, &bmp, 10, 10, c.want, 1, c.what) + } +} + +// A paint carrying no soft mask must not cost a later paint the plane it +// needs. Every paint inside a transparency group is exactly such a paint -- +// 11.6.6 clears the mask for the contents -- so one group used to disable an +// ExtGState's mask for the whole rest of the page. +@(test) +test_rdev_a_group_does_not_consume_the_soft_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + res := rt_group_res(doc) + + alone := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&alone) + rt_run(t, doc, &alone, res, "q /GM gs /Right Do Q") + rt_expect_near(t, &alone, 15, 10, {127, 127, 127, 255}, 1, "the plain form alone") + + after := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&after) + rt_run(t, doc, &after, res, "q /GM gs /GrpL Do /Right Do Q") + rt_expect_near(t, &after, 5, 10, {127, 127, 127, 255}, 1, "the group is masked") + rt_expect_near(t, &after, 15, 10, {127, 127, 127, 255}, 1, + "and the plain form after it still is") +} + +// The device half of the same story, with no interpreter in it: the mask +// arrives on `begin_group` and `rdev_layer_finish` is the only thing that can +// apply it. It used to composite the finished layer with +// `blend_bitmap(..., l.alpha, l.blend, nil)` -- alpha and blend mode honoured, +// the mask hard-nil -- so a group under a 50% mask came back fully opaque. +@(test) +test_rdev_group_result_is_composited_through_its_mask :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 100, 100) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // A luminosity mask over the whole page whose form paints mid-grey: half + // of whatever it modulates gets through. + dev_begin_mask(dev, Rect{0, 0, 100, 100}, true, rt_paint(0, 0, 0)) + rt_rect(rd, 0, 0, 100, 100, rt_paint(0.5, 0.5, 0.5)) + sm := rt_mask(31) + dev_end_mask(dev, &sm, nil) + + // 11.6.6: the group's CONTENTS carry no mask -- that is the interpreter's + // job and it is what makes this the only channel the mask has left. + dev_begin_group(dev, Rect{0, 0, 50, 100}, true, false, .Normal, 1, &sm) + rt_rect(rd, 0, 0, 50, 100, rt_paint(0, 0, 0)) + dev_end_group(dev) + + rt_expect_near(t, &bmp, 25, 50, {127, 127, 127, 255}, 1, "the group's result through the mask") + rt_expect(t, &bmp, 75, 50, WHITE, "outside the group") +} + +// The plane a group's RESULT will be composited through has to survive the +// cache until `end_group` -- which can be a whole content stream later. Until +// then nothing has painted through it, and "a slot nothing ever painted +// through goes first" is exactly the rule that would give it away: an +// ExtGState mask over a group, with more masks installed inside the group, is +// ordinary Illustrator output. +@(test) +test_rdev_group_mask_survives_cache_pressure :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 16, 16) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // Fill every other slot with a mask that has been painted through, so the + // next install has to evict one of them or the group's. + others: [RDEV_MAX_SOFT_MASKS - 1]Soft_Mask + for i in 0 ..< len(others) { + others[i] = rt_mask(i + 1) + rt_half_mask(rd, 0, 16, &others[i]) + p := rt_paint(1, 1, 1) // white on white: used, but invisible + p.soft_mask = &others[i] + rt_rect(rd, 0, 0, 1, 1, p) + } + + group_mask := rt_mask(9) + rt_half_mask(rd, 0, 8, &group_mask) + + dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Normal, 1, &group_mask) + rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) + // A mask installed INSIDE the group: the interpreter does this whenever + // the group's own contents carry an /SMask of their own. + inner := rt_mask(10) + rt_half_mask(rd, 8, 16, &inner) + dev_end_group(dev) + + rt_expect(t, &bmp, 4, 8, BLACK, "the lit half of the group's mask") + rt_expect(t, &bmp, 12, 8, WHITE, "the half its mask holds back") +} + +// A group nested inside a masked group. The mask applies to the OUTER group's +// result exactly once: the interpreter clears it for the outer group's +// contents, so the inner group is begun with no mask of its own. Applying it +// again there would square it -- a 50% mask would show as 25%. +@(test) +test_rdev_soft_mask_over_a_nested_group :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + res := rt_group_res(doc) + + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, "/GM gs /Nest Do") + // 0 is the mask dropped, 191 is the mask applied twice. + rt_expect_near(t, &bmp, 10, 10, {127, 127, 127, 255}, 1, + "a group inside a masked group, masked once") +} + +// Defect 5's shape end to end: a masked group, then a paint with no mask at +// all, then another masked paint. The plane used to be claimed by the first +// paint to arrive after `end_mask`, so the group's interior fill -- which +// carries no mask by design -- threw the claim away and everything after it +// was painted unmasked. +@(test) +test_rdev_soft_mask_survives_an_unmasked_paint_between_two_masked_ones :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + res := rt_group_res(doc) + + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, + "q /GM gs /GrpL Do Q " + // masked transparency group, x < 10 + "10 0 4 20 re f " + // no mask in force at all + "q /GM gs 14 0 6 20 re f Q") // masked again + rt_expect_near(t, &bmp, 5, 10, {127, 127, 127, 255}, 1, "the masked group") + rt_expect(t, &bmp, 12, 10, BLACK, "the unmasked paint between them") + rt_expect_near(t, &bmp, 17, 10, {127, 127, 127, 255}, 1, + "the masked paint after both of them") +} + +// The same defect one level up: a form whose /BBox has no area must draw +// nothing, because the box is a clip on its contents (8.10.2). +@(test) +test_rdev_form_with_a_zero_area_bbox_draws_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + res := rt_group_res(doc) + + bmp := rt_bitmap(t, 20, 20) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, "/Zero Do") + testing.expect_value(t, rt_ink(&bmp), 0) +} + +// A NON-ISOLATED group is handed a copy of its backdrop, so its finished +// bitmap already IS "contents over backdrop". Compositing that OVER the +// backdrop again blends the backdrop with itself: `C over C` under Normal +// takes alpha 0.5 to 0.75. That is wrong wherever the backdrop is not opaque, +// which is every nested group and every page rendered with a transparent +// background -- and it damages the whole area the group covers, whether or not +// the group draws anything. +@(test) +test_rdev_non_isolated_group_over_a_transparent_backdrop :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) + before := rt_px(&bmp, 4, 4) + rt_expect(t, &bmp, 4, 4, {128, 0, 0, 128}, "half a red, premultiplied") + + dev_begin_group(dev, Rect{0, 0, 8, 8}, false, false, .Normal, 1, nil) + dev_end_group(dev) + testing.expectf(t, rt_px(&bmp, 4, 4) == before, + "an empty non-isolated group must leave the backdrop alone: got %v, was %v", + rt_px(&bmp, 4, 4), before) +} + +// And with contents: wrapping a paint in a non-isolated group must give +// exactly what painting it directly gives, once the group's own alpha is 1 and +// its blend mode Normal. +@(test) +test_rdev_non_isolated_group_matches_painting_directly :: proc(t: ^testing.T) { + direct := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) + defer bitmap_destroy(&direct) + { + rd := rt_dev(t, &direct) + defer raster_device_destroy(rd) + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 1, 0, 0.5)) + } + + grouped := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) + defer bitmap_destroy(&grouped) + { + rd := rt_dev(t, &grouped) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) + dev_begin_group(dev, Rect{0, 0, 8, 8}, false, false, .Normal, 1, nil) + rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 1, 0, 0.5)) + dev_end_group(dev) + } + + for y in 0 ..< 8 { + for x in 0 ..< 8 { + a := rt_px(&direct, x, y) + b := rt_px(&grouped, x, y) + if a != b { + testing.expectf(t, false, "(%d,%d): grouped %v, direct %v", x, y, b, a) + return + } + } + } +} + +// --------------------------------------------------------------------------- +// Minification +// --------------------------------------------------------------------------- + +// A one-pixel checkerboard is exactly half black, so however far it is +// minified the answer is mid-grey. Capping the taps at RDEV_MAX_TAPS without +// reducing the source first turned the "box filter" into a strided point +// sample: at 32x the stride is 2, every tap landed on the same parity, and the +// whole image came out solid. Bilevel one-pixel detail -- fax scans, halftone +// screens, hairline rules -- is exactly the content that hits this, and +// thumbnail-scale rendering is exactly where it happens. +@(test) +test_rdev_heavy_minification_does_not_alias :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + W :: 320 + samples := make([]byte, W * W) + defer delete(samples) + for y in 0 ..< W { + for x in 0 ..< W { + samples[y * W + x] = (x + y) % 2 == 0 ? 255 : 0 + } + } + + res := new_dict(doc.allocator) + xo := new_dict(doc.allocator) + dict_set(xo, "Im", cf_stream(doc, + "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", samples)) + dict_set(res, "XObject", xo) + + // 10x10 device pixels for a 320x320 source: 32x minification per axis. + for size in ([3]int{10, 5, 2}) { + bmp := rt_bitmap(t, size, size) + defer bitmap_destroy(&bmp) + src := fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size) + rt_run(t, doc, &bmp, res, src) + for y in 0 ..< size { + for x in 0 ..< size { + got := rt_px(&bmp, x, y) + if abs(int(got[0]) - 128) > 24 { + testing.expectf(t, false, + "%dx minification at (%d,%d): got %v, want ~128 grey", + W / size, x, y, got) + return + } + } + } + } +} + +// The same source across the whole range of minification, not just the one +// factor that broke. A box filter is a box filter only while its taps still +// cover the footprint, and there are two ways to lose that: too few taps for +// the footprint (which is what capping them without reducing the source did) +// and a reduction that drops or double-counts source pixels at the edges. Both +// show as the mean of the output drifting away from the mean of the source, so +// that is what this asserts -- at every factor from 1x to 64x, and per pixel +// from 2x up, where one device pixel covers a whole number of cells. +@(test) +test_rdev_minification_sweep_holds_the_source_mean :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + W :: 320 + samples := make([]byte, W * W) + defer delete(samples) + for y in 0 ..< W { + for x in 0 ..< W { + samples[y * W + x] = (x + y) % 2 == 0 ? 255 : 0 + } + } + + res := new_dict(doc.allocator) + xo := new_dict(doc.allocator) + dict_set(xo, "Im", cf_stream(doc, + "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", samples)) + dict_set(res, "XObject", xo) + + // Every divisor of 320 in the range, so a device pixel covers a whole + // number of source pixels and the exact answer is the source mean. + for size in ([]int{320, 160, 80, 64, 40, 32, 20, 16, 10, 8, 5}) { + bmp := rt_bitmap(t, size, size) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size)) + + factor := W / size + sum := 0 + for y in 0 ..< size { + for x in 0 ..< size { + v := int(rt_px(&bmp, x, y)[0]) + sum += v + // At 1x the image IS the checkerboard and no filtering is + // wanted; past it every pixel is an average of both colours. + if factor > 1 && abs(v - 128) > 24 { + testing.expectf(t, false, + "%dx minification at (%d,%d): got %d, want ~128 grey", factor, x, y, v) + return + } + } + } + mean := f64(sum) / f64(size * size) + testing.expectf(t, abs(mean - 127.5) <= 4, + "%dx minification: mean %.1f, want ~127.5", factor, mean) + } +} + +// A stencil takes a different span procedure to a colour image and the same +// sampler, and a 1-bit stencil is the content the aliasing actually shows up +// on: a fax scan or a halftone screen minified to thumbnail size. Half the +// bits are set, so half the ink lands, whatever the factor. +@(test) +test_rdev_minified_stencil_is_averaged :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + W :: 320 + STRIDE :: W / 8 + bits := make([]byte, STRIDE * W) + defer delete(bits) + for y in 0 ..< W { + // 0xAA and 0x55 are the two phases of a one-pixel checkerboard. + fill := u8(y % 2 == 0 ? 0xAA : 0x55) + for i in 0 ..< STRIDE { + bits[y * STRIDE + i] = fill + } + } + + res := new_dict(doc.allocator) + xo := new_dict(doc.allocator) + dict_set(xo, "Im", cf_stream(doc, + "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + + "/ImageMask true /BitsPerComponent 1 >>", bits)) + dict_set(res, "XObject", xo) + + for size in ([]int{10, 5}) { + bmp := rt_bitmap(t, size, size) + defer bitmap_destroy(&bmp) + rt_run(t, doc, &bmp, res, fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size)) + for y in 0 ..< size { + for x in 0 ..< size { + v := int(rt_px(&bmp, x, y)[0]) + if abs(v - 128) > 24 { + testing.expectf(t, false, + "%dx stencil minification at (%d,%d): got %d, want ~128 grey", + W / size, x, y, v) + return + } + } + } + } +} + +// --------------------------------------------------------------------------- +// The clip stack is bounded +// --------------------------------------------------------------------------- + +// `W n` needs no `q`, and nothing pops until the enclosing scope ends, so a +// page whose content is one non-rectangular full-page clip repeated pushes a +// stack entry and a coverage plane the size of the scissor every time. At +// 612x792 that is half a megabyte each and the token budget buys hundreds of +// thousands of them; every other stack in this device is capped and this one +// was not. +@(test) +test_rdev_clip_stack_is_bounded :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 200, 200) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + // Five points, so it is not one of the rectangles the scissor fast path + // recognises, and its bounds are the whole page, so the scissor never + // narrows and every plane is full-sized. + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 200, 0) + path_line_to(&p, 200, 200) + path_line_to(&p, 0, 200) + path_line_to(&p, 1, 1) + + N :: RDEV_MAX_CLIPS + 1000 + start := time.now() + for _ in 0 ..< N { + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + } + elapsed := time.duration_seconds(time.since(start)) + + testing.expectf(t, len(rd.clips) <= RDEV_MAX_CLIPS, "%d clip entries", len(rd.clips)) + testing.expectf(t, len(rd.masks) <= RDEV_MAX_CLIP_MASKS, "%d coverage planes", len(rd.masks)) + testing.expectf(t, rd.mask_bytes <= RDEV_MAX_CLIP_MASK_BYTES, + "%d bytes of coverage plane", rd.mask_bytes) + // Not a performance bound: uncapped, this same loop allocates 200 MB of + // planes and rasterises the path into every one of them. + testing.expectf(t, elapsed < 10, "%d clips took %.2fs", N, elapsed) + + // A refused push must not be popped by somebody else's `Q`: the pops are + // matched here, so the scissor has to come back to the whole page and the + // planes have to be gone. + for _ in 0 ..< N { + dev_pop_clip(dev) + } + testing.expect_value(t, len(rd.clips), 0) + testing.expect_value(t, len(rd.masks), 0) + testing.expect_value(t, rd.mask_bytes, 0) + testing.expect_value(t, rdev_scissor(rd), Raster_Clip{0, 0, 200, 200}) + + // And it still draws: a fill after all that lands on the page. + rt_rect(rd, 10, 10, 20, 20, rt_paint(1, 0, 0)) + rt_expect(t, &bmp, 15, 15, RED, "the device is still usable") +} + +// --------------------------------------------------------------------------- +// Geometry the clip cannot reach costs nothing +// --------------------------------------------------------------------------- + +// One `c` with control points ten trillion units away flattens to 65 537 +// points, and sixteen of them exhaust MAX_FLATTEN_POINTS: a fill that costs +// 250 bytes of content materialises a megapoint of polyline, and the token +// budget pays for thirty thousand such fills. The device hands the clip to +// the flattener so that none of it is built. +@(test) +test_rdev_unreachable_curves_cost_nothing :: proc(t: ^testing.T) { + bmp := rt_bitmap(t, 612, 792) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp) + defer raster_device_destroy(rd) + dev := raster_device_as_device(rd) + + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 100, 100) + for _ in 0 ..< 16 { + path_curve_to(&p, -4.98e13, 5.61e13, 7.23e13, 9.65e13, -7.82e13, 9.74e13) + } + path_line_to(&p, 300, 300) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + + // The load-independent half: one fill, and what it had to build for it. + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 1)) + testing.expectf(t, len(rd.fp.points) <= 128, + "one fill flattened %d points", len(rd.fp.points)) + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + testing.expectf(t, len(rd.fp.points) <= 128, + "one clip flattened %d points", len(rd.fp.points)) + dev_pop_clip(dev) + + start := time.now() + for _ in 0 ..< 200 { + dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 1)) + dev_clip_path(dev, &p, false, MATRIX_IDENTITY) + dev_pop_clip(dev) + dev_stroke_path(dev, &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 1)) + } + elapsed := time.duration_seconds(time.since(start)) + // Not a performance bound: without the clip in hand this is a megapoint of + // polyline per operator and the same loop takes 79 seconds unloaded, so + // the budget is fifteen times what the work costs and still an order of + // magnitude below the failure it guards. + testing.expectf(t, elapsed < 30, "800 operators on an unreachable path took %.2fs", elapsed) +} + +// A path too big to flatten whole is truncated, and truncation is damage: the +// page is drawn with part of a shape missing. A caller reading `doc.repairs` +// has to be able to tell that from a page this library drew faithfully. +@(test) +test_rdev_truncated_flattening_is_recorded :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := rt_bitmap(t, 64, 64) + defer bitmap_destroy(&bmp) + rd := rt_dev(t, &bmp, doc) + defer raster_device_destroy(rd) + + // Every segment lands on the page, so nothing here is culled and the cap + // is the only thing that stops it. + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + for i in 0 ..< MAX_FLATTEN_POINTS + 16 { + path_line_to(&p, f64(i % 61) + 1, f64((i * 7) % 61) + 1) + } + + before := len(doc.repairs) + dev_fill_path(raster_device_as_device(rd), &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 0)) + testing.expectf(t, len(rd.fp.points) == MAX_FLATTEN_POINTS, + "the flattening stops at the cap: %d points", len(rd.fp.points)) + testing.expect(t, len(doc.repairs) > before, "and says so") + testing.expect(t, rt_ink(&bmp) > 0, "what did fit is still drawn") +} diff --git a/device_test.odin b/device_test.odin new file mode 100644 index 0000000..11daa49 --- /dev/null +++ b/device_test.odin @@ -0,0 +1,90 @@ +package pdf + +import "core:testing" + +// Every Device hook is optional: the text device leaves all the path and image +// hooks nil, the display-list device leaves the mask hooks nil. The interpreter +// goes through the dev_* wrappers precisely so that costs one predictable +// branch instead of a crash, so an all-nil device is the case to pin. +@(test) +test_device_wrappers_tolerate_nil_hooks :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 10, 10) + + txt := text_make() + defer text_destroy(&txt) + + stroke := STROKE_STATE_DEFAULT + paint: Paint + + call_everything :: proc(dev: ^Device, p: ^Path, txt: ^Text, stroke: ^Stroke_State, paint: Paint) { + dev_fill_path(dev, p, false, MATRIX_IDENTITY, paint) + dev_stroke_path(dev, p, stroke, MATRIX_IDENTITY, paint) + dev_clip_path(dev, p, true, MATRIX_IDENTITY) + dev_clip_stroke_path(dev, p, stroke, MATRIX_IDENTITY) + dev_clip_text(dev, txt, MATRIX_IDENTITY) + dev_clip_image_mask(dev, nil, MATRIX_IDENTITY) + dev_pop_clip(dev) + dev_fill_text(dev, txt, MATRIX_IDENTITY, paint) + dev_stroke_text(dev, txt, stroke, MATRIX_IDENTITY, paint) + dev_ignore_text(dev, txt, MATRIX_IDENTITY) + dev_fill_image(dev, nil, MATRIX_IDENTITY, paint) + dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, paint) + dev_fill_shade(dev, nil, MATRIX_IDENTITY, paint) + dev_begin_mask(dev, RECT_EMPTY, true, paint) + dev_end_mask(dev, nil, nil) + dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) + dev_end_group(dev) + _ = dev_begin_tile(dev, RECT_EMPTY, RECT_EMPTY, 1, 1, MATRIX_IDENTITY) + dev_end_tile(dev) + dev_begin_layer(dev, "layer") + dev_end_layer(dev) + dev_close(dev) + } + + // A device with no hooks at all. + empty: Device + call_everything(&empty, &p, &txt, &stroke, paint) + + // And no device at all, which is what an interpreter run with a nil sink + // must survive. + call_everything(nil, &p, &txt, &stroke, paint) + + // begin_tile must report "run the content once" when unimplemented, not a + // negative or unbounded count. + testing.expect_value(t, dev_begin_tile(&empty, RECT_EMPTY, RECT_EMPTY, 1, 1, MATRIX_IDENTITY), 0) + testing.expect_value(t, dev_begin_tile(nil, RECT_EMPTY, RECT_EMPTY, 1, 1, MATRIX_IDENTITY), 0) +} + +// The hints let a device tell the interpreter what it can skip -- decoding a +// 4000x4000 JPEG to extract text off the page it sits on is pure waste. +@(test) +test_device_hints :: proc(t: ^testing.T) { + dev: Device + testing.expect(t, dev_wants_images(&dev), "a device with no hints wants everything") + testing.expect(t, dev_wants_shades(&dev), "") + + dev.hints = {.Ignore_Image} + testing.expect(t, !dev_wants_images(&dev), "") + testing.expect(t, dev_wants_shades(&dev), "one hint must not imply another") + + dev.hints = {.Ignore_Image, .Ignore_Shade} + testing.expect(t, !dev_wants_images(&dev), "") + testing.expect(t, !dev_wants_shades(&dev), "") + + // A nil device wants nothing, so the interpreter skips the work entirely. + testing.expect(t, !dev_wants_images(nil), "") + testing.expect(t, !dev_wants_shades(nil), "") +} + +@(test) +test_text_make_and_destroy :: proc(t: ^testing.T) { + txt := text_make() + append(&txt.items, Text_Item{gid = 5, unicode = 'A', advance = 500}) + testing.expect_value(t, len(txt.items), 1) + text_destroy(&txt) + testing.expect_value(t, len(txt.items), 0) + text_destroy(&txt) // idempotent + text_destroy(nil) +} diff --git a/device_text.odin b/device_text.odin new file mode 100644 index 0000000..6c96ecf --- /dev/null +++ b/device_text.odin @@ -0,0 +1,990 @@ +package pdf + +import "core:math" +import "core:mem" +import "core:slice" +import "core:strings" +import "core:unicode/utf8" + +// The text-extraction Device (ISO 32000-2, 9.4.4 for the geometry it undoes). +// +// A PDF has no notion of a word, a line or a paragraph. It has glyphs at +// coordinates, emitted in whatever order suited the producer -- every column +// of a table first, or a whole page of body text with each word positioned +// individually and not one space character in the file. Reconstructing reading +// order from that is the entire job of this file; drawing is somebody else's. +// +// WHAT ARRIVES +// +// One `Text` per text-showing operator, each item carrying `trm`, the complete +// glyph-space-to-device-space matrix, and `unicode`, which font.odin has +// already resolved through /ToUnicode, the glyph name and the CMap. Three +// hooks feed the same collector: `fill_text`, `stroke_text` (render mode 1 is +// real text) and `ignore_text` -- mode 3 is invisible, and invisible is +// exactly how a scanned page stores its OCR layer, which is the text a user +// most wants. `clip_text` feeds it too, so that mode 7 is not lost; a run that +// arrives twice because its render mode both fills and clips is recognised and +// counted once. +// +// One glyph can stand for several characters -- "ffi" is one glyph and three +// letters -- and `unicode`, being one rune, reports the first. The producer +// said the rest in /ToUnicode, so this file asks for it: without that, +// "Specification" extracts as "Specifcation", which is what the corpus does. +// +// HOW A GLYPH IS PLACED +// +// `trm` maps glyph space, which is the font program's own unit system, to +// device space. Undoing the font matrix, +// +// T = font_matrix(f)^-1 * trm +// +// gives text space to device space, and that is the frame everything here +// works in: T's translation is the pen, |(T.a, T.b)| is one em along the +// writing direction (with the horizontal scale folded in, which is what +// advances are measured against) and |(T.c, T.d)| is one em vertically, which +// is the font size a caller wants reported. Deriving both from the matrix is +// what makes rotated, skewed and mirrored text come out in the right order +// without a special case: "left to right" is along T's x axis, never along +// device x. +// +// A glyph's advance is taken from the Font rather than from `Text_Item.advance` +// on purpose. The interpreter positioned the next glyph with the width out of +// that same Font, so measuring against it is self-consistent by construction: +// a font whose /Widths are all zero piles its glyphs on one spot and yields no +// gaps, instead of yielding a gap between every pair. +// +// THE THRESHOLDS, AND WHY THEY ARE WHAT THEY ARE +// +// All of them are fractions of the em, so they hold at any font size. +// +// space 0.20 A gap wider than this becomes a space. Kerning pairs +// are tightenings -- negative gaps -- and the widest +// positive kern in a real font is around 0.05 em; a +// word space is 0.25 to 0.33 em. 0.20 sits in the empty +// band between the two. +// tracking cap 0.15 Letter-spacing (Tc) widens every gap in a run +// equally. The smallest gap in a run is subtracted from +// all of them, capped here so that a table row emitted +// as one widely-spaced TJ keeps its spaces. +// same line 0.35 Baseline separation, incrementally, and 0.80 in +// total, so a subscript joins its line and two lines of +// text never merge -- leading below a third of the font +// size does not exist. +// new block 1.60 Baseline step that ends a paragraph. Normal leading +// is 1.0 to 1.5 em. +// duplicate 0.10 The same character re-drawn within a tenth of an em +// is a producer faking bold by overprinting, not a +// repeated letter. +// +// Nothing here is a Document operation, so there is no `add_repair` to call; +// damage shows up as `Text_Device.truncated` or is simply skipped. + +// Gap, as a fraction of the em, that becomes a space. +@(private) +TEXT_SPACE_EM :: 0.20 +// Most letter-spacing that will be compensated away, in ems. +@(private) +TEXT_TRACK_EM :: 0.15 +// Smallest tracking worth compensating for, in ems. +@(private) +TEXT_TRACK_MIN_EM :: 0.05 +// Adjacent pairs a run needs before its tracking is estimated at all. +@(private) +TEXT_TRACK_PAIRS :: 3 +// Baseline separation that keeps two glyphs on one line: step to step, and +// over the whole line. +@(private) +TEXT_LINE_EM :: 0.35 +@(private) +TEXT_LINE_SPREAD_EM :: 0.80 +// Baseline step that starts a new block. +@(private) +TEXT_BLOCK_EM :: 1.60 +// Distance within which a repeat of the same character is fake bold. +@(private) +TEXT_DUP_EM :: 0.10 +// Em box used for a glyph's rectangle, in the absence of real per-glyph +// bounding boxes: a typical ascent and descent. +@(private) +TEXT_ASCENT_EM :: 0.80 +@(private) +TEXT_DESCENT_EM :: 0.20 +// Glyphs kept from one page. A page of dense CJK holds a few thousand; this +// is three orders of magnitude above any real page and bounds the memory a +// hostile content stream can make us hold. +@(private) +MAX_TEXT_GLYPHS :: 1 << 18 +// Bytes of /ToUnicode text one glyph may stand for. A ligature is three or +// four; a producer that maps a glyph to a paragraph is not describing a glyph. +@(private) +MAX_TEXT_UNICODE_BYTES :: 64 +// An advance is clamped to this many ems. /Widths is attacker-controlled and +// font.odin's own clamp still allows 10 000 ems. +@(private) +MAX_TEXT_ADVANCE_EM :: 100.0 + +// --------------------------------------------------------------------------- +// Structured output +// --------------------------------------------------------------------------- + +// Text_Span is a run of characters on one line in one font at one size. +// `rect` is in device space -- the space the CTM handed to the interpreter +// maps into -- and covers the em boxes of the span's glyphs. +Text_Span :: struct { + text: string, + rect: Rect, + font: ^Font, + size: f64, +} + +// Text_Line is one baseline's worth of spans, ordered along the writing +// direction. +Text_Line :: struct { + spans: []Text_Span, + rect: Rect, +} + +// Text_Block is a run of lines with no unusual vertical gap between them: a +// paragraph, as far as coordinates can tell. +Text_Block :: struct { + lines: []Text_Line, + rect: Rect, +} + +// --------------------------------------------------------------------------- +// The device +// --------------------------------------------------------------------------- + +// Text_Glyph is one placed character, in device space and in the frame of its +// own writing direction. +@(private) +Text_Glyph :: struct { + // Pen position, and where the pen lands after this glyph's advance. + pen, end: Point, + // Unit writing direction, unit "up" (the em's y axis), and the unit normal + // pointing the way successive lines advance. + dir, up, n: Point, + // One em along the writing direction, and one em vertically. + em, size: f64, + // pen projected on n and on dir, in the canonical frame of the glyph's + // angle bucket. Filled in at layout time. + u, v: f64, + r: rune, + // The whole of what the glyph stands for, when that is more than one code + // point; empty otherwise, and `r` is the character. A view into the font's + // /ToUnicode CMap, which the Document owns. + text: string, + font: ^Font, + // Emission order: the last tiebreak in every sort, so the result is + // deterministic, and the order in which equally-placed glyphs are joined. + seq: int, + // Writing angle in whole degrees, 0..359, and its rank in reading order. + bucket: int, + border: int, +} + +// Text_Run_Sig identifies a glyph run closely enough to recognise the same run +// arriving twice. Render mode 2 fills and strokes, and modes 4 to 6 also clip, +// so a device that implements every text hook sees one run two or three times. +@(private) +Text_Run_Sig :: struct { + count: int, + font: ^Font, + first, last: Matrix, + r0, r1: rune, +} + +Text_Device :: struct { + dev: Device, + allocator: mem.Allocator, + glyphs: [dynamic]Text_Glyph, + seq: int, + // Set when MAX_TEXT_GLYPHS was reached and glyphs were dropped. + truncated: bool, + sig: Text_Run_Sig, + have_sig: bool, +} + +// text_device_make builds a device that collects text instead of pixels. +// Everything it allocates comes from `allocator` and is released by +// text_device_destroy. +text_device_make :: proc(allocator := context.allocator) -> (^Text_Device, Error) { + td, err := new(Text_Device, allocator) + if err != nil || td == nil { + return nil, .Out_Of_Memory + } + td.allocator = allocator + gs, gerr := make([dynamic]Text_Glyph, 0, 256, allocator) + if gerr != nil { + free(td, allocator) + return nil, .Out_Of_Memory + } + td.glyphs = gs + + td.dev.user = td + // Text sits on top of photographs and gradients constantly, and decoding + // either to extract it is pure waste. No outlines either: this device + // never draws a glyph, only places it -- which for a Type 3 font also + // spares the interpreter running one content stream per character. And no + // tiling: a pattern cell is replayed once per placement, so a watermark + // lattice with a word in it arrives once per tile. + // + // The transparency hooks are deliberately nil rather than suppressing + // what is drawn between them. Text with a gradient fill is drawn INTO a + // luminosity soft mask and the colour painted through it, so a device + // that ignored mask content would lose exactly that text. + td.dev.hints = {.Ignore_Image, .Ignore_Shade, .No_Glyph_Outlines, .No_Tiling} + td.dev.fill_text = text_dev_fill_text + td.dev.stroke_text = text_dev_stroke_text + td.dev.ignore_text = text_dev_ignore_text + td.dev.clip_text = text_dev_clip_text + return td, .None +} + +text_device_destroy :: proc(td: ^Text_Device) { + if td == nil { + return + } + allocator := td.allocator + delete(td.glyphs) + td^ = {} + free(td, allocator) +} + +// text_device_as_device hands the interpreter the vtable. The Device is +// embedded in the Text_Device, so the two share a lifetime. +text_device_as_device :: proc(td: ^Text_Device) -> ^Device { + if td == nil { + return nil + } + return &td.dev +} + +@(private) +text_dev_self :: proc(dev: ^Device) -> ^Text_Device { + if dev == nil { + return nil + } + return cast(^Text_Device)dev.user +} + +@(private) +text_dev_fill_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { + text_dev_collect(text_dev_self(dev), text) +} + +@(private) +text_dev_stroke_text :: proc(dev: ^Device, text: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { + text_dev_collect(text_dev_self(dev), text) +} + +@(private) +text_dev_ignore_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + text_dev_collect(text_dev_self(dev), text) +} + +// clip_text is implemented so that render mode 7 -- text used only as a clip, +// which is how some producers letter shapes over an image -- is still +// extracted. It pushes no clip: this device has no clip stack, and pop_clip +// stays nil. +@(private) +text_dev_clip_text :: proc(dev: ^Device, text: ^Text, ctm: Matrix) { + text_dev_collect(text_dev_self(dev), text) +} + +// --------------------------------------------------------------------------- +// Collecting +// --------------------------------------------------------------------------- + +@(private) +text_run_sig :: proc(text: ^Text) -> (sig: Text_Run_Sig) { + n := len(text.items) + sig.count = n + sig.font = text.font + if n > 0 { + sig.first = text.items[0].trm + sig.last = text.items[n - 1].trm + sig.r0 = text.items[0].unicode + sig.r1 = text.items[n - 1].unicode + } + return +} + +@(private) +text_dev_collect :: proc(td: ^Text_Device, text: ^Text) { + if td == nil || text == nil || len(text.items) == 0 { + return + } + // The same run reaching us twice is the render mode drawing it twice, not + // the page saying it twice: identical glyphs at identical positions are + // one piece of text however many times they are inked. + sig := text_run_sig(text) + if td.have_sig && sig == td.sig { + return + } + td.have_sig = true + td.sig = sig + + f := text.font + fm := font_matrix(f) + fm_inv, inv_ok := matrix_invert(fm) + if !inv_ok { + // A singular font matrix draws nothing, but the text is still there to + // be read; assume the 1/1000 glyph space everything but Type3 uses. + fm_inv = Matrix{1000, 0, 0, 1000, 0, 0} + } + vertical := text.wmode == 1 + + start := len(td.glyphs) + for &it in text.items { + if len(td.glyphs) >= MAX_TEXT_GLYPHS { + td.truncated = true + break + } + if !matrix_is_finite(it.trm) { + continue + } + t := matrix_mul(fm_inv, it.trm) + if !matrix_is_finite(t) { + continue + } + + g: Text_Glyph + g.pen = Point{t.e, t.f} + hvec := Point{t.a, t.b} + vvec := Point{t.c, t.d} + hlen := text_len(hvec) + vlen := text_len(vvec) + if hlen < 1e-9 && vlen < 1e-9 { + // A matrix that collapses to a point places nothing anywhere. + continue + } + // One axis flattened still positions text, so the surviving axis + // stands in for the other: it gives the missing size, and turning it + // a quarter turn gives the missing direction. Upright text has the + // x axis to the right and the y axis up the page, which in a y-down + // device space is (1, 0) and (0, -1) -- that is the turn taken here. + hdir := hlen > 1e-9 ? Point{hvec.x / hlen, hvec.y / hlen} : Point{} + up := vlen > 1e-9 ? Point{vvec.x / vlen, vvec.y / vlen} : Point{} + if hlen < 1e-9 { + hdir = Point{-up.y, up.x} + hlen = vlen + } else if vlen < 1e-9 { + up = Point{hdir.y, -hdir.x} + vlen = hlen + } + if vertical { + // A vertical font writes down text space's -y, and its columns + // advance along -x (9.7.4.3). + g.dir = Point{-up.x, -up.y} + g.n = text_perp_towards(g.dir, Point{-hdir.x, -hdir.y}) + g.em = vlen + } else { + g.dir = hdir + g.n = text_perp_towards(g.dir, Point{-up.x, -up.y}) + g.em = hlen + } + g.up = up + g.size = vlen + g.font = f + g.r = text_rune_of(it.unicode) + g.text = text_glyph_text(f, &it) + + adv := text_em_advance(f, &it, vertical) * g.em + g.end = Point{g.pen.x + g.dir.x * adv, g.pen.y + g.dir.y * adv} + if !is_finite(g.end.x) || !is_finite(g.end.y) { + // An advance that overflows says nothing about where the next + // glyph starts; a zero-width glyph is the honest reading. + g.end = g.pen + } + + deg := math.atan2(g.dir.y, g.dir.x) * 180 / math.PI + if !is_finite(deg) { + continue + } + b := int(math.round(deg)) % 360 + if b < 0 { b += 360 } + g.bucket = b + + g.seq = td.seq + td.seq += 1 + append(&td.glyphs, g) + } + + text_run_track(td.glyphs[start:]) +} + +// text_run_track compensates for letter-spacing. Tc widens the gap after every +// glyph in a run by the same amount, which at 0.2 em and up would otherwise +// put a space between every pair of letters in a tracked heading. The smallest +// gap in the run is the one least likely to be a word break, so it is taken as +// the tracking and removed from all of them. +// +// The cap is what keeps this safe: a table row emitted as one TJ with huge +// gaps between the cells has a large "smallest gap", and without the cap the +// spaces between its cells would be compensated away. +@(private) +text_run_track :: proc(gs: []Text_Glyph) { + if len(gs) < TEXT_TRACK_PAIRS + 1 { + return + } + least := math.INF_F64 + pairs := 0 + for i in 0 ..< len(gs) - 1 { + a := &gs[i] + b := &gs[i + 1] + if a.em <= 0 || a.bucket != b.bucket { + continue + } + d := Point{b.pen.x - a.end.x, b.pen.y - a.end.y} + // Only pairs that sit on one baseline say anything about tracking. + if abs(d.x * a.n.x + d.y * a.n.y) > TEXT_LINE_EM * a.em { + continue + } + gap := (d.x * a.dir.x + d.y * a.dir.y) / a.em + least = min(least, gap) + pairs += 1 + } + if pairs < TEXT_TRACK_PAIRS || !is_finite(least) || least < TEXT_TRACK_MIN_EM { + return + } + track := min(least, TEXT_TRACK_EM) + for &g in gs { + g.end.x += g.dir.x * track * g.em + g.end.y += g.dir.y * track * g.em + } +} + +// text_em_advance is the glyph's advance in text space, where 1 is an em. +@(private) +text_em_advance :: proc(f: ^Font, it: ^Text_Item, vertical: bool) -> f64 { + if f == nil { + return 0 + } + w: f64 + if f.kind == .Type0 { + w = font_cid_width(f, it.cid) + if vertical { + w1y, _, _ := font_cid_vmetric(f, it.cid, w) + // The writing direction points along text space -y, so the + // displacement -- negative by convention -- is a positive distance + // along it. + return is_finite(w1y) ? text_clamp_em(-w1y / 1000) : 0 + } + } else if it.code < FONT_CODES { + w = f.code_width[it.code] + } + if !is_finite(w) { + return 0 + } + if f.kind == .Type3 { + // A Type3 width is in the font's own glyph space and only its + // /FontMatrix says how big that is (9.6.4). + v := transform_vector(font_matrix(f), Point{w, 0}) + return is_finite(v.x) ? text_clamp_em(v.x) : 0 + } + // Every other width in a PDF is thousandths of text space (9.6.2.2). + return text_clamp_em(w / 1000) +} + +@(private) +text_clamp_em :: proc(x: f64) -> f64 { + return clamp(x, -MAX_TEXT_ADVANCE_EM, MAX_TEXT_ADVANCE_EM) +} + +// text_rune_of maps what the font resolved to a character to put in the +// output. A glyph the font offers no Unicode for becomes U+FFFD: dropping it +// silently would join the words either side of it, and the caller cannot tell +// that happened. +@(private) +text_rune_of :: proc(r: rune) -> rune { + switch { + case r <= 0: + // font.odin uses -1 for "none"; 0 is not a character a glyph maps to. + return 0xFFFD + case r < 0x20: + // A control code is not a glyph. Producers emit them through a broken + // /ToUnicode; a space keeps the line structure this file imposes. + return ' ' + case r > 0x10FFFF, r >= 0xD800 && r <= 0xDFFF: + return 0xFFFD + } + return r +} + +// text_glyph_text is the several code points a glyph stands for, when it +// stands for more than one. +// +// Font_Code.unicode is a single rune -- the frozen shape of the struct -- so +// an "ffi" ligature arrives as 'f' and the word loses two letters. That is not +// font.odin being wrong: its own tests say the first character is what the +// field reports and "the whole string is cmap_lookup_text's to give". This is +// the caller asking for the rest of it. +// +// The result is only used when its first code point is the one font.odin +// chose. A disagreement means /ToUnicode was not the route it took -- a +// mapping it rejected as malformed, say -- and its answer wins. +@(private) +text_glyph_text :: proc(f: ^Font, it: ^Text_Item) -> string { + if f == nil || f.to_unicode == nil || it.unicode < 0 { + return "" + } + txt, ok := cmap_lookup_text(f.to_unicode, it.code) + if !ok || len(txt) < 2 || len(txt) > MAX_TEXT_UNICODE_BYTES { + return "" + } + r0, size := utf8.decode_rune_in_string(txt) + if r0 != it.unicode || size >= len(txt) { + return "" + } + return txt +} + +@(private) +text_len :: proc(p: Point) -> f64 { + return math.sqrt(p.x * p.x + p.y * p.y) +} + +// text_perp_towards is the unit normal of `dir`, turned to agree with `want`. +// Taking the sign from a vector out of the same matrix is what keeps mirrored +// text -- and a page whose base matrix flips y -- ordered the right way round. +@(private) +text_perp_towards :: proc(dir, want: Point) -> Point { + p := Point{-dir.y, dir.x} + if p.x * want.x + p.y * want.y < 0 { + return Point{-p.x, -p.y} + } + return p +} + +// --------------------------------------------------------------------------- +// Layout: glyphs to lines to blocks +// --------------------------------------------------------------------------- + +@(private) +Text_Layout_Line :: struct { + first, last: int, // half-open range of glyphs + u: f64, // baseline, in the bucket's frame + size: f64, // largest em on the line + dir, n: Point, + border: int, + block: int, +} + +@(private) +Text_Layout :: struct { + glyphs: []Text_Glyph, + lines: [dynamic]Text_Layout_Line, + blocks: int, + allocator: mem.Allocator, +} + +@(private) +text_layout_destroy :: proc(lay: ^Text_Layout) { + if lay == nil { + return + } + delete(lay.glyphs, lay.allocator) + delete(lay.lines) + lay^ = {} +} + +@(private) +Text_Bucket_Rank :: struct { + seq, bucket: int, +} + +@(private) +text_layout :: proc(td: ^Text_Device, allocator: mem.Allocator) -> (lay: Text_Layout) { + lay.allocator = allocator + if td == nil || len(td.glyphs) == 0 { + return + } + gs, err := make([]Text_Glyph, len(td.glyphs), allocator) + if err != nil { + return + } + copy(gs, td.glyphs[:]) + lay.glyphs = gs + + // One canonical frame per whole degree of writing angle. Projecting every + // glyph in a bucket on one frame -- rather than each on its own -- is what + // makes the baseline coordinate comparable: half a degree of disagreement + // is seven points of drift across a line of text, which is a whole line + // height of error. + first_seq: [360]int + dirs, norms: [360]Point + for i in 0 ..< 360 { + first_seq[i] = max(int) + } + for g in gs { + if g.seq < first_seq[g.bucket] { + first_seq[g.bucket] = g.seq + dirs[g.bucket] = g.dir + norms[g.bucket] = g.n + } + } + // Buckets are read in the order they were first drawn, so that a rotated + // caption on an upright page lands where the producer put it rather than + // at an angle-sorted position nobody chose. + ranks: [360]Text_Bucket_Rank + nranks := 0 + for i in 0 ..< 360 { + if first_seq[i] != max(int) { + ranks[nranks] = {first_seq[i], i} + nranks += 1 + } + } + slice.sort_by(ranks[:nranks], proc(a, b: Text_Bucket_Rank) -> bool { + return a.seq < b.seq + }) + order: [360]int + for e, i in ranks[:nranks] { + order[e.bucket] = i + } + for &g in gs { + d := dirs[g.bucket] + n := norms[g.bucket] + g.u = g.pen.x * n.x + g.pen.y * n.y + g.v = g.pen.x * d.x + g.pen.y * d.y + g.border = order[g.bucket] + } + text_layout_rescale(gs) + + slice.sort_by(gs, proc(a, b: Text_Glyph) -> bool { + if a.border != b.border { return a.border < b.border } + if a.u != b.u { return a.u < b.u } + if a.v != b.v { return a.v < b.v } + return a.seq < b.seq + }) + + lay.lines = make([dynamic]Text_Layout_Line, 0, 32, allocator) + i := 0 + for i < len(gs) { + anchor := gs[i].u + prev := gs[i].u + size := gs[i].size + j := i + 1 + for j < len(gs) { + g := gs[j] + if g.border != gs[i].border { + break + } + limit := max(size, g.size) + if g.u - prev > TEXT_LINE_EM * limit || g.u - anchor > TEXT_LINE_SPREAD_EM * limit { + break + } + prev = g.u + size = max(size, g.size) + j += 1 + } + // Reading order along the baseline. The emission order is the + // tiebreak, so glyphs stacked at one point keep the order they were + // drawn in. + slice.sort_by(gs[i:j], proc(a, b: Text_Glyph) -> bool { + if a.v != b.v { return a.v < b.v } + return a.seq < b.seq + }) + append(&lay.lines, Text_Layout_Line{ + first = i, + last = j, + u = anchor, + size = size, + dir = dirs[gs[i].bucket], + n = norms[gs[i].bucket], + border = gs[i].border, + }) + i = j + } + + nblocks := 0 + for &ln, k in lay.lines { + if k > 0 { + p := lay.lines[k - 1] + if ln.border != p.border || ln.u - p.u > TEXT_BLOCK_EM * max(ln.size, p.size) { + nblocks += 1 + } + } + ln.block = nblocks + } + lay.blocks = len(lay.lines) > 0 ? nblocks + 1 : 0 + return +} + +// text_layout_rescale is a guard, not a feature. Text_Item.trm is contracted +// to map glyph space to device space, and this file divides the font matrix +// back out of it to recover text space. A producer of Text_Items that handed +// us the text-space matrix instead would leave every em a thousand times too +// large, and a thousand-point em turns a page of text into one unreadable +// line with no spaces in it. +// +// That is detectable without ambiguity: if every glyph on the page fits inside +// a small fraction of a single em, the matrices cannot mean what they claim. +// Only a page with enough glyphs to be sure is touched, and a page that really +// does stack all its text on one spot has nothing to lose either way. +@(private) +text_layout_rescale :: proc(gs: []Text_Glyph) { + if len(gs) < 16 { + return + } + lo := gs[0].pen + hi := gs[0].pen + em := 0.0 + for g in gs { + lo.x = min(lo.x, g.pen.x); lo.y = min(lo.y, g.pen.y) + hi.x = max(hi.x, g.pen.x); hi.y = max(hi.y, g.pen.y) + em = max(em, g.em, g.size) + } + extent := text_len(Point{hi.x - lo.x, hi.y - lo.y}) + if em <= 20 * extent { + return + } + for &g in gs { + s := matrix_expansion(font_matrix(g.font)) + if !is_finite(s) || s <= 0 || s >= 1 { + continue + } + g.em *= s + g.size *= s + g.end.x = g.pen.x + (g.end.x - g.pen.x) * s + g.end.y = g.pen.y + (g.end.y - g.pen.y) * s + } +} + +// --------------------------------------------------------------------------- +// Rendering the layout +// --------------------------------------------------------------------------- + +@(private) +Text_Rect_Acc :: struct { + r: Rect, + have: bool, +} + +@(private) +text_acc_point :: proc(acc: ^Text_Rect_Acc, p: Point) { + if !is_finite(p.x) || !is_finite(p.y) { + return + } + if !acc.have { + acc.r = Rect{p.x, p.y, p.x, p.y} + acc.have = true + return + } + acc.r.x0 = min(acc.r.x0, p.x) + acc.r.y0 = min(acc.r.y0, p.y) + acc.r.x1 = max(acc.r.x1, p.x) + acc.r.y1 = max(acc.r.y1, p.y) +} + +@(private) +text_acc_rect :: proc(acc: ^Text_Rect_Acc, r: Rect, have: bool) { + if !have { + return + } + text_acc_point(acc, Point{r.x0, r.y0}) + text_acc_point(acc, Point{r.x1, r.y1}) +} + +// text_glyph_box is the em box the glyph occupies, as a device-space +// rectangle. Real per-glyph bounding boxes would need the outlines this device +// asks the interpreter not to load, and a selection rectangle wants the line's +// full height anyway. +@(private) +text_glyph_box :: proc(g: ^Text_Glyph, acc: ^Text_Rect_Acc) { + a := g.up * (TEXT_ASCENT_EM * g.size) + d := g.up * (TEXT_DESCENT_EM * g.size) + text_acc_point(acc, g.pen + a) + text_acc_point(acc, g.pen - d) + text_acc_point(acc, g.end + a) + text_acc_point(acc, g.end - d) +} + +// text_device_blocks returns the page's text as blocks of lines of spans. +// +// The blocks, the lines, the spans and every string in them are allocated with +// `allocator` and belong to the caller: release them with text_blocks_destroy. +// Calling this twice is fine; nothing is consumed. +text_device_blocks :: proc(td: ^Text_Device, allocator := context.allocator) -> []Text_Block { + if td == nil { + return nil + } + lay := text_layout(td, td.allocator) + defer text_layout_destroy(&lay) + if len(lay.lines) == 0 { + return nil + } + + blocks := make([dynamic]Text_Block, 0, lay.blocks, allocator) + lines := make([dynamic]Text_Line, 0, 8, allocator) + spans := make([dynamic]Text_Span, 0, 4, allocator) + sb := strings.builder_make(td.allocator) + defer strings.builder_destroy(&sb) + + block_acc: Text_Rect_Acc + line_acc: Text_Rect_Acc + span_acc: Text_Rect_Acc + + cur_block := 0 + for ln, li in lay.lines { + if li > 0 && ln.block != cur_block { + if len(lines) > 0 { + append(&blocks, Text_Block{lines = slice.clone(lines[:], allocator), rect = block_acc.r}) + clear(&lines) + } + block_acc = {} + cur_block = ln.block + } + + clear(&spans) + line_acc = {} + span_acc = {} + strings.builder_reset(&sb) + span_font: ^Font + span_size: f64 + have_span := false + prev: ^Text_Glyph + + for k in ln.first ..< ln.last { + g := &lay.glyphs[k] + if prev != nil && g.r == prev.r && g.text == prev.text && + text_len(Point{g.pen.x - prev.pen.x, g.pen.y - prev.pen.y}) <= TEXT_DUP_EM * max(g.em, prev.em) { + // The same character re-inked in the same place: fake bold. + continue + } + space := false + if prev != nil { + d := Point{g.pen.x - prev.end.x, g.pen.y - prev.end.y} + gap := d.x * ln.dir.x + d.y * ln.dir.y + space = gap > TEXT_SPACE_EM * max(g.em, prev.em) + } + // A space is only ever a separator, so it never opens a line and + // never doubles one that is already there. + if space && (!have_span || text_builder_ends_space(&sb)) { + space = false + } + if have_span && (g.font != span_font || abs(g.size - span_size) > 0.01 * max(g.size, span_size)) { + if space { + strings.write_byte(&sb, ' ') + space = false + } + text_flush_span(&spans, &sb, span_font, span_size, &span_acc, allocator) + text_acc_rect(&line_acc, span_acc.r, span_acc.have) + span_acc = {} + have_span = false + } + if !have_span { + span_font = g.font + span_size = g.size + have_span = true + } + if space { + strings.write_byte(&sb, ' ') + } + if g.r == ' ' && len(g.text) == 0 && strings.builder_len(sb) == 0 && len(spans) == 0 { + // Leading space glyphs carry nothing the rectangle does not. + continue + } + if len(g.text) > 0 { + for r in g.text { + strings.write_rune(&sb, text_rune_of(r)) + } + } else { + strings.write_rune(&sb, g.r) + } + text_glyph_box(g, &span_acc) + prev = g + } + // Trailing spaces are producer noise, not content. + text_builder_trim_right(&sb) + if have_span && strings.builder_len(sb) > 0 { + text_flush_span(&spans, &sb, span_font, span_size, &span_acc, allocator) + text_acc_rect(&line_acc, span_acc.r, span_acc.have) + } + if len(spans) == 0 { + continue + } + append(&lines, Text_Line{spans = slice.clone(spans[:], allocator), rect = line_acc.r}) + text_acc_rect(&block_acc, line_acc.r, line_acc.have) + } + if len(lines) > 0 { + append(&blocks, Text_Block{lines = slice.clone(lines[:], allocator), rect = block_acc.r}) + } + delete(lines) + delete(spans) + return blocks[:] +} + +@(private) +text_flush_span :: proc(spans: ^[dynamic]Text_Span, sb: ^strings.Builder, font: ^Font, size: f64, + acc: ^Text_Rect_Acc, allocator: mem.Allocator) { + if strings.builder_len(sb^) == 0 { + return + } + append(spans, Text_Span{ + text = strings.clone(strings.to_string(sb^), allocator), + rect = acc.r, + font = font, + size = size, + }) + strings.builder_reset(sb) +} + +@(private) +text_builder_ends_space :: proc(sb: ^strings.Builder) -> bool { + n := len(sb.buf) + return n > 0 && sb.buf[n - 1] == ' ' +} + +@(private) +text_builder_trim_right :: proc(sb: ^strings.Builder) { + for len(sb.buf) > 0 && sb.buf[len(sb.buf) - 1] == ' ' { + pop(&sb.buf) + } +} + +// text_blocks_destroy releases what text_device_blocks returned. `allocator` +// must be the one it was given. +text_blocks_destroy :: proc(blocks: []Text_Block, allocator := context.allocator) { + for b in blocks { + for l in b.lines { + for s in l.spans { + delete(s.text, allocator) + } + delete(l.spans, allocator) + } + delete(b.lines, allocator) + } + delete(blocks, allocator) +} + +// text_device_result is the page's text as one string: spans joined with +// nothing, lines with a newline, blocks with a blank line. There is no +// trailing newline, so a caller stitching pages together decides its own +// separator. +// +// The string is allocated with `allocator` and belongs to the caller; release +// it with `delete(s, allocator)`. It is built from exactly the same layout +// text_device_blocks returns, so the two can never disagree. +text_device_result :: proc(td: ^Text_Device, allocator := context.allocator) -> string { + b := strings.builder_make(allocator) + if td == nil { + return strings.to_string(b) + } + blocks := text_device_blocks(td, td.allocator) + defer text_blocks_destroy(blocks, td.allocator) + for blk, bi in blocks { + if bi > 0 { + strings.write_string(&b, "\n\n") + } + for ln, li in blk.lines { + if li > 0 { + strings.write_byte(&b, '\n') + } + for sp in ln.spans { + strings.write_string(&b, sp.text) + } + } + } + return strings.to_string(b) +} diff --git a/device_text_test.odin b/device_text_test.odin new file mode 100644 index 0000000..ffe2fbf --- /dev/null +++ b/device_text_test.odin @@ -0,0 +1,1090 @@ +package pdf + +import "core:math" +import "core:strings" +import "core:testing" + +// The text device is tested the way the interpreter will drive it: by handing +// it Text runs whose items carry a `trm`. Nothing here needs a Document -- a +// Font is a plain struct, and `trm` is the only geometry the device reads -- +// which keeps these tests independent of content.odin. +// +// Every run is at size 10 with 500/1000 glyphs unless it says otherwise, so +// one glyph advances 5 device units, an em is 10, and the space threshold +// (0.20 em) is 2. + +@(private) +tdt_font_simple :: proc(f: ^Font, width: f64 = 500) { + f.kind = .Type1 + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + for c in 0 ..< FONT_CODES { + f.code_width[c] = width + } +} + +// tdt_tm is the text matrix a page's base matrix produces for upright text: +// x to the right, y down the device, which is the flip render_page installs. +@(private) +tdt_tm :: proc(x, y, size: f64) -> Matrix { + return Matrix{size, 0, 0, -size, x, y} +} + +@(private) +tdt_item :: proc(f: ^Font, tm: Matrix, r: rune) -> Text_Item { + return Text_Item{ + trm = matrix_mul(font_matrix(f), tm), + code = 65, + cid = 65, + unicode = r, + advance = 5, + } +} + +@(private) +tdt_show :: proc(td: ^Text_Device, f: ^Font, items: []Text_Item, wmode := 0) { + txt := text_make() + defer text_destroy(&txt) + txt.font = f + txt.wmode = wmode + for it in items { + append(&txt.items, it) + } + dev_fill_text(text_device_as_device(td), &txt, MATRIX_IDENTITY, Paint{}) +} + +// tdt_line shows an ASCII string with one x position per byte. +@(private) +tdt_line :: proc(td: ^Text_Device, f: ^Font, s: string, xs: []f64, y, size: f64) { + items := make([dynamic]Text_Item, 0, len(s)) + defer delete(items) + for i in 0 ..< len(s) { + x := i < len(xs) ? xs[i] : f64(i) * size * 0.5 + append(&items, tdt_item(f, tdt_tm(x, y, size), rune(s[i]))) + } + tdt_show(td, f, items[:]) +} + +// tdt_run shows an ASCII string laid out on one baseline at a fixed step. +@(private) +tdt_run :: proc(td: ^Text_Device, f: ^Font, s: string, x0, y, size, step: f64) { + xs := make([dynamic]f64, 0, len(s)) + defer delete(xs) + for i in 0 ..< len(s) { + append(&xs, x0 + f64(i) * step) + } + tdt_line(td, f, s, xs[:], y, size) +} + +@(private) +tdt_text :: proc(t: ^testing.T, td: ^Text_Device) -> string { + s := text_device_result(td) + return s +} + +// --------------------------------------------------------------------------- + +@(test) +test_text_device_lifecycle_and_hints :: proc(t: ^testing.T) { + td, err := text_device_make() + testing.expect_value(t, err, Error.None) + testing.expect(t, td != nil, "a text device must be built") + defer text_device_destroy(td) + + dev := text_device_as_device(td) + testing.expect(t, dev != nil, "") + // The whole point of the hints: a page whose text sits on a 4000x4000 + // photograph must not decode it. + testing.expect(t, !dev_wants_images(dev), "the text device must not want images") + testing.expect(t, !dev_wants_shades(dev), "the text device must not want shadings") + testing.expect(t, .No_Glyph_Outlines in dev.hints, "outlines are never drawn here") + // Every path and image hook stays nil; the interpreter's dev_* wrappers + // are what make that free. + testing.expect(t, dev.fill_path == nil && dev.stroke_path == nil, "") + testing.expect(t, dev.fill_image == nil && dev.fill_shade == nil, "") + testing.expect(t, dev.pop_clip == nil, "a device with no clip stack pops nothing") + testing.expect(t, dev.fill_text != nil && dev.stroke_text != nil && dev.ignore_text != nil, "") + + // An empty page extracts to an empty string, not to a newline. + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "") + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect_value(t, len(blocks), 0) + + text_device_destroy(nil) + testing.expect(t, text_device_as_device(nil) == nil, "") +} + +// Glyphs emitted left to right, each advancing by its own width, are one word: +// no spaces invented between letters. +@(test) +test_text_device_joins_glyphs_into_a_word :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "Hello", 0, 100, 10, 5) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "Hello") +} + +// The device reads positions, not the order the producer chose to emit them +// in. This is the same word with its glyphs shown in a shuffled order. +@(test) +test_text_device_sorts_out_of_order_glyphs :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + order := [5]int{2, 0, 4, 1, 3} + word := "Hello" + items := make([dynamic]Text_Item, 0, 5) + defer delete(items) + for k in order { + append(&items, tdt_item(&f, tdt_tm(f64(k) * 5, 100, 10), rune(word[k]))) + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "Hello") +} + +// Producers routinely position words instead of emitting a space character. +// A gap wider than 0.20 em is a word break; anything narrower is kerning. +@(test) +test_text_device_wide_gap_becomes_a_space :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // "the" ends at 15; "quick" starts at 18, so the gap is 3 = 0.30 em. + xs := [8]f64{0, 5, 10, 18, 23, 28, 33, 38} + tdt_line(td, &f, "thequick", xs[:], 100, 10) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "the quick") +} + +@(test) +test_text_device_kerning_gap_is_not_a_space :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + + // 0.8 device units is 0.08 em: a wide positive kern, and the largest gap + // that must NOT become a space. + { + td, _ := text_device_make() + defer text_device_destroy(td) + xs := [2]f64{0, 5.8} + tdt_line(td, &f, "AV", xs[:], 100, 10) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "AV") + } + // 2.5 device units is 0.25 em, a word space, and must. + { + td, _ := text_device_make() + defer text_device_destroy(td) + xs := [2]f64{0, 7.5} + tdt_line(td, &f, "AV", xs[:], 100, 10) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "A V") + } +} + +// Two baselines a normal leading apart are two lines of one paragraph. +@(test) +test_text_device_two_baselines_are_two_lines :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "abc", 0, 100, 10, 5) + tdt_run(td, &f, "def", 0, 112, 10, 5) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abc\ndef") + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect_value(t, len(blocks), 1) + testing.expect_value(t, len(blocks[0].lines), 2) +} + +// Baselines that drift by less than the step tolerance would otherwise chain +// glyph to glyph until a whole page was one line. The total spread of a line +// is bounded as well as the step. +@(test) +test_text_device_drifting_baselines_do_not_chain :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // 0.3 em of drift per glyph: under the 0.35 em step, over the 0.80 em a + // line is allowed to span in total. + items := make([dynamic]Text_Item, 0, 5) + defer delete(items) + word := "abcde" + for i in 0 ..< len(word) { + append(&items, tdt_item(&f, tdt_tm(f64(i) * 5, 100 + f64(i) * 3, 10), rune(word[i]))) + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abc\nde") +} + +// A page rotated 90 degrees carries the rotation in the text matrix, so +// "left to right" is not device x and "top to bottom" is not device y. The +// same code has to read it without a special case. +@(test) +test_text_device_rotated_text_reads_in_order :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // Writing direction is device -y (up the page); the glyph's own up axis + // points at device -x. Successive lines therefore advance along +x. + tm :: proc(x, y: f64) -> Matrix { return Matrix{0, -10, -10, 0, x, y} } + items := make([dynamic]Text_Item, 0, 6) + defer delete(items) + append(&items, tdt_item(&f, tm(100, 0), 'a')) + append(&items, tdt_item(&f, tm(100, -5), 'b')) + append(&items, tdt_item(&f, tm(100, -10), 'c')) + append(&items, tdt_item(&f, tm(112, 0), 'd')) + append(&items, tdt_item(&f, tm(112, -5), 'e')) + append(&items, tdt_item(&f, tm(112, -10), 'f')) + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + // Read along the baseline, and the columns in the order the rotation puts + // them; reading device x and y instead would give "ad\nbe\ncf". + testing.expect_value(t, s, "abc\ndef") +} + +// Render mode 3 paints nothing at all, which is exactly how a scanned page +// stores the OCR layer a user most wants to select. It reaches the device +// through ignore_text and must come out whole. +@(test) +test_text_device_invisible_text_is_extracted :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + txt := text_make() + defer text_destroy(&txt) + txt.font = &f + txt.render_mode = .Invisible + ocr := "OCR" + for i in 0 ..< len(ocr) { + append(&txt.items, tdt_item(&f, tdt_tm(f64(i) * 5, 100, 10), rune(ocr[i]))) + } + dev_ignore_text(text_device_as_device(td), &txt, MATRIX_IDENTITY) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "OCR") +} + +// Mode 1 is stroked text and is real text; mode 7 is a clip and is still text +// on the page. Both arrive on their own hook. +@(test) +test_text_device_stroked_and_clipped_text_is_extracted :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + stroke := STROKE_STATE_DEFAULT + a := text_make() + defer text_destroy(&a) + a.font = &f + append(&a.items, tdt_item(&f, tdt_tm(0, 100, 10), 'S')) + dev_stroke_text(text_device_as_device(td), &a, &stroke, MATRIX_IDENTITY, Paint{}) + + b := text_make() + defer text_destroy(&b) + b.font = &f + append(&b.items, tdt_item(&f, tdt_tm(5, 100, 10), 'C')) + dev_clip_text(text_device_as_device(td), &b, MATRIX_IDENTITY) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "SC") +} + +// Render mode 2 fills and strokes, and modes 4 to 6 also clip, so one run +// reaches a device that implements every text hook two or three times. It is +// one piece of text. +@(test) +test_text_device_run_seen_twice_is_counted_once :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + stroke := STROKE_STATE_DEFAULT + txt := text_make() + defer text_destroy(&txt) + txt.font = &f + txt.render_mode = .Fill_Stroke_Clip + append(&txt.items, tdt_item(&f, tdt_tm(0, 100, 10), 'A')) + append(&txt.items, tdt_item(&f, tdt_tm(5, 100, 10), 'B')) + + dev := text_device_as_device(td) + dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{}) + dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, Paint{}) + dev_clip_text(dev, &txt, MATRIX_IDENTITY) + + // A different run at a different place is not a repeat. + other := text_make() + defer text_destroy(&other) + other.font = &f + append(&other.items, tdt_item(&f, tdt_tm(10, 100, 10), 'C')) + dev_fill_text(dev, &other, MATRIX_IDENTITY, Paint{}) + + // The glyphs are dropped on arrival, not tidied up at the end: three + // passes over a two-glyph run must not cost six glyphs of memory. + testing.expect_value(t, len(td.glyphs), 3) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "ABC") +} + +// A glyph the font resolves no Unicode for must leave a mark. Dropping it +// silently joins the words either side of it and the caller cannot tell. +@(test) +test_text_device_missing_unicode_becomes_replacement :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 3) + defer delete(items) + append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'A')) + it := tdt_item(&f, tdt_tm(5, 100, 10), 'x') + it.unicode = -1 // font.odin's "no Unicode at all" + append(&items, it) + append(&items, tdt_item(&f, tdt_tm(10, 100, 10), 'B')) + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "A�B") + testing.expect_value(t, len(s), 5) // three characters, one of them 3 bytes +} + +// More than a line height between two baselines is a paragraph break. +@(test) +test_text_device_blank_line_between_blocks :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "abc", 0, 100, 10, 5) + tdt_run(td, &f, "def", 0, 112, 10, 5) // 1.2 em: same paragraph + tdt_run(td, &f, "ghi", 0, 152, 10, 5) // 4.0 em: a new one + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abc\ndef\n\nghi") + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect_value(t, len(blocks), 2) + testing.expect_value(t, len(blocks[0].lines), 2) + testing.expect_value(t, len(blocks[1].lines), 1) +} + +// The structured form is what a viewer needs to draw a selection rectangle. +@(test) +test_text_device_blocks_carry_spans_and_rectangles :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + g: Font + tdt_font_simple(&g) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 4) + defer delete(items) + append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'a')) + append(&items, tdt_item(&f, tdt_tm(5, 100, 10), 'b')) + tdt_show(td, &f, items[:]) + clear(&items) + // A second font on the same baseline, at a different size. + append(&items, tdt_item(&g, tdt_tm(10, 100, 20), 'C')) + tdt_show(td, &g, items[:]) + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect_value(t, len(blocks), 1) + testing.expect_value(t, len(blocks[0].lines), 1) + + line := blocks[0].lines[0] + testing.expect_value(t, len(line.spans), 2) + testing.expect_value(t, line.spans[0].text, "ab") + testing.expect_value(t, line.spans[0].size, 10) + testing.expect(t, line.spans[0].font == &f, "a span names the font it was set in") + testing.expect_value(t, line.spans[1].text, "C") + testing.expect_value(t, line.spans[1].size, 20) + testing.expect(t, line.spans[1].font == &g, "") + + // "ab" spans x 0..10 and, at 0.8 em up and 0.2 em down from a baseline at + // y=100, y 92..102. + r := line.spans[0].rect + testing.expect(t, abs(r.x0 - 0) < 1e-9 && abs(r.x1 - 10) < 1e-9, "span x") + testing.expect(t, abs(r.y0 - 92) < 1e-9 && abs(r.y1 - 102) < 1e-9, "span y") + // The line and block rectangles cover every span. + testing.expect(t, line.rect.x1 >= 20 && line.rect.y0 <= 92, "line rect covers its spans") + testing.expect_value(t, blocks[0].rect, line.rect) +} + +// Letter-spacing (Tc) widens every gap in a run equally. Without +// compensation a tracked heading extracts as "S P A C E D". +@(test) +test_text_device_tracking_is_not_word_spaces :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // Advance 5 plus 2.5 of tracking: every gap is 0.25 em, over the space + // threshold, and none of them is a word break. + xs := [7]f64{0, 7.5, 15, 22.5, 30, 37.5, 50.5} + // ... except the last, which is 8 further on than the tracking explains. + tdt_line(td, &f, "SPACEDX", xs[:], 100, 10) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "SPACED X") +} + +// The cap on that compensation is what keeps a table row -- one TJ with wide +// gaps between the cells -- from having every space compensated away. +@(test) +test_text_device_tracking_cap_keeps_table_cells_apart :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // Five cells, 8 units (0.8 em) of clear space between each. + xs := [5]f64{0, 13, 26, 39, 52} + tdt_line(td, &f, "ABCDE", xs[:], 100, 10) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "A B C D E") +} + +// Producers fake bold by drawing the same glyph twice a hair apart. It is one +// letter. +@(test) +test_text_device_fake_bold_is_not_doubled :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 4) + defer delete(items) + append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'A')) + append(&items, tdt_item(&f, tdt_tm(0.3, 100, 10), 'A')) + append(&items, tdt_item(&f, tdt_tm(5, 100, 10), 'B')) + append(&items, tdt_item(&f, tdt_tm(5.3, 100, 10), 'B')) + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "AB") +} + +// A real repeated letter is not fake bold: it is a whole advance away. +@(test) +test_text_device_repeated_letter_survives :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "aardvark", 0, 100, 10, 5) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "aardvark") +} + +// A composite font's advances come from /W and /DW, not from the 256-entry +// table a simple font uses, and a Type3's come through its /FontMatrix. Get +// either wrong and a line of text turns into a line of spaced letters. +@(test) +test_text_device_composite_and_type3_advances :: proc(t: ^testing.T) { + { + f: Font + f.kind = .Type0 + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + f.default_width = 1000 // a full em per glyph + td, _ := text_device_make() + defer text_device_destroy(td) + tdt_run(td, &f, "CJK", 0, 100, 10, 10) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "CJK") + } + { + // A Type3 whose glyph space is hundredths: /Widths of 50 is half an em. + f: Font + f.kind = .Type3 + f.mtx = Matrix{0.01, 0, 0, 0.01, 0, 0} + for c in 0 ..< FONT_CODES { + f.code_width[c] = 50 + } + td, _ := text_device_make() + defer text_device_destroy(td) + tdt_run(td, &f, "abc", 0, 100, 10, 5) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abc") + } +} + +// A vertical CMap writes down text space's -y, and its columns advance +// leftwards, so the rightmost column is read first (ISO 32000-2, 9.7.4.3). +@(test) +test_text_device_vertical_writing :: proc(t: ^testing.T) { + f: Font + f.kind = .Type0 + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + f.vertical = true + f.default_width = 1000 + f.default_w1 = -1000 + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 6) + defer delete(items) + append(&items, tdt_item(&f, tdt_tm(100, 0, 10), 'a')) + append(&items, tdt_item(&f, tdt_tm(100, 10, 10), 'b')) + append(&items, tdt_item(&f, tdt_tm(100, 20, 10), 'c')) + append(&items, tdt_item(&f, tdt_tm(88, 0, 10), 'd')) + append(&items, tdt_item(&f, tdt_tm(88, 10, 10), 'e')) + append(&items, tdt_item(&f, tdt_tm(88, 20, 10), 'f')) + tdt_show(td, &f, items[:], 1) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abc\ndef") +} + +// The device reads the matrices, not Text_Item.advance, precisely so that it +// cannot be desynchronised from where the glyphs actually landed. +@(test) +test_text_device_ignores_the_advance_field :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 8) + defer delete(items) + xs := [8]f64{0, 5, 10, 18, 23, 28, 33, 38} + word := "thequick" + for i in 0 ..< 8 { + it := tdt_item(&f, tdt_tm(xs[i], 100, 10), rune(word[i])) + it.advance = f64(i) * -1e6 // nonsense, in nonsense units + append(&items, it) + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "the quick") +} + +// Content streams are the most malformed data in a PDF. None of this may +// crash, hang or leak, and whatever is still readable must still be read. +@(test) +test_text_device_survives_degenerate_input :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + dev := text_device_as_device(td) + + stroke := STROKE_STATE_DEFAULT + dev_fill_text(dev, nil, MATRIX_IDENTITY, Paint{}) + dev_stroke_text(dev, nil, &stroke, MATRIX_IDENTITY, Paint{}) + dev_ignore_text(dev, nil, MATRIX_IDENTITY) + + empty := text_make() + defer text_destroy(&empty) + dev_fill_text(dev, &empty, MATRIX_IDENTITY, Paint{}) + + // A run with no font at all: font_matrix falls back, widths are zero. + nofont := text_make() + defer text_destroy(&nofont) + append(&nofont.items, Text_Item{trm = Matrix{0.01, 0, 0, -0.01, 0, 0}, unicode = 'Z'}) + dev_fill_text(dev, &nofont, MATRIX_IDENTITY, Paint{}) + + bad := text_make() + defer text_destroy(&bad) + bad.font = &f + nan := math.QNAN_F64 + inf := math.INF_F64 + append(&bad.items, Text_Item{trm = Matrix{nan, 0, 0, nan, 0, 0}, unicode = 'a'}) + append(&bad.items, Text_Item{trm = Matrix{1, 0, 0, 1, inf, 0}, unicode = 'b'}) + append(&bad.items, Text_Item{trm = Matrix{0, 0, 0, 0, 5, 5}, unicode = 'c'}) + append(&bad.items, tdt_item(&f, tdt_tm(0, 100, 10), 'K')) + dev_fill_text(dev, &bad, MATRIX_IDENTITY, Paint{}) + + s := text_device_result(td) + defer delete(s) + // The one placeable glyph in that soup is the one that comes back. + testing.expect(t, strings.contains(s, "K"), "a readable glyph must survive its neighbours") + testing.expect(t, !strings.contains(s, "a"), "a NaN matrix places nothing") + testing.expect(t, !strings.contains(s, "b"), "an infinite position places nothing") + testing.expect(t, !strings.contains(s, "c"), "a collapsed matrix places nothing") + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect(t, len(blocks) >= 1, "") + for b in blocks { + testing.expect(t, rect_is_finite(b.rect), "no NaN may reach a caller's rectangle") + for l in b.lines { + testing.expect(t, rect_is_finite(l.rect), "") + for sp in l.spans { + testing.expect(t, rect_is_finite(sp.rect) && is_finite(sp.size), "") + } + } + } +} + +// Both outputs are rendered from one layout, so they cannot disagree about +// what the page says. +@(test) +test_text_device_result_and_blocks_agree :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "alpha", 0, 100, 10, 5) + tdt_run(td, &f, "beta", 0, 112, 10, 5) + tdt_run(td, &f, "gamma", 0, 160, 10, 5) + + s := text_device_result(td) + defer delete(s) + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + + b := strings.builder_make() + defer strings.builder_destroy(&b) + for blk, bi in blocks { + if bi > 0 { strings.write_string(&b, "\n\n") } + for ln, li in blk.lines { + if li > 0 { strings.write_byte(&b, '\n') } + for sp in ln.spans { strings.write_string(&b, sp.text) } + } + } + testing.expect_value(t, s, strings.to_string(b)) + testing.expect_value(t, s, "alpha\nbeta\n\ngamma") +} + +// Extracting twice must give the same answer: nothing is consumed. +@(test) +test_text_device_result_is_repeatable :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + tdt_run(td, &f, "twice", 0, 100, 10, 5) + + a := text_device_result(td) + defer delete(a) + b := text_device_result(td) + defer delete(b) + testing.expect_value(t, a, b) + testing.expect_value(t, a, "twice") +} + +// Text_Item.trm is contracted to map glyph space to device space. A producer +// of Text_Items that handed over the text-space matrix instead would leave +// every em a thousand times too large, and the whole page would collapse into +// one line with no spaces in it. The guard is a page whose text cannot +// possibly fit inside one em. +@(test) +test_text_device_rescales_impossible_matrices :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + word := "abcdefghij klmnopqrst" + items := make([dynamic]Text_Item, 0, len(word)) + defer delete(items) + x := 0.0 + for i in 0 ..< len(word) { + if word[i] == ' ' { + x += 3 + continue + } + // Note: no font matrix folded in, which is the mistake being guarded. + append(&items, Text_Item{ + trm = tdt_tm(x, 100, 10), + code = 65, + cid = 65, + unicode = rune(word[i]), + }) + x += 5 + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abcdefghij klmnopqrst") +} + +// Trailing space glyphs are producer noise; a space glyph at the start of a +// line carries nothing the rectangle does not. +@(test) +test_text_device_trims_edge_spaces :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, " hi ", 0, 100, 10, 5) + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "hi") +} + +// A run that is emitted twice with a gap between the two halves still reads +// as one line: runs are joined by position, not by operator. +@(test) +test_text_device_joins_runs_on_one_baseline :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + tdt_run(td, &f, "one", 0, 100, 10, 5) + tdt_run(td, &f, "two", 15, 100, 10, 5) // butts straight on + tdt_run(td, &f, "three", 33, 100, 10, 5) // 3 units clear: a word break + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "onetwo three") +} + +// A text matrix with a flattened axis -- a producer hiding text, or a scale +// that rounded to zero -- still says where every glyph is. The surviving axis +// stands in for the other rather than the run being thrown away. +@(test) +test_text_device_flattened_matrix_still_reads :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 4) + defer delete(items) + word := "flat" + for i in 0 ..< len(word) { + // No y axis at all: {size, 0, 0, 0, x, y}. + append(&items, tdt_item(&f, Matrix{10, 0, 0, 0, f64(i) * 5, 100}, rune(word[i]))) + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "flat") + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect_value(t, len(blocks), 1) + // The borrowed axis has to give the glyph box a real height, or a + // selection rectangle is a zero-height line. + r := blocks[0].lines[0].spans[0].rect + testing.expect(t, rect_height(r) > 5, "the em box must survive a flattened matrix") + testing.expect(t, rect_width(r) > 15, "") +} + +// Fake italics are a shear in the text matrix: the y axis leans but the +// baseline does not. Lines still group by their baselines. +@(test) +test_text_device_sheared_matrix_reads_in_order :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + // A hard lean, and lines long enough that measuring baselines along the + // leaning y axis rather than along the true normal would tear each line + // into pieces. + shear :: proc(x, y: f64) -> Matrix { return Matrix{10, 0, 8, -10, x, y} } + items := make([dynamic]Text_Item, 0, 20) + defer delete(items) + top := "abcdefghij" + bot := "klmnopqrst" + for i in 0 ..< len(top) { + append(&items, tdt_item(&f, shear(f64(i) * 5, 100), rune(top[i]))) + } + for i in 0 ..< len(bot) { + append(&items, tdt_item(&f, shear(f64(i) * 5, 112), rune(bot[i]))) + } + tdt_show(td, &f, items[:]) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "abcdefghij\nklmnopqrst") +} + +// The glyph budget is a bound on what a hostile content stream can make the +// device hold, and it has to bite without losing what came before it. +@(test) +test_text_device_glyph_budget_bites :: proc(t: ^testing.T) { + f: Font + tdt_font_simple(&f) + td, _ := text_device_make() + defer text_device_destroy(td) + + items := make([dynamic]Text_Item, 0, 1000) + defer delete(items) + for line in 0 ..< 400 { + clear(&items) + for i in 0 ..< 1000 { + append(&items, tdt_item(&f, tdt_tm(f64(i) * 5, f64(line) * 12, 10), 'x')) + } + tdt_show(td, &f, items[:]) + if td.truncated { + break + } + } + testing.expect(t, td.truncated, "400 000 glyphs must not all be kept") + testing.expect_value(t, len(td.glyphs), MAX_TEXT_GLYPHS) + + blocks := text_device_blocks(td) + defer text_blocks_destroy(blocks) + testing.expect(t, len(blocks) > 0, "what was collected before the budget ran out is still text") +} + +// --------------------------------------------------------------------------- +// End to end, through the real interpreter +// +// Everything above hands the device Text runs built by hand. These run actual +// content streams through content.odin, which is where the two files' readings +// of `Text_Item.trm` have to agree: the interpreter folds the font matrix into +// it and this device divides that back out, and if either changed its mind +// every threshold here would be a thousand times off. +// +// The fixture font is half an em wide for every code -- /BaseFont is not one +// of the standard 14, so no AFM metric can get in ahead of /MissingWidth -- +// which makes every expected position arithmetic at 10 point. +// --------------------------------------------------------------------------- + +@(private = "file") +TDT_FONT :: "<< /Type /Font /Subtype /Type1 /BaseFont /Halfwidth " + + "/FontDescriptor << /Flags 32 /MissingWidth 500 >> >>" + +@(private = "file") +TDT_TYPE3 :: "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " + + "/FontBBox [0 0 100 100] /FirstChar 65 /LastChar 66 /Widths [100 100] " + + "/Encoding << /Type /Encoding /Differences [65 /A 66 /B] >> >>" + +// A /ToUnicode that maps one code to a ligature's two letters and another to +// a single character, so that the multi-character path and the ordinary one +// are both live in the same font. +@(private = "file") +TDT_TOUNICODE :: `begincmap +1 begincodespacerange <00> endcodespacerange +2 beginbfchar +<41> <00660069> +<42> <0058> +endbfchar +endcmap` + +@(private = "file") +tdt_resources :: proc(doc: ^Document) -> ^Dict { + res := new_dict(doc.allocator) + fonts := new_dict(doc.allocator) + dict_set(fonts, "F1", cf_parse(doc, TDT_FONT)) + + if lig, ok := to_dict(cf_parse(doc, TDT_FONT)); ok { + dict_set(lig, "ToUnicode", cf_stream(doc, "<< >>", transmute([]byte)string(TDT_TOUNICODE))) + dict_set(fonts, "FL", lig) + } + + t3 := cf_parse(doc, TDT_TYPE3) + if t3d, ok := to_dict(t3); ok { + procs := new_dict(doc.allocator) + // A whole-em square, declared with d0 the way a Type 3 glyph must. + dict_set(procs, "A", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 d0 0 0 100 100 re f"))) + dict_set(procs, "B", cf_stream(doc, "<< >>", + transmute([]byte)string("100 0 d0"))) + dict_set(t3d, "CharProcs", procs) + } + dict_set(fonts, "T3", t3) + dict_set(res, "Font", fonts) + return res +} + +// tdt_extract runs a content stream under the page flip a real render installs. +@(private = "file") +tdt_extract :: proc(t: ^testing.T, doc: ^Document, src: string) -> string { + td, err := text_device_make() + if err != .None || td == nil { + testing.expect(t, false, "text device") + return "" + } + defer text_device_destroy(td) + flip := Matrix{1, 0, 0, -1, 0, 792} + rerr := run_content(doc, text_device_as_device(td), transmute([]byte)src, + tdt_resources(doc), flip) + testing.expect_value(t, rerr, Error.None) + return text_device_result(td) +} + +@(test) +test_text_device_end_to_end_lines :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + s := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td (Hello) Tj 0 -12 Td (World) Tj ET") + defer delete(s) + testing.expect_value(t, s, "Hello\nWorld") + + // Four lines of leading is a paragraph break, not a line break. + p := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td (one) Tj 0 -40 Td (two) Tj ET") + defer delete(p) + testing.expect_value(t, p, "one\n\ntwo") +} + +// TJ's numbers are thousandths of an em, subtracted from the pen. A -250 is a +// word space nobody wrote; a -80 is kerning. +@(test) +test_text_device_end_to_end_tj_spacing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + s := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td [(the) -250 (quick) -80 (brown)] TJ ET") + defer delete(s) + testing.expect_value(t, s, "the quickbrown") +} + +// Render mode 3 is the OCR layer under a scan. +@(test) +test_text_device_end_to_end_invisible :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + s := tdt_extract(t, doc, "BT /F1 10 Tf 3 Tr 100 700 Td (scanned) Tj ET") + defer delete(s) + testing.expect_value(t, s, "scanned") +} + +// Modes 2 and 4 to 6 reach the device twice: once to be painted and once as a +// clip. The page still says it once. +@(test) +test_text_device_end_to_end_clipped_text_is_not_doubled :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + for mode in ([]string{"2", "4", "5", "6", "7"}) { + src := strings.concatenate({"BT /F1 10 Tf ", mode, " Tr 100 700 Td (clip) Tj ET"}) + defer delete(src) + s := tdt_extract(t, doc, src) + defer delete(s) + testing.expectf(t, s == "clip", "render mode %s extracted %q", mode, s) + } +} + +// Tc widens every gap in the run; Tw widens the one after a space glyph. The +// first must not become spaces and the second must not become two. +@(test) +test_text_device_end_to_end_char_and_word_spacing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 3 points of tracking at 10 point: 0.3 em, over the space threshold. + a := tdt_extract(t, doc, "BT /F1 10 Tf 3 Tc 100 700 Td (SPACED) Tj ET") + defer delete(a) + testing.expect_value(t, a, "SPACED") + + // 5 points of word spacing, on top of the space glyph's own 5. + b := tdt_extract(t, doc, "BT /F1 10 Tf 5 Tw 100 700 Td (a b) Tj ET") + defer delete(b) + testing.expect_value(t, b, "a b") +} + +// A Type 3 glyph is a content stream, so it never reaches a text hook as an +// outline -- the interpreter hands the run over separately, which is the only +// reason a Type 3 page extracts at all. Its widths are in the font's own +// glyph space and its /FontMatrix is the only thing that says how wide an em +// is: read as thousandths, these two glyphs would be a tenth of the size and +// the line would come out as "A B". +@(test) +test_text_device_end_to_end_type3 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + s := tdt_extract(t, doc, "BT /T3 10 Tf 100 700 Td (AB) Tj ET") + defer delete(s) + testing.expect_value(t, s, "AB") +} + +// A page rotated 90 degrees puts the rotation in the CTM, and every glyph +// matrix carries it. Reading order comes out of the matrices, so the text is +// the same text. +@(test) +test_text_device_end_to_end_rotated_page :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + td, _ := text_device_make() + defer text_device_destroy(td) + // The base matrix a viewer builds for /Rotate 90: flip y, then turn. + ctm := matrix_mul(Matrix{1, 0, 0, -1, 0, 792}, rotate_matrix(90)) + err := run_content(doc, text_device_as_device(td), + transmute([]byte)string("BT /F1 10 Tf 100 700 Td (Hello) Tj 0 -12 Td (World) Tj ET"), + tdt_resources(doc), ctm) + testing.expect_value(t, err, Error.None) + + s := text_device_result(td) + defer delete(s) + testing.expect_value(t, s, "Hello\nWorld") +} + +// A ligature glyph stands for several characters, and /ToUnicode is where the +// producer says so. Reporting only the first of them is how "Specification" +// extracts as "Specifcation" -- which is what the corpus does when this is +// missing. +@(test) +test_text_device_end_to_end_ligature :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + s := tdt_extract(t, doc, "BT /FL 10 Tf 100 700 Td (ABA) Tj ET") + defer delete(s) + testing.expect_value(t, s, "fiXfi") +} diff --git a/docs/build-plan.md b/docs/build-plan.md new file mode 100644 index 0000000..8c45c07 --- /dev/null +++ b/docs/build-plan.md @@ -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 diff --git a/docs/corpus-notes.md b/docs/corpus-notes.md new file mode 100644 index 0000000..c6c5d13 --- /dev/null +++ b/docs/corpus-notes.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. diff --git a/docs/render-layer-contract.md b/docs/render-layer-contract.md new file mode 100644 index 0000000..4921682 --- /dev/null +++ b/docs/render-layer-contract.md @@ -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) +``` diff --git a/document.odin b/document.odin new file mode 100644 index 0000000..b559c22 --- /dev/null +++ b/document.odin @@ -0,0 +1,552 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:os" +import "core:slice" +import "core:strings" + +// --------------------------------------------------------------------------- +// Limits +// +// Every one of these bounds something an attacker (or a fuzzer, or a merely +// broken producer) controls. They are deliberately generous -- real documents +// stay far below them -- and exist only so that a malformed file fails instead +// of allocating the machine to death or spinning forever. +// --------------------------------------------------------------------------- + +// Nested arrays and dictionaries, and form-XObject recursion. +MAX_DEPTH :: 32 +// Object numbers above this are rejected outright (ISO 32000-2, 7.5.4 caps +// them at 8388607 for a reason: the xref table is indexed by them). +MAX_OBJECT_NUMBER :: 8388607 +// /Prev hops through the cross-reference chain. +MAX_XREF_CHAIN :: 64 +// Operands stacked before a content operator; the format never needs more +// than 8 apart from `d0`/`d1` and the shading operators. +MAX_CONTENT_OPERANDS :: 32 +// 268M pixels, about 16k x 16k. Above this `render_page` refuses. +MAX_BITMAP_PIXELS :: 1 << 28 +// Ref chains: "1 0 R" pointing at "2 0 R" pointing at ... Legal, but only +// ever seen in files trying to make a reader loop. +MAX_REF_CHAIN :: 32 +// How deeply one object's definition may require another's before the library +// stops following. This is *nesting*, not chaining: it bounds only the cases +// where parsing object A cannot finish until object B is parsed -- an indirect +// /Length, an object stream's container, an indirect /Filter. Legitimate files +// nest two or three deep. Without this bound a file can name an acyclic chain +// of 100000 such objects and overflow the native stack, which the cycle +// guards cannot catch because no object number ever repeats. +MAX_LOAD_DEPTH :: 64 +// Bytes an object stream is allowed to expand to. +MAX_OBJSTM_SIZE :: 1 << 30 +// Repairs recorded before the library stops bothering; a file this broken is +// not going to be diagnosed by reading more of the list. +MAX_REPAIRS :: 4096 +// Pages one document may contain. The largest real documents run to a few +// tens of thousands; a /Kids graph that yields more than this is a file +// trying to make the page-tree walk allocate without end. +MAX_PAGES :: 1 << 20 + +// --------------------------------------------------------------------------- +// Cross-reference table +// +// Declared here rather than in xref.odin because the table is the Document's +// own state; xref.odin holds the algorithms that fill it in. +// --------------------------------------------------------------------------- + +Xref_Kind :: enum u8 { + // Never defined, or on the free list. Reads as null. + Free = 0, + // Lives at `offset` in the file as "num gen obj ... endobj". + Normal, + // Lives inside object stream `container` at index `index`. + Compressed, +} + +Xref_Entry :: struct { + offset: i64, // Normal: byte offset in the file + container: u32, // Compressed: object number of the /ObjStm + index: u32, // Compressed: position within that stream + gen: u16, + kind: Xref_Kind, + // Set once the entry has been read and its object cached, so a second + // lookup skips straight to the cache. + cached: bool, +} + +// --------------------------------------------------------------------------- +// Document +// --------------------------------------------------------------------------- + +Open_Options :: struct { + // User or owner password for an encrypted document. Empty is correct for + // the common case of a file encrypted with an empty user password -- which + // is what "restricted permissions, opens without prompting" means. + password: string, + // When true the Document takes ownership of the byte slice handed to + // `open_bytes` and frees it in `close`. `open_file` sets this. + take_ownership: bool, + // Skip the page-tree walk. Useful for tools that only want to inspect + // objects, and for opening documents whose page tree is broken. + skip_page_tree: bool, + // Allocator for the Document handle itself, and for the file bytes when + // `open_file` reads them. + // + // It is deliberately NOT where parsed objects come from: those live in a + // growing virtual-memory arena that takes pages from the OS directly, so + // that a large document costs one address-space reservation rather than + // thousands of small allocations. A caller that needs to account for a + // document's real footprint should read `document_memory_used`, not + // instrument this allocator. + allocator: mem.Allocator, +} + +Document :: struct { + // The whole file. Object offsets index into this directly, and most + // strings and stream payloads are sub-slices of it rather than copies. + data: []byte, + owns_data: bool, + backing: mem.Allocator, + + // Everything parsed out of the file lives here and dies with `close`. + arena: virtual.Arena, + allocator: mem.Allocator, + + version_major: int, + version_minor: int, + // Byte position of "%PDF-". Files with junk prepended (a mail header, an + // HTTP response, a self-extracting wrapper) are common, and every offset + // inside such a file is relative to this, not to zero. + header_offset: i64, + + xref: [dynamic]Xref_Entry, + trailer: ^Dict, + catalog: ^Dict, + + // Resolved objects, keyed by object number. Generation is not part of the + // key: a file never has two live generations of the same number, and the + // ones that try are broken in ways the generation check makes worse. + cache: map[u32]Object, + // Objects currently being parsed, to break reference cycles that would + // otherwise recurse until the stack ends. + loading: map[u32]bool, + // Decoded object streams, keyed by their object number. + objstm_cache: map[u32]^Objstm, + + // Colour spaces and functions built from this file, keyed by the address + // of the object they were built from. A page that names the same space in + // four hundred XObjects, or a shading that names one tint transform from + // every pattern that uses it, must build each of them once. Both are + // filled in lazily and, like everything else the Document owns, die with + // its arena. + cs_cache: map[uintptr]^Colorspace, + fn_cache: map[uintptr]^Function, + // Fonts, keyed the same way, by the address of the font dictionary. A + // page that selects the same font two hundred times must build it once, + // and building one means parsing an embedded font program. + font_cache: map[uintptr]^Font, + + // Images read from this file, and the decoded pixels of the ones most + // recently painted. Unlike the two caches above this one has a byte + // budget and evicts, because a decoded photograph is megabytes and a + // catalogue has hundreds of them; see image.odin. + img_cache: Image_Cache, + + encrypt: ^Encryption, + + // Page dictionaries in document order, flattened from the page tree. + pages: [dynamic]^Dict, + page_refs: [dynamic]Ref, + + repairs: [dynamic]Repair, + // True once anything was worked around. Cheaper to test than len(repairs). + is_damaged: bool, +} + +// --------------------------------------------------------------------------- +// Opening and closing +// --------------------------------------------------------------------------- + +// open_file reads a PDF from disk. The Document owns the bytes. +open_file :: proc(path: string, options := Open_Options{}) -> (doc: ^Document, err: Error) { + opts := options + if opts.allocator.procedure == nil { + opts.allocator = context.allocator + } + data, read_err := os.read_entire_file(path, opts.allocator) + if read_err != nil { + return nil, .Cant_Open_File + } + opts.take_ownership = true + return open_bytes(data, opts) +} + +// open_bytes reads a PDF already in memory. +// +// Unless `options.take_ownership` is set the slice is borrowed, not copied, and +// must outlive the Document -- object offsets, string values and stream +// payloads all point into it. +open_bytes :: proc(data: []byte, options := Open_Options{}) -> (doc: ^Document, err: Error) { + opts := options + if opts.allocator.procedure == nil { + opts.allocator = context.allocator + } + if len(data) == 0 { + return nil, .Not_A_PDF + } + + doc = new(Document, opts.allocator) + doc.data = data + doc.owns_data = opts.take_ownership + doc.backing = opts.allocator + + if arena_err := virtual.arena_init_growing(&doc.arena); arena_err != nil { + // `close` cannot run yet -- there is no arena for it to destroy -- so + // the bytes we took ownership of have to be released here. + if doc.owns_data { + delete(doc.data, opts.allocator) + } + free(doc, opts.allocator) + return nil, .Out_Of_Memory + } + doc.allocator = virtual.arena_allocator(&doc.arena) + + doc.xref = make([dynamic]Xref_Entry, doc.allocator) + doc.cache = make(map[u32]Object, 64, doc.allocator) + doc.loading = make(map[u32]bool, 16, doc.allocator) + doc.objstm_cache = make(map[u32]^Objstm, 8, doc.allocator) + doc.pages = make([dynamic]^Dict, doc.allocator) + doc.page_refs = make([dynamic]Ref, doc.allocator) + doc.repairs = make([dynamic]Repair, doc.allocator) + + // A missing header is not fatal on its own: enough files in the wild have + // one that a reader which insists on it is less useful than one that goes + // looking for objects. + read_header(doc) + + if xerr := xref_load(doc); xerr != .None { + close(doc) + return nil, xerr + } + + if cerr := crypt_init(doc, transmute([]byte)opts.password); cerr != .None { + close(doc) + return nil, cerr + } + + if derr := find_catalog(doc); derr != .None { + close(doc) + return nil, derr + } + + if !opts.skip_page_tree { + if perr := pagetree_build(doc); perr != .None { + close(doc) + return nil, perr + } + } + + return doc, .None +} + +// close releases everything the Document owns. The Document pointer, every +// object reachable from it, and (if it was taken) the file's byte slice are all +// invalid afterwards. +close :: proc(doc: ^Document) { + if doc == nil { return } + crypt_destroy(doc) + // Decoded image pixels are the one thing the arena does not own: they have + // to be individually freeable for the cache's budget to mean anything. + image_cache_release(doc) + virtual.arena_destroy(&doc.arena) + if doc.owns_data && doc.data != nil { + delete(doc.data, doc.backing) + } + backing := doc.backing + free(doc, backing) +} + +// read_header locates "%PDF-M.N" and records the version and the offset +// everything else in the file is measured from. +@(private) +read_header :: proc(doc: ^Document) { + // The header is required to be at byte 0, but junk in front of it is + // ordinary enough that viewers all scan a window for it. 1 KiB matches + // what the format's own conformance guidance allows for. + limit := min(len(doc.data), 4096) + idx := strings.index(string(doc.data[:limit]), "%PDF-") + if idx < 0 { + // Last resort: some generators emit "%!PS-Adobe" wrappers, and some + // emit nothing at all. Leave the offset at zero and let the xref + // rebuild find objects by scanning. + doc.version_major = 1 + doc.version_minor = 7 + return + } + + doc.header_offset = i64(idx) + v := doc.data[idx + 5:] + doc.version_major = 1 + doc.version_minor = 7 + if len(v) >= 3 && v[0] >= '0' && v[0] <= '9' && v[1] == '.' && v[2] >= '0' && v[2] <= '9' { + doc.version_major = int(v[0] - '0') + doc.version_minor = int(v[2] - '0') + } +} + +// document_version returns the version from the header, upgraded by the +// catalog's /Version entry when that is newer (ISO 32000-2, 7.5.5: a file may +// be upgraded by an incremental update without rewriting the header). +document_version :: proc(doc: ^Document) -> (major, minor: int) { + major, minor = doc.version_major, doc.version_minor + if v := dict_name(doc, doc.catalog, "Version"); len(v) >= 3 && v[1] == '.' && + v[0] >= '0' && v[0] <= '9' && v[2] >= '0' && v[2] <= '9' { + cm := int(v[0] - '0') + cn := int(v[2] - '0') + if cm > major || (cm == major && cn > minor) { + major, minor = cm, cn + } + } + return +} + +// --------------------------------------------------------------------------- +// Object access +// --------------------------------------------------------------------------- + +// resolve follows indirect references until it reaches a direct object. +// +// This is the single most-called procedure in the library: nearly every value +// read out of a dictionary is allowed to be a reference, and callers that skip +// it work on most files and mysteriously fail on the rest. A nil Document +// makes it a no-op, which is what the standalone object parser wants. +resolve :: proc(doc: ^Document, o: Object) -> Object { + if doc == nil { return o } + obj := o + for i in 0 ..< MAX_REF_CHAIN { + ref, is_ref := obj.(Ref) + if !is_ref { return obj } + obj = get_object(doc, ref) + _ = i + } + // A chain this long is a loop. Null is what a viewer shows. + return nil +} + +// get_object loads one indirect object, from the cache if it is there. +// +// A reference to an object that does not exist is null, not an error: the +// format says so (ISO 32000-2, 7.3.10) and files rely on it. +get_object :: proc(doc: ^Document, ref: Ref) -> Object { + if doc == nil || ref.num == 0 || ref.num > MAX_OBJECT_NUMBER { + return nil + } + if cached, ok := doc.cache[ref.num]; ok { + return cached + } + // Already being parsed further up the stack: an object whose own + // definition refers back to it. Break the cycle with null. + if doc.loading[ref.num] or_else false { + add_repair(doc, .Missing_Object, 0, ref) + return nil + } + // `loading` holds exactly the objects whose parses are in flight, so its + // size is the current nesting depth. Bounding it stops an acyclic chain -- + // object 2's /Length pointing at object 3's, and so on for thousands of + // distinct objects -- from walking the native stack off its end. The + // cycle guard above cannot see such a chain, because no object number + // ever repeats. + if len(doc.loading) >= MAX_LOAD_DEPTH { + add_repair(doc, .Recursion_Limit_Hit, 0, ref) + return nil + } + if int(ref.num) >= len(doc.xref) { + return nil + } + + entry := doc.xref[ref.num] + doc.loading[ref.num] = true + defer delete_key(&doc.loading, ref.num) + + obj: Object + switch entry.kind { + case .Free: + obj = nil + case .Normal: + parsed, got, err := parse_indirect_object_at(doc, entry.offset, Ref{ref.num, entry.gen}) + if err == .None { + // An xref pointing at the wrong object is a classic symptom of a + // file that was edited without its table being updated. Take what + // is actually there, but only if the number matches -- taking a + // different object's body would be worse than nothing. + if got.num == ref.num { + obj = parsed + } else { + add_repair(doc, .Xref_Entry_Mismatch, entry.offset, ref) + obj = nil + } + } else { + add_repair(doc, .Missing_Object, entry.offset, ref) + } + case .Compressed: + parsed, err := objstm_get_object(doc, entry.container, entry.index, ref.num) + if err == .None { + obj = parsed + } else { + add_repair(doc, .Missing_Object, 0, ref) + } + } + + doc.cache[ref.num] = obj + return obj +} + +// get_dict, get_array, get_stream are the typed forms of get_object, for the +// many call sites that know what they are looking for. +get_dict :: proc(doc: ^Document, ref: Ref) -> ^Dict { + d, _ := to_dict(get_object(doc, ref)) + return d +} + +get_array :: proc(doc: ^Document, ref: Ref) -> ^Array { + a, _ := to_array(get_object(doc, ref)) + return a +} + +get_stream :: proc(doc: ^Document, ref: Ref) -> ^Stream { + s, _ := to_stream(get_object(doc, ref)) + return s +} + +// document_memory_used is how many bytes the Document's arena has handed out: +// every object, dictionary, stream payload and decoded buffer parsed so far. +// It grows as pages are visited, since streams decode lazily, and drops to +// zero only at `close`. +document_memory_used :: proc(doc: ^Document) -> int { + if doc == nil { return 0 } + return int(doc.arena.total_used) +} + +// document_memory_reserved is the address space the arena holds. On a 64-bit +// system this is reserved, not committed, so it is much larger than the +// resident cost and is not the number to budget against. +document_memory_reserved :: proc(doc: ^Document) -> int { + if doc == nil { return 0 } + return int(doc.arena.total_reserved) +} + +// object_count is the highest object number the file defines, plus one. +object_count :: proc(doc: ^Document) -> int { + return len(doc.xref) +} + +// --------------------------------------------------------------------------- +// Catalog +// --------------------------------------------------------------------------- + +@(private) +find_catalog :: proc(doc: ^Document) -> Error { + if root := dict_dict(doc, doc.trailer, "Root"); root != nil { + // Trust it only if it looks like a catalog. Some files have a /Root + // pointing at a page or at a free object after a bad incremental + // update, and scanning finds the real one. + if dict_type(doc, root) == "Catalog" || dict_has(root, "Pages") { + doc.catalog = root + return .None + } + } + + // Scan every object for /Type /Catalog. Costly, but only reached on files + // whose trailer is unusable, and the alternative is refusing to open them. + for num in 1 ..< len(doc.xref) { + obj := get_object(doc, Ref{u32(num), doc.xref[num].gen}) + d, ok := to_dict(obj) + if !ok { continue } + if dict_type(doc, d) == "Catalog" { + doc.catalog = d + add_repair(doc, .Missing_Root, 0, Ref{u32(num), 0}) + return .None + } + } + + // A file with no catalog can still have pages; pagetree_build falls back to + // scanning for /Type /Page. Report the damage and let it try. + add_repair(doc, .Missing_Root, 0, {}) + return .None +} + +// --------------------------------------------------------------------------- +// Repairs +// --------------------------------------------------------------------------- + +add_repair :: proc(doc: ^Document, kind: Repair_Kind, offset: i64, ref := Ref{}) { + if doc == nil { return } + doc.is_damaged = true + if len(doc.repairs) >= MAX_REPAIRS { return } + append(&doc.repairs, Repair{kind = kind, offset = offset, ref = ref}) +} + +// --------------------------------------------------------------------------- +// Byte-slice helpers used across the reader +// --------------------------------------------------------------------------- + +// slice_at returns doc.data[offset:] with the offset clamped, so a garbage +// offset out of a broken xref yields an empty slice instead of a panic. +@(private) +slice_at :: proc(doc: ^Document, offset: i64) -> []byte { + if offset < 0 || offset >= i64(len(doc.data)) { + return nil + } + return doc.data[offset:] +} + +// arena_bytes copies `src` into the Document's arena. Used when a value cannot +// borrow from `doc.data` -- decrypted strings, decoded streams, anything +// synthesised. +@(private) +arena_bytes :: proc(doc: ^Document, src: []byte) -> []byte { + if len(src) == 0 { return nil } + dst := make([]byte, len(src), doc.allocator) + copy(dst, src) + return dst +} + +@(private) +arena_string :: proc(doc: ^Document, src: string) -> string { + return string(arena_bytes(doc, transmute([]byte)src)) +} + +// index_bytes finds `needle` in `haystack`, or -1. +@(private) +index_bytes :: proc(haystack, needle: []byte) -> int { + return slice_indexof_bytes(haystack, needle) +} + +@(private) +slice_indexof_bytes :: proc(haystack, needle: []byte) -> int { + if len(needle) == 0 { return 0 } + if len(needle) > len(haystack) { return -1 } + first := needle[0] + for i := 0; i + len(needle) <= len(haystack); i += 1 { + if haystack[i] != first { continue } + if slice.equal(haystack[i:i + len(needle)], needle) { + return i + } + } + return -1 +} + +// last_index_bytes finds the last occurrence of `needle`, or -1. Used to walk +// backwards to `startxref`. +@(private) +last_index_bytes :: proc(haystack, needle: []byte) -> int { + if len(needle) == 0 { return len(haystack) } + if len(needle) > len(haystack) { return -1 } + for i := len(haystack) - len(needle); i >= 0; i -= 1 { + if slice.equal(haystack[i:i + len(needle)], needle) { + return i + } + } + return -1 +} diff --git a/document_test.odin b/document_test.odin new file mode 100644 index 0000000..60fc3fb --- /dev/null +++ b/document_test.odin @@ -0,0 +1,328 @@ +package pdf + +import "core:fmt" +import "core:os" +import "core:testing" + +// --------------------------------------------------------------------------- +// Recursion bounds +// +// The two tests below are regressions for a pair of crashes, not hypotheticals. +// Both worked the same way: an object's definition cannot be finished until +// another object is parsed, and a file can name a long chain of such objects in +// which no object number ever repeats. The cycle guards look for repetition, so +// they never fire, and the native stack runs out. `MAX_LOAD_DEPTH` bounds the +// nesting itself, which is the only thing that catches an acyclic chain. +// --------------------------------------------------------------------------- + +// Objects 3..N are streams whose /Length is an indirect reference to the next +// one, so parsing object 3 cannot finish until object N has been parsed. +// Before the depth bound this segfaulted at roughly six thousand links. +@(test) +test_indirect_length_chain_terminates :: proc(t: ^testing.T) { + N :: 8000 + + pb := pb_make() + defer pb_destroy(&pb) + + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") + for k in 3 ..< N { + pb_mark(&pb, u32(k)) + pb_num(&pb, k) + pb_str(&pb, " 0 obj\n<< /Length ") + pb_num(&pb, k + 1) + pb_str(&pb, " 0 R >>\nstream\nX\nendstream\nendobj\n") + } + pb_mark(&pb, u32(N)) + pb_num(&pb, N) + pb_str(&pb, " 0 obj\n<< /Length 1 >>\nstream\nX\nendstream\nendobj\n") + + at := pb_classic_xref(&pb) + pb_trailer(&pb, fmt.tprintf("<< /Size %d /Root 1 0 R >>", N + 1), at) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + defer close(doc) + + // Walking the chain must return, and must give up rather than pretend. + head := get_object(doc, Ref{3, 0}) + testing.expect(t, head != nil, "the head of the chain should still parse") + testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), + "hitting the nesting bound must be recorded as a repair") + + // The bound is on nesting, so only a bounded prefix of the chain is ever + // touched -- proof the walk stopped rather than merely surviving. + testing.expect(t, len(doc.cache) <= MAX_LOAD_DEPTH + 8, + "only a bounded prefix of the chain should have been parsed") +} + +// Object k's xref entry says it lives inside object stream k+1, whose own entry +// says the same of k+2, and so on. Resolving object 2 walks the whole chain. +@(test) +test_objstm_container_chain_terminates :: proc(t: ^testing.T) { + N :: 3000 + + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + + // Rows 0..N+1: 0 free, 1 the catalog, 2..N-1 compressed pointing one + // further along, N the xref stream itself. + rows := make([]Xref_Row, N + 1) + defer delete(rows) + rows[0] = Xref_Row{0, 0, 65535} + rows[1] = Xref_Row{1, u64(pb.offs[1]), 0} + for k in 2 ..< N { + rows[k] = Xref_Row{2, u64(k + 1), 0} + } + + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + at := pb_here(&pb) + rows[N] = Xref_Row{1, u64(at), 0} + // The stream object's own row has to carry its real offset, so re-encode + // now that it is known. + raw2 := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw2) + pb_stream_obj(&pb, u32(N), fmt.tprintf("/Type /XRef /Size %d /W [1 4 2] /Root 1 0 R", N + 1), raw2) + pb_startxref(&pb, at) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + defer close(doc) + + // The chain resolves to null instead of walking the stack off its end. + obj := get_object(doc, Ref{2, 0}) + testing.expect(t, obj == nil, "a container chain that goes nowhere resolves to null") + testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), + "hitting the nesting bound must be recorded as a repair") +} + +// --------------------------------------------------------------------------- +// Object access +// --------------------------------------------------------------------------- + +@(test) +test_get_object_rejects_bad_refs :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + doc, err := pb_open(&pb) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + defer close(doc) + + // Object 0 is the head of the free list and is never a real object. + testing.expect(t, get_object(doc, Ref{0, 0}) == nil, "object 0 is not an object") + testing.expect(t, get_object(doc, Ref{9999, 0}) == nil, "past the table reads as null") + testing.expect(t, get_object(doc, Ref{MAX_OBJECT_NUMBER + 1, 0}) == nil, + "an object number above the format's cap reads as null") + testing.expect(t, get_object(nil, Ref{1, 0}) == nil, "a nil document reads as null") + + // The catalog is really there, so the checks above are not passing by + // accident on a document where nothing resolves. + testing.expect(t, get_dict(doc, Ref{1, 0}) != nil, "the catalog should resolve") +} + +// A reference pointing at a reference pointing at a reference ... is legal but +// only ever written to make a reader loop. It must terminate at null. +@(test) +test_resolve_ref_chain_is_capped :: proc(t: ^testing.T) { + LINKS :: MAX_REF_CHAIN + 20 + + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") + for k in 3 ..< 3 + LINKS { + pb_obj(&pb, u32(k), fmt.tprintf("%d 0 R", k + 1)) + } + pb_obj(&pb, u32(3 + LINKS), "42") + + at := pb_classic_xref(&pb) + pb_trailer(&pb, fmt.tprintf("<< /Size %d /Root 1 0 R >>", 4 + LINKS), at) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + defer close(doc) + + testing.expect(t, resolve(doc, Ref{3, 0}) == nil, "an over-long ref chain resolves to null") + + // A chain inside the cap still resolves to its value. + short := resolve(doc, Ref{u32(2 + LINKS), 0}) + v, ok := to_int(short) + testing.expect(t, ok, "a short chain should reach the integer") + testing.expect_value(t, v, 42) +} + +// --------------------------------------------------------------------------- +// open_file, ownership and teardown +// +// open_file is the entry point a viewer actually calls and the only place the +// library frees memory it owns outside the arena, so it needs a test that does +// not depend on a corpus directory being present. +// --------------------------------------------------------------------------- + +@(test) +test_open_file_owns_and_frees_its_bytes :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + + path := "pdf_odin_open_file_test.pdf" + werr := os.write_entire_file(path, pb.buf[:]) + if !testing.expect(t, werr == nil, "could not write the temporary file") { + return + } + defer os.remove(path) + + doc, err := open_file(path) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + + testing.expect(t, doc.owns_data, "open_file must take ownership of the bytes it read") + testing.expect_value(t, page_count(doc), 1) + + // The test allocator tracks leaks, so this call is the assertion: if close + // failed to free the file buffer it owns, the suite reports it. + close(doc) +} + +@(test) +test_open_file_missing :: proc(t: ^testing.T) { + doc, err := open_file("this_file_does_not_exist_pdf_odin.pdf") + testing.expect_value(t, err, Error.Cant_Open_File) + testing.expect(t, doc == nil, "a failed open must not hand back a document") +} + +@(test) +test_open_bytes_rejects_empty :: proc(t: ^testing.T) { + doc, err := open_bytes(nil) + testing.expect_value(t, err, Error.Not_A_PDF) + testing.expect(t, doc == nil, "") +} + +// --------------------------------------------------------------------------- +// Version and header +// --------------------------------------------------------------------------- + +@(test) +test_document_version :: proc(t: ^testing.T) { + check :: proc(t: ^testing.T, header, catalog_extra: string, want_major, want_minor: int) { + pb := pb_make(header) + defer pb_destroy(&pb) + pb_obj(&pb, 1, fmt.tprintf("<< /Type /Catalog /Pages 2 0 R %s >>", catalog_extra)) + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 3 /Root 1 0 R >>", at) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + if !testing.expect(t, err == .None && doc != nil, "should open") { return } + defer close(doc) + + major, minor := document_version(doc) + testing.expect_value(t, major, want_major) + testing.expect_value(t, minor, want_minor) + } + + check(t, "%PDF-1.4\n", "", 1, 4) + // An incremental update may raise the version without rewriting the header. + check(t, "%PDF-1.4\n", "/Version /1.7", 1, 7) + // It may not lower it. + check(t, "%PDF-1.7\n", "/Version /1.4", 1, 7) + check(t, "%PDF-2.0\n", "", 2, 0) + // Garbage in /Version must be ignored, not read as arithmetic on bytes. + check(t, "%PDF-1.4\n", "/Version /XX", 1, 4) + check(t, "%PDF-1.4\n", "/Version /A.B", 1, 4) + check(t, "%PDF-1.4\n", "/Version /1", 1, 4) +} + +// Junk before %PDF- is common: a mail header, an HTTP response, a wrapper. +// Every offset in such a file is relative to the header, not to zero. +@(test) +test_header_offset :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + + JUNK :: "GARBAGE BEFORE THE HEADER\r\n" + shifted := make([dynamic]byte) + defer delete(shifted) + append(&shifted, JUNK) + append(&shifted, ..pb.buf[:]) + + doc, err := open_bytes(shifted[:], Open_Options{skip_page_tree = true}) + testing.expect_value(t, err, Error.None) + if doc == nil { return } + defer close(doc) + + testing.expect_value(t, doc.header_offset, i64(len(JUNK))) + testing.expect(t, get_dict(doc, Ref{1, 0}) != nil, "objects must still resolve") +} + +// --------------------------------------------------------------------------- +// Memory accounting +// --------------------------------------------------------------------------- + +@(test) +test_document_memory_used :: proc(t: ^testing.T) { + testing.expect_value(t, document_memory_used(nil), 0) + testing.expect_value(t, document_memory_reserved(nil), 0) + + pb := pb_sample_classic() + defer pb_destroy(&pb) + doc, err := pb_open(&pb) + if !testing.expect(t, err == .None && doc != nil, "should open") { return } + defer close(doc) + + used := document_memory_used(doc) + testing.expect(t, used > 0, "an opened document has parsed something") + testing.expect(t, document_memory_reserved(doc) >= used, "reserved covers used") + + // Parsing more must be visible, since this is the only way a caller can + // account for a document's footprint. + _ = get_dict(doc, Ref{3, 0}) + testing.expect(t, document_memory_used(doc) >= used, "usage does not go backwards") +} + +// --------------------------------------------------------------------------- +// Repairs +// --------------------------------------------------------------------------- + +@(test) +test_add_repair_is_bounded :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + doc, err := pb_open(&pb) + if !testing.expect(t, err == .None && doc != nil, "should open") { return } + defer close(doc) + + for _ in 0 ..< MAX_REPAIRS + 100 { + add_repair(doc, .Missing_Object, 0, Ref{1, 0}) + } + testing.expect(t, len(doc.repairs) <= MAX_REPAIRS, "the repair log must be capped") + testing.expect(t, doc.is_damaged, "recording a repair marks the document damaged") + + // A nil document must be safe to report against. + add_repair(nil, .Missing_Object, 0, {}) +} + +@(test) +test_repair_describe :: proc(t: ^testing.T) { + with_ref := repair_describe(Repair{kind = .Bad_Stream_Length, offset = 42, ref = Ref{7, 0}}) + defer delete(with_ref) + testing.expect(t, len(with_ref) > 0, "") + + no_ref := repair_describe(Repair{kind = .Rebuilt_Xref, offset = 9}) + defer delete(no_ref) + testing.expect(t, len(no_ref) > 0, "") + + // Every kind must have a description, so a log line never says "unknown". + for k in Repair_Kind { + testing.expect(t, repair_string(k) != "unknown repair", "every repair kind needs a description") + } + for e in Error { + testing.expect(t, error_string(e) != "unknown error", "every error needs a description") + } +} diff --git a/encoding.odin b/encoding.odin new file mode 100644 index 0000000..3b6d0f8 --- /dev/null +++ b/encoding.odin @@ -0,0 +1,673 @@ +package pdf + +import "core:bytes" +import "core:strings" +import "core:unicode/utf8" + +// Text strings, dates, and the name/number trees that hold a document's +// named destinations, embedded files and page labels. +// +// Everything here works on bytes that came out of a file, so everything here +// is written to survive nonsense: a truncated date reads as far as it can, an +// unpaired UTF-16 surrogate becomes U+FFFD rather than invalid UTF-8, and a +// tree whose /Kids point back at their parent terminates. + +// --------------------------------------------------------------------------- +// Text strings (ISO 32000-2, 7.9.2.2) +// --------------------------------------------------------------------------- + +// PDFDocEncoding as a byte -> Unicode table (ISO 32000-2, Annex D.2). +// +// It is Latin-1 apart from three regions: 0x18-0x1F, which hold the accents +// Latin-1 puts in the C1 area; 0x80-0x9F, which hold the typographic +// characters that Latin-1 has no room for; and 0xA0, which is the euro sign +// rather than a no-break space. +// +// A zero entry means "not defined by PDFDocEncoding" -- 0x00, 0x7F, 0x9F and +// 0xAD. Those decode to the byte's own value, which is what every viewer does +// and what keeps round-tripping a Latin-1 string through this table lossless. +@(private, rodata) +PDF_DOC_ENCODING := [256]rune{ + 0x18 = 0x02D8, // breve + 0x19 = 0x02C7, // caron + 0x1A = 0x02C6, // modifier letter circumflex accent + 0x1B = 0x02D9, // dot above + 0x1C = 0x02DD, // double acute accent + 0x1D = 0x02DB, // ogonek + 0x1E = 0x02DA, // ring above + 0x1F = 0x02DC, // small tilde + + 0x80 = 0x2022, // bullet + 0x81 = 0x2020, // dagger + 0x82 = 0x2021, // double dagger + 0x83 = 0x2026, // horizontal ellipsis + 0x84 = 0x2014, // em dash + 0x85 = 0x2013, // en dash + 0x86 = 0x0192, // latin small letter f with hook + 0x87 = 0x2044, // fraction slash + 0x88 = 0x2039, // single left-pointing angle quotation mark + 0x89 = 0x203A, // single right-pointing angle quotation mark + 0x8A = 0x2212, // minus sign + 0x8B = 0x2030, // per mille sign + 0x8C = 0x201E, // double low-9 quotation mark + 0x8D = 0x201C, // left double quotation mark + 0x8E = 0x201D, // right double quotation mark + 0x8F = 0x2018, // left single quotation mark + 0x90 = 0x2019, // right single quotation mark + 0x91 = 0x201A, // single low-9 quotation mark + 0x92 = 0x2122, // trade mark sign + 0x93 = 0xFB01, // latin small ligature fi + 0x94 = 0xFB02, // latin small ligature fl + 0x95 = 0x0141, // latin capital letter l with stroke + 0x96 = 0x0152, // latin capital ligature oe + 0x97 = 0x0160, // latin capital letter s with caron + 0x98 = 0x0178, // latin capital letter y with diaeresis + 0x99 = 0x017D, // latin capital letter z with caron + 0x9A = 0x0131, // latin small letter dotless i + 0x9B = 0x0142, // latin small letter l with stroke + 0x9C = 0x0153, // latin small ligature oe + 0x9D = 0x0161, // latin small letter s with caron + 0x9E = 0x017E, // latin small letter z with caron + + 0xA0 = 0x20AC, // euro sign +} + +// decode_text_string converts a PDF text string to UTF-8. +// +// A leading U+FEFF byte order mark (FE FF) means the rest is UTF-16BE; an +// EF BB BF mark means it is already UTF-8; anything else is PDFDocEncoding +// (ISO 32000-2, 7.9.2.2). An FF FE mark is not in the format, but enough +// producers emit little-endian UTF-16 with one that decoding it costs nothing +// and rescues those files -- no PDFDocEncoding text begins with "y-diaeresis, +// thorn". +// +// The result is always freshly allocated with `allocator` and owned by the +// caller. It is always valid UTF-8: a lone surrogate, a truncated UTF-16 code +// unit or a malformed UTF-8 sequence each become U+FFFD. +decode_text_string :: proc(raw: []byte, allocator := context.allocator) -> string { + // +1 so that the empty string is still a real allocation the caller can + // delete unconditionally. + b := strings.builder_make(0, len(raw) + 1, allocator) + switch { + case len(raw) >= 2 && raw[0] == 0xFE && raw[1] == 0xFF: + text_write_utf16(&b, raw[2:], true) + case len(raw) >= 2 && raw[0] == 0xFF && raw[1] == 0xFE: + text_write_utf16(&b, raw[2:], false) + case len(raw) >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF: + text_write_utf8(&b, raw[3:]) + case: + text_write_pdfdoc(&b, raw) + } + return strings.to_string(b) +} + +// decode_text_object is decode_text_string for a string object, and the form +// most callers want: /Title, /Author, /Subject and the rest are all strings +// that may be indirect. +decode_text_object :: proc(doc: ^Document, o: Object, allocator := context.allocator) -> string { + raw, _ := to_bytes(resolve(doc, o)) + return decode_text_string(raw, allocator) +} + +@(private) +text_write_pdfdoc :: proc(b: ^strings.Builder, raw: []byte) { + for c in raw { + r := PDF_DOC_ENCODING[c] + if r == 0 { + r = rune(c) + } + strings.write_rune(b, r) + } +} + +@(private) +text_write_utf8 :: proc(b: ^strings.Builder, raw: []byte) { + // Re-encoding rather than copying: the mark only claims the bytes are + // UTF-8, and a claim out of a file is not a guarantee. + for i := 0; i < len(raw); { + r, size := utf8.decode_rune(raw[i:]) + if size <= 0 { + strings.write_rune(b, utf8.RUNE_ERROR) + break + } + strings.write_rune(b, r) + i += size + } +} + +@(private) +text_write_utf16 :: proc(b: ^strings.Builder, raw: []byte, big_endian: bool) { + unit :: proc(raw: []byte, i: int, big_endian: bool) -> u16 { + hi, lo := raw[i], raw[i + 1] + if !big_endian { + hi, lo = lo, hi + } + return u16(hi) << 8 | u16(lo) + } + + i := 0 + for i + 1 < len(raw) { + u := unit(raw, i, big_endian) + i += 2 + switch { + case u >= 0xD800 && u <= 0xDBFF: + // High surrogate: only a following low surrogate completes it. + if i + 1 < len(raw) { + v := unit(raw, i, big_endian) + if v >= 0xDC00 && v <= 0xDFFF { + i += 2 + strings.write_rune(b, rune(0x10000 + (u32(u - 0xD800) << 10 | u32(v - 0xDC00)))) + continue + } + } + strings.write_rune(b, utf8.RUNE_ERROR) + case u >= 0xDC00 && u <= 0xDFFF: + // Low surrogate with nothing in front of it. + strings.write_rune(b, utf8.RUNE_ERROR) + case: + strings.write_rune(b, rune(u)) + } + } + // A trailing half code unit: the string was truncated somewhere. + if i < len(raw) { + strings.write_rune(b, utf8.RUNE_ERROR) + } +} + +// --------------------------------------------------------------------------- +// Dates (ISO 32000-2, 7.9.4) +// --------------------------------------------------------------------------- + +// Date is a decoded PDF date. Absent fields take their lowest legal value: +// month and day 1, everything else 0. +// +// `tz_hour` and `tz_minute` both carry the sign of the offset, so the offset +// from UT in minutes is always `tz_hour * 60 + tz_minute` and never needs a +// separate sign flag. They are meaningless unless `has_tz` is set -- a date +// with no offset is local time of an unknown locale, which is not the same +// thing as UT. +Date :: struct { + year, month, day: int, + hour, minute, second: int, + tz_hour, tz_minute: int, + has_tz: bool, +} + +// parse_date decodes "D:YYYYMMDDHHmmSSOHH'mm'". +// +// Every field after the year is optional and truncated dates are the norm. +// So is malformation: the "D:" prefix is missing in plenty of files, the +// apostrophes around the offset minutes are variously absent, singular or +// doubled, and a colon or a space turns up where an apostrophe belongs. All of +// that is accepted. Out-of-range fields are clamped rather than rejected, +// because a viewer showing 31 December for a date that says month 13 is more +// useful than one showing nothing. +// +// ok is false only when no four-digit year could be found at all. +parse_date :: proc(raw: []byte) -> (date: Date, ok: bool) { + // A date is a few dozen bytes. Anything longer is not a date with junk + // after it, it is a different kind of object, and scanning all of it for + // something year-shaped would only invent a date out of arbitrary digits. + scan := raw + if len(scan) > 64 { + scan = scan[:64] + } + + p := 0 + for p < len(scan) && date_is_space(scan[p]) { + p += 1 + } + if p + 1 < len(scan) && (scan[p] == 'D' || scan[p] == 'd') && scan[p + 1] == ':' { + p += 2 + } + // Producers put spaces, a second colon, or nothing at all between the + // prefix and the year; skip forward to the first run of four digits. + start := -1 + for j := p; j < len(scan); j += 1 { + if date_digit_run(scan, j) >= 4 { + start = j + break + } + } + if start < 0 { + return {}, false + } + p = start + + y, _ := date_take(scan, &p, 4) + date.year = clamp(y, 0, 9999) + date.month, date.day = 1, 1 + + if m, m_ok := date_take(scan, &p, 2); m_ok { + date.month = clamp(m, 1, 12) + if d, d_ok := date_take(scan, &p, 2); d_ok { + date.day = clamp(d, 1, date_days_in_month(date.year, date.month)) + if h, h_ok := date_take(scan, &p, 2); h_ok { + // 24 as "midnight ending the day" turns up; clamp it back + // rather than rolling the date over. + date.hour = clamp(h, 0, 23) + if mi, mi_ok := date_take(scan, &p, 2); mi_ok { + date.minute = clamp(mi, 0, 59) + if s, s_ok := date_take(scan, &p, 2); s_ok { + // A leap second (60) is clamped: nothing downstream + // has a use for it. + date.second = clamp(s, 0, 59) + } + } + } + } + } + + date_parse_offset(scan, p, &date) + return date, true +} + +@(private) +date_parse_offset :: proc(raw: []byte, from: int, date: ^Date) { + p := from + for p < len(raw) && date_is_space(raw[p]) { + p += 1 + } + if p >= len(raw) { + return + } + + sign := 0 + switch raw[p] { + case 'Z', 'z': + // Z is UT. Some producers write "Z00'00'"; a non-zero offset after a + // Z contradicts itself, so the Z wins and the digits are ignored. + date.has_tz = true + return + case '+': + sign = 1 + case '-': + sign = -1 + case: + return + } + p += 1 + + h, h_ok := date_take(raw, &p, 2) + if !h_ok { + // A sign with no hours is not an offset. + return + } + date.has_tz = true + date.tz_hour = sign * clamp(h, 0, 23) + + for p < len(raw) && (raw[p] == '\'' || raw[p] == ':' || raw[p] == ' ') { + p += 1 + } + if m, m_ok := date_take(raw, &p, 2); m_ok { + date.tz_minute = sign * clamp(m, 0, 59) + } +} + +// date_offset_minutes is the offset from UT in minutes, 0 when the date +// carries none. +date_offset_minutes :: proc(d: Date) -> int { + if !d.has_tz { + return 0 + } + return d.tz_hour * 60 + d.tz_minute +} + +@(private) +date_is_space :: proc "contextless" (c: byte) -> bool { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 0 +} + +@(private) +date_is_digit :: proc "contextless" (c: byte) -> bool { + return c >= '0' && c <= '9' +} + +// date_digit_run counts consecutive digits at `i`, stopping at 4: nothing +// here needs to tell a four-digit run from a fourteen-digit one. +@(private) +date_digit_run :: proc "contextless" (raw: []byte, i: int) -> int { + n := 0 + for j := i; j < len(raw) && n < 4; j += 1 { + if !date_is_digit(raw[j]) { + break + } + n += 1 + } + return n +} + +// date_take reads exactly `n` digits, advancing `p` only if it finds them. +// A field with one digit where two belong is treated as absent: guessing +// whether "D:2005032" means the 2nd or the 20th is worse than stopping. +@(private) +date_take :: proc(raw: []byte, p: ^int, n: int) -> (value: int, ok: bool) { + if p^ < 0 || p^ + n > len(raw) { + return 0, false + } + v := 0 + for i in 0 ..< n { + c := raw[p^ + i] + if !date_is_digit(c) { + return 0, false + } + v = v * 10 + int(c - '0') + } + p^ += n + return v, true +} + +@(private) +date_days_in_month :: proc "contextless" (year, month: int) -> int { + switch month { + case 4, 6, 9, 11: + return 30 + case 2: + leap := (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 + return leap ? 29 : 28 + } + return 31 +} + +// --------------------------------------------------------------------------- +// Name trees (ISO 32000-2, 7.9.6) and number trees (7.9.7) +// --------------------------------------------------------------------------- + +// Work budget for one traversal, counted in nodes visited plus entries +// linearly scanned. A well-formed tree of any realistic size stays far below +// it; a tree built to make a reader spin does not get to. +@(private) +MAX_TREE_STEPS :: 1 << 16 + +// Entries a single collect will return. A file claiming more than this has a +// tree that is not going to be useful to anyone. +@(private) +MAX_TREE_ENTRIES :: 1 << 20 + +@(private) +Tree_Kind :: enum u8 { + Name, + Number, +} + +// name_tree_lookup finds `key` in a name tree, or null if it is not there. +// +// `root` is the tree's root node -- the value of /Dests, /EmbeddedFiles and +// the rest of the catalog's /Names dictionary. Keys are compared by byte +// value, which is the order the format requires the /Names arrays to be in. +name_tree_lookup :: proc(doc: ^Document, root: ^Dict, key: []byte) -> Object { + path: [MAX_DEPTH]^Dict + steps := MAX_TREE_STEPS + value, _ := tree_find(doc, root, .Name, key, 0, 0, &path, &steps) + return value +} + +// number_tree_lookup finds `key` in a number tree (/PageLabels, /ParentTree), +// or null. +number_tree_lookup :: proc(doc: ^Document, root: ^Dict, key: i64) -> Object { + path: [MAX_DEPTH]^Dict + steps := MAX_TREE_STEPS + value, _ := tree_find(doc, root, .Number, nil, key, 0, &path, &steps) + return value +} + +// tree_find descends one node. +// +// `path` holds the ancestors of the current node so that a /Kids entry +// pointing back up the tree is detected; with the depth cap that is enough, +// because the walk only ever moves downwards. `steps` bounds the total work, +// which matters because a node with no /Limits has to be searched +// speculatively. +@(private) +tree_find :: proc( + doc: ^Document, node: ^Dict, kind: Tree_Kind, skey: []byte, ikey: i64, + depth: int, path: ^[MAX_DEPTH]^Dict, steps: ^int, +) -> (value: Object, found: bool) { + if node == nil || depth >= MAX_DEPTH || steps^ <= 0 { + return nil, false + } + for i in 0 ..< depth { + if path[i] == node { + return nil, false + } + } + path[depth] = node + steps^ -= 1 + + leaf := dict_array(doc, node, kind == .Name ? "Names" : "Nums") + if leaf != nil { + if v, hit := tree_search_leaf(doc, leaf, kind, skey, ikey, steps); hit { + return v, true + } + } + + kids := dict_array(doc, node, "Kids") + for i in 0 ..< array_len(kids) { + if steps^ <= 0 { + break + } + kid, is_dict := to_dict(array_get(doc, kids, i)) + if !is_dict { + continue + } + if !tree_limits_contain(doc, kid, kind, skey, ikey) { + continue + } + if v, hit := tree_find(doc, kid, kind, skey, ikey, depth + 1, path, steps); hit { + return v, true + } + } + return nil, false +} + +// tree_limits_contain tests a kid's /Limits. A node with no usable /Limits is +// searched rather than skipped: the entry is required, but files that omit it +// are readable and the alternative is losing every key below that node. +@(private) +tree_limits_contain :: proc(doc: ^Document, node: ^Dict, kind: Tree_Kind, skey: []byte, ikey: i64) -> bool { + limits := dict_array(doc, node, "Limits") + if array_len(limits) < 2 { + return true + } + if kind == .Name { + if lo, lo_ok := to_bytes(array_get(doc, limits, 0)); lo_ok && bytes.compare(skey, lo) < 0 { + return false + } + if hi, hi_ok := to_bytes(array_get(doc, limits, 1)); hi_ok && bytes.compare(skey, hi) > 0 { + return false + } + return true + } + if lo, lo_ok := to_int(array_get(doc, limits, 0)); lo_ok && ikey < lo { + return false + } + if hi, hi_ok := to_int(array_get(doc, limits, 1)); hi_ok && ikey > hi { + return false + } + return true +} + +// tree_search_leaf binary-searches a leaf's key/value pairs. +// +// The format requires the pairs to be sorted, and a binary search over a few +// thousand named destinations is the whole point of the structure. But an +// unsorted (or ragged, or partly non-string) array does occur, and a miss +// there would silently lose a destination that is plainly present, so a miss +// falls back to a linear scan paid for out of the step budget. +@(private) +tree_search_leaf :: proc( + doc: ^Document, leaf: ^Array, kind: Tree_Kind, skey: []byte, ikey: i64, steps: ^int, +) -> (value: Object, found: bool) { + n := array_len(leaf) / 2 + lo, hi := 0, n + for lo < hi { + if steps^ <= 0 { + return nil, false + } + steps^ -= 1 + mid := lo + (hi - lo) / 2 + cmp, ok := tree_compare(kind, array_get(doc, leaf, mid * 2), skey, ikey) + if !ok { + break + } + switch { + case cmp == 0: + return array_get(doc, leaf, mid * 2 + 1), true + case cmp < 0: + lo = mid + 1 + case: + hi = mid + } + } + + for i in 0 ..< n { + if steps^ <= 0 { + return nil, false + } + steps^ -= 1 + if cmp, ok := tree_compare(kind, array_get(doc, leaf, i * 2), skey, ikey); ok && cmp == 0 { + return array_get(doc, leaf, i * 2 + 1), true + } + } + return nil, false +} + +// tree_compare orders a key found in the file against the key being looked +// for. ok is false when the entry is not of the tree's key type, which the +// caller reads as "this array cannot be binary-searched". +@(private) +tree_compare :: proc(kind: Tree_Kind, entry: Object, skey: []byte, ikey: i64) -> (cmp: int, ok: bool) { + if kind == .Name { + b, is_str := to_bytes(entry) + if !is_str { + return 0, false + } + return bytes.compare(b, skey), true + } + v, is_int := to_int(entry) + if !is_int { + return 0, false + } + switch { + case v < ikey: return -1, true + case v > ikey: return +1, true + } + return 0, true +} + +@(private) +Tree_Collector :: struct { + // All nil on the counting pass. A name tree fills `keys`, a number tree + // fills `nums`; only one of the two is ever non-nil. + keys: [][]byte, + nums: []i64, + values: []Object, + n: int, + steps: int, +} + +// name_tree_collect returns every key and value in a name tree, in tree order. +// +// Both slices are allocated with `allocator` and are the caller's to free. +// The keys are *not* copies: each one points at the string bytes inside the +// Document and is only valid while that Document is open. Copy them if they +// have to outlive it. +name_tree_collect :: proc( + doc: ^Document, root: ^Dict, allocator := context.allocator, +) -> (keys: [][]byte, values: []Object) { + path: [MAX_DEPTH]^Dict + + count := Tree_Collector{steps = MAX_TREE_STEPS} + tree_walk(doc, root, .Name, &count, 0, &path) + n := min(count.n, MAX_TREE_ENTRIES) + if n == 0 { + return nil, nil + } + + keys = make([][]byte, n, allocator) + values = make([]Object, n, allocator) + fill := Tree_Collector{keys = keys, values = values, steps = MAX_TREE_STEPS} + tree_walk(doc, root, .Name, &fill, 0, &path) + return keys, values +} + +// number_tree_collect is name_tree_collect for a number tree. Same ownership +// rules; the keys are values, not pointers, so they outlive the Document. +number_tree_collect :: proc( + doc: ^Document, root: ^Dict, allocator := context.allocator, +) -> (keys: []i64, values: []Object) { + path: [MAX_DEPTH]^Dict + + count := Tree_Collector{steps = MAX_TREE_STEPS} + tree_walk(doc, root, .Number, &count, 0, &path) + n := min(count.n, MAX_TREE_ENTRIES) + if n == 0 { + return nil, nil + } + + // The walker fills a [][]byte; a number tree's keys are gathered here + // instead, out of the same values array, so only one walker exists. + nkeys := make([]i64, n, allocator) + values = make([]Object, n, allocator) + fill := Tree_Collector{nums = nkeys, values = values, steps = MAX_TREE_STEPS} + tree_walk(doc, root, .Number, &fill, 0, &path) + return nkeys, values +} + +@(private) +tree_walk :: proc( + doc: ^Document, node: ^Dict, kind: Tree_Kind, c: ^Tree_Collector, depth: int, path: ^[MAX_DEPTH]^Dict, +) { + if node == nil || depth >= MAX_DEPTH || c.steps <= 0 { + return + } + for i in 0 ..< depth { + if path[i] == node { + return + } + } + path[depth] = node + c.steps -= 1 + + leaf := dict_array(doc, node, kind == .Name ? "Names" : "Nums") + for i in 0 ..< array_len(leaf) / 2 { + if c.steps <= 0 || c.n >= MAX_TREE_ENTRIES { + return + } + c.steps -= 1 + entry := array_get(doc, leaf, i * 2) + if kind == .Name { + kb, is_str := to_bytes(entry) + if !is_str { + continue + } + if c.keys != nil && c.n < len(c.keys) { + c.keys[c.n] = kb + } + } else { + kn, is_int := to_int(entry) + if !is_int { + continue + } + if c.nums != nil && c.n < len(c.nums) { + c.nums[c.n] = kn + } + } + if c.values != nil && c.n < len(c.values) { + c.values[c.n] = array_get(doc, leaf, i * 2 + 1) + } + c.n += 1 + } + + kids := dict_array(doc, node, "Kids") + for i in 0 ..< array_len(kids) { + if c.steps <= 0 { + return + } + kid, is_dict := to_dict(array_get(doc, kids, i)) + if !is_dict { + continue + } + tree_walk(doc, kid, kind, c, depth + 1, path) + } +} diff --git a/encoding_test.odin b/encoding_test.odin new file mode 100644 index 0000000..cc9ebf2 --- /dev/null +++ b/encoding_test.odin @@ -0,0 +1,778 @@ +package pdf + +import "core:fmt" +import "core:mem" +import "core:mem/virtual" +import "core:testing" +import "core:unicode/utf8" + +// --------------------------------------------------------------------------- +// Helpers +// +// Test objects are built in a local arena so that a failing assertion cannot +// leak: the arena is destroyed on the way out whatever happened. Lookups are +// driven with a nil Document, which `dict_get` and friends degrade to direct +// access for -- the trees under test hold no indirect references. +// --------------------------------------------------------------------------- + +@(private = "file") +Fixture :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private = "file") +Pair :: struct { + key: string, + value: Object, +} + +@(private = "file") +fixture_init :: proc(f: ^Fixture) { + _ = virtual.arena_init_growing(&f.arena) + f.alloc = virtual.arena_allocator(&f.arena) +} + +@(private = "file") +fixture_destroy :: proc(f: ^Fixture) { + virtual.arena_destroy(&f.arena) +} + +@(private = "file") +str :: proc(s: string) -> Object { + return String(transmute([]byte)s) +} + +@(private = "file") +arr :: proc(f: ^Fixture, items: ..Object) -> ^Array { + a := new_array(f.alloc, max(len(items), 1)) + for it in items { + array_push(a, it) + } + return a +} + +@(private = "file") +dict :: proc(f: ^Fixture, pairs: ..Pair) -> ^Dict { + d := new_dict(f.alloc, max(len(pairs), 1)) + for p in pairs { + dict_set(d, p.key, p.value) + } + return d +} + +@(private = "file") +kv :: proc(key: string, value: Object) -> Pair { + return Pair{key, value} +} + +// --------------------------------------------------------------------------- +// decode_text_string +// --------------------------------------------------------------------------- + +@(test) +test_text_string_empty :: proc(t: ^testing.T) { + got := decode_text_string({}) + defer delete(got) + testing.expect_value(t, got, "") +} + +@(test) +test_text_string_ascii :: proc(t: ^testing.T) { + got := decode_text_string(transmute([]byte)string("Hello, PDF")) + defer delete(got) + testing.expect_value(t, got, "Hello, PDF") +} + +// The whole of PDFDocEncoding that is not Latin-1: ISO 32000-2, Annex D.2. +@(test) +test_pdfdoc_encoding_specials :: proc(t: ^testing.T) { + Case :: struct { + code: byte, + want: rune, + } + cases := []Case{ + // 0x18-0x1F: the accents Latin-1 leaves out. + {0x18, '˘'}, {0x19, 'ˇ'}, {0x1A, 'ˆ'}, {0x1B, '˙'}, + {0x1C, '�'}, {0x1D, '˛'}, {0x1E, '˚'}, {0x1F, '˜'}, + // 0x80-0x9E: the typography Latin-1 has no room for. + {0x80, '•'}, {0x81, '†'}, {0x82, '‡'}, {0x83, '…'}, + {0x84, '—'}, {0x85, '–'}, {0x86, 'ƒ'}, {0x87, '�'}, + {0x88, '‹'}, {0x89, '›'}, {0x8A, '−'}, {0x8B, '‰'}, + {0x8C, '„'}, {0x8D, '“'}, {0x8E, '�'}, {0x8F, '‘'}, + {0x90, '’'}, {0x91, '‚'}, {0x92, '™'}, {0x93, '�'}, + {0x94, 'fl'}, {0x95, '�'}, {0x96, 'Œ'}, {0x97, 'Š'}, + {0x98, 'Ÿ'}, {0x99, 'Ž'}, {0x9A, 'ı'}, {0x9B, 'ł'}, + {0x9C, 'œ'}, {0x9D, 'š'}, {0x9E, 'ž'}, + // 0xA0 is the euro sign, not a no-break space. + {0xA0, '€'}, + // The four codes PDFDocEncoding leaves undefined fall back to their + // own value, which keeps a Latin-1 round trip lossless. + {0x00, '\x00'}, {0x7F, '\x7f'}, {0x9F, '\u009f'}, {0xAD, '\u00ad'}, + // Everything else is Latin-1. + {0x01, '\x01'}, {0x41, 'A'}, {0x20, ' '}, {0xE9, 'é'}, {0xFF, 'ÿ'}, + {0xA9, '©'}, {0xB5, 'µ'}, + } + for c in cases { + raw := []byte{c.code} + got := decode_text_string(raw) + defer delete(got) + want := utf8.runes_to_string({c.want}) + defer delete(want) + if got != want { + testing.expectf(t, false, "PDFDocEncoding 0x%02X: got %q, want %q", c.code, got, want) + } + } +} + +@(test) +test_pdfdoc_encoding_round_trip :: proc(t: ^testing.T) { + // Every byte decodes to exactly one rune, and no byte is dropped. + raw := make([]byte, 256) + defer delete(raw) + for i in 0 ..< 256 { + raw[i] = byte(i) + } + got := decode_text_string(raw) + defer delete(got) + testing.expect_value(t, utf8.rune_count_in_string(got), 256) +} + +@(test) +test_text_string_utf16be :: proc(t: ^testing.T) { + Case :: struct { + name: string, + raw: []byte, + want: string, + } + cases := []Case{ + {"bom only", {0xFE, 0xFF}, ""}, + {"ascii", {0xFE, 0xFF, 0x00, 'H', 0x00, 'i'}, "Hi"}, + {"bmp", {0xFE, 0xFF, 0x00, 0xE9}, "é"}, + // U+1F600 GRINNING FACE, the canonical surrogate pair. + {"surrogate pair", {0xFE, 0xFF, 0xD8, 0x3D, 0xDE, 0x00}, "\U0001F600"}, + {"pair then text", {0xFE, 0xFF, 0xD8, 0x3D, 0xDE, 0x00, 0x00, '!'}, "\U0001F600!"}, + // A high surrogate followed by an ordinary character: the character + // survives and the surrogate becomes U+FFFD, never invalid UTF-8. + {"unpaired high", {0xFE, 0xFF, 0xD8, 0x3D, 0x00, 'A'}, "�A"}, + {"unpaired high at end", {0xFE, 0xFF, 0xD8, 0x3D}, "�"}, + {"unpaired low", {0xFE, 0xFF, 0xDE, 0x00}, "�"}, + {"two highs", {0xFE, 0xFF, 0xD8, 0x3D, 0xD8, 0x3D, 0xDE, 0x00}, "�\U0001F600"}, + // Truncated in the middle of a code unit. + {"odd length", {0xFE, 0xFF, 0x00, 'A', 0x00}, "A�"}, + {"odd length only", {0xFE, 0xFF, 0x41}, "�"}, + } + for c in cases { + got := decode_text_string(c.raw) + defer delete(got) + if got != c.want { + testing.expectf(t, false, "utf16be %s: got %q, want %q", c.name, got, c.want) + } + } +} + +@(test) +test_text_string_utf16le :: proc(t: ^testing.T) { + // Not in the format, but produced in the wild often enough to decode. + got := decode_text_string({0xFF, 0xFE, 'H', 0x00, 'i', 0x00}) + defer delete(got) + testing.expect_value(t, got, "Hi") + + pair := decode_text_string({0xFF, 0xFE, 0x3D, 0xD8, 0x00, 0xDE}) + defer delete(pair) + testing.expect_value(t, pair, "\U0001F600") +} + +@(test) +test_text_string_utf8_bom :: proc(t: ^testing.T) { + got := decode_text_string({0xEF, 0xBB, 0xBF, 0xC3, 0xA9, 'x'}) + defer delete(got) + testing.expect_value(t, got, "éx") + + // A truncated sequence after a mark that promised UTF-8. + bad := decode_text_string({0xEF, 0xBB, 0xBF, 'a', 0xC3}) + defer delete(bad) + testing.expect_value(t, bad, "a�") + + // A byte that can never start a UTF-8 sequence. + lone := decode_text_string({0xEF, 0xBB, 0xBF, 0xFF, 'z'}) + defer delete(lone) + testing.expect_value(t, lone, "�z") + + empty := decode_text_string({0xEF, 0xBB, 0xBF}) + defer delete(empty) + testing.expect_value(t, empty, "") +} + +@(test) +test_text_string_short_marks :: proc(t: ^testing.T) { + // One byte of what would have been a mark is PDFDocEncoding, not a + // truncated UTF-16 string. + one := decode_text_string({0xFE}) + defer delete(one) + testing.expect_value(t, one, "þ") + + two := decode_text_string({0xEF, 0xBB}) + defer delete(two) + testing.expect_value(t, two, "ï»") +} + +@(test) +test_decode_text_object :: proc(t: ^testing.T) { + got := decode_text_object(nil, str("Title")) + defer delete(got) + testing.expect_value(t, got, "Title") + + // A missing or wrongly typed entry is the empty string, not a crash. + none := decode_text_object(nil, nil) + defer delete(none) + testing.expect_value(t, none, "") + + num := decode_text_object(nil, i64(7)) + defer delete(num) + testing.expect_value(t, num, "") +} + +// --------------------------------------------------------------------------- +// parse_date +// --------------------------------------------------------------------------- + +@(test) +test_parse_date_shapes :: proc(t: ^testing.T) { + Case :: struct { + raw: string, + want: Date, + } + cases := []Case{ + // The form the spec gives (ISO 32000-2, 7.9.4). + {"D:199812231952-08'00'", {1998, 12, 23, 19, 52, 0, -8, 0, true}}, + {"D:20050320174900+01'00'", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // An offset whose minutes are not zero: the sign is on both fields. + {"D:20050320174900-05'30'", {2005, 3, 20, 17, 49, 0, -5, -30, true}}, + // Apostrophes missing entirely -- several writers do this. + {"D:200503201749+0100", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // A doubled trailing apostrophe, which the 1.7 spec's own example had. + {"D:20050320174900+01'00''", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // A colon where the apostrophe belongs (XMP habits leaking in). + {"D:20050320174900+01:00", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // Only the hours of the offset. + {"D:20050320174900+01", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // Z, with and without the redundant zero offset after it. + {"D:20050320174900Z", {2005, 3, 20, 17, 49, 0, 0, 0, true}}, + {"D:20050320174900Z00'00'", {2005, 3, 20, 17, 49, 0, 0, 0, true}}, + // No "D:" prefix, and a lower-case one. + {"20050320174900", {2005, 3, 20, 17, 49, 0, 0, 0, false}}, + {"d:20050320", {2005, 3, 20, 0, 0, 0, 0, 0, false}}, + // Truncated at every field boundary. + {"D:2005", {2005, 1, 1, 0, 0, 0, 0, 0, false}}, + {"D:200503", {2005, 3, 1, 0, 0, 0, 0, 0, false}}, + {"D:20050320", {2005, 3, 20, 0, 0, 0, 0, 0, false}}, + {"D:2005032017", {2005, 3, 20, 17, 0, 0, 0, 0, false}}, + {"D:200503201749", {2005, 3, 20, 17, 49, 0, 0, 0, false}}, + // An odd digit where a two-digit field belongs is dropped, not guessed. + {"D:2005032", {2005, 3, 1, 0, 0, 0, 0, 0, false}}, + // Leading whitespace, and a space before the offset. + {" D:20050320174900 -03'00'", {2005, 3, 20, 17, 49, 0, -3, 0, true}}, + // Every field out of range: clamped, not rejected. + {"D:20051345996199+99'99'", {2005, 12, 31, 23, 59, 59, 23, 59, true}}, + // Month 0 and day 0 are what "unset" looks like in some writers. + {"D:20050000000000", {2005, 1, 1, 0, 0, 0, 0, 0, false}}, + // 29 February against the Gregorian leap rule, century years included. + {"D:20210229", {2021, 2, 28, 0, 0, 0, 0, 0, false}}, + {"D:20200229", {2020, 2, 29, 0, 0, 0, 0, 0, false}}, + {"D:19000229", {1900, 2, 28, 0, 0, 0, 0, 0, false}}, + {"D:20000229", {2000, 2, 29, 0, 0, 0, 0, 0, false}}, + // 31 April clamps to the 30th. + {"D:20050431", {2005, 4, 30, 0, 0, 0, 0, 0, false}}, + // A sign with no digits after it is not an offset. + {"D:20050320174900+", {2005, 3, 20, 17, 49, 0, 0, 0, false}}, + // Trailing junk after a complete date is ignored. + {"D:20050320174900+01'00'garbage", {2005, 3, 20, 17, 49, 0, 1, 0, true}}, + // A 24th hour (midnight ending the day) clamps back rather than + // rolling the date over. + {"D:20050320240000", {2005, 3, 20, 23, 0, 0, 0, 0, false}}, + // A leap second clamps. + {"D:20051231235960Z", {2005, 12, 31, 23, 59, 59, 0, 0, true}}, + // A lower-case z is accepted the way a lower-case d: is. + {"D:20051231235900z", {2005, 12, 31, 23, 59, 0, 0, 0, true}}, + } + for c in cases { + got, ok := parse_date(transmute([]byte)c.raw) + if !ok { + testing.expectf(t, false, "parse_date(%q): not ok", c.raw) + continue + } + if got != c.want { + testing.expectf(t, false, "parse_date(%q):\n got %v\nwant %v", c.raw, got, c.want) + } + } +} + +@(test) +test_parse_date_rejects :: proc(t: ^testing.T) { + bad := []string{ + "", + "D:", + "D", + "not a date", + "D:203", // three digits is not a year + "\x00\x00\x00", + "D: ", + "199", + // Junk long enough that a year-shaped run late in it is out of the + // window: a date is a few dozen bytes, not a haystack. + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1999", + } + for s in bad { + if _, ok := parse_date(transmute([]byte)s); ok { + testing.expectf(t, false, "parse_date(%q): expected failure", s) + } + } +} + +@(test) +test_date_offset_minutes :: proc(t: ^testing.T) { + d, ok := parse_date(transmute([]byte)string("D:20050320174900-05'30'")) + testing.expect(t, ok) + testing.expect_value(t, date_offset_minutes(d), -330) + + d2, ok2 := parse_date(transmute([]byte)string("D:20050320174900+05'45'")) + testing.expect(t, ok2) + testing.expect_value(t, date_offset_minutes(d2), 345) + + d3, ok3 := parse_date(transmute([]byte)string("D:20050320")) + testing.expect(t, ok3) + testing.expect_value(t, date_offset_minutes(d3), 0) +} + +// --------------------------------------------------------------------------- +// Name and number trees +// --------------------------------------------------------------------------- + +// name_leaf builds a leaf holding the given keys, each mapped to the name +// "v", with the /Limits the format requires of a non-root node. +@(private = "file") +name_leaf :: proc(f: ^Fixture, keys: ..string) -> ^Dict { + names := new_array(f.alloc, 2 * len(keys)) + for k in keys { + array_push(names, str(k)) + array_push(names, Name(fmt.aprintf("v%s", k, allocator = f.alloc))) + } + return dict(f, + kv("Names", names), + kv("Limits", arr(f, str(keys[0]), str(keys[len(keys) - 1]))), + ) +} + +@(private = "file") +inner :: proc(f: ^Fixture, lo, hi: string, kids: ..^Dict) -> ^Dict { + ka := new_array(f.alloc, len(kids)) + for k in kids { + array_push(ka, k) + } + return dict(f, kv("Kids", ka), kv("Limits", arr(f, str(lo), str(hi)))) +} + +// A three-level tree: root -> 2 branches -> 2 leaves each, 12 keys in all. +@(private = "file") +build_name_tree :: proc(f: ^Fixture) -> ^Dict { + l1 := name_leaf(f, "alpha", "bravo", "charlie") + l2 := name_leaf(f, "delta", "echo", "foxtrot") + l3 := name_leaf(f, "golf", "hotel", "india") + l4 := name_leaf(f, "juliet", "kilo", "lima") + b1 := inner(f, "alpha", "foxtrot", l1, l2) + b2 := inner(f, "golf", "lima", l3, l4) + return dict(f, kv("Kids", arr(f, b1, b2))) +} + +@(private = "file") +TREE_KEYS := []string{ + "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", + "golf", "hotel", "india", "juliet", "kilo", "lima", +} + +@(test) +test_name_tree_lookup :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + root := build_name_tree(&f) + for k in TREE_KEYS { + got := name_tree_lookup(nil, root, transmute([]byte)k) + want := fmt.aprintf("v%s", k, allocator = f.alloc) + if !name_is(got, want) { + testing.expectf(t, false, "name_tree_lookup(%q): got %v, want /%s", k, got, want) + } + } +} + +@(test) +test_name_tree_misses :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + root := build_name_tree(&f) + misses := []string{ + "", // before everything + "aardvark", // below the first limit + "bravado", // inside a leaf's range, not present + "foxtrot2", // between two branches + "zulu", // above the last limit + "ALPHA", // byte order is case sensitive + "alph", // a prefix of a key is not the key + "alphabet", // a key with more after it is not the key + } + for k in misses { + got := name_tree_lookup(nil, root, transmute([]byte)k) + if got != nil { + testing.expectf(t, false, "name_tree_lookup(%q): got %v, want null", k, got) + } + } +} + +@(test) +test_name_tree_edge_nodes :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + // A nil root, an empty dictionary, and a root that is itself a leaf. + testing.expect(t, name_tree_lookup(nil, nil, transmute([]byte)string("x")) == nil) + + empty := dict(&f) + testing.expect(t, name_tree_lookup(nil, empty, transmute([]byte)string("x")) == nil) + + flat := dict(&f, kv("Names", arr(&f, str("only"), i64(42)))) + n, ok := to_int(name_tree_lookup(nil, flat, transmute([]byte)string("only"))) + testing.expect(t, ok) + testing.expect_value(t, n, 42) + + // An odd-length /Names array: the dangling key has no value, and reading + // one must not run off the end of the array. + ragged := dict(&f, kv("Names", arr(&f, str("a"), i64(1), str("b")))) + testing.expect(t, name_tree_lookup(nil, ragged, transmute([]byte)string("b")) == nil) + got, _ := to_int(name_tree_lookup(nil, ragged, transmute([]byte)string("a"))) + testing.expect_value(t, got, 1) + + // /Names of the wrong type altogether. + wrong := dict(&f, kv("Names", i64(3)), kv("Kids", str("nope"))) + testing.expect(t, name_tree_lookup(nil, wrong, transmute([]byte)string("a")) == nil) +} + +@(test) +test_name_tree_unsorted_leaf :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + // A /Names array in the wrong order, and one whose keys are not all + // strings. A binary search misses both; the linear fallback finds them. + unsorted := dict(&f, kv("Names", arr(&f, + str("zulu"), i64(1), str("alpha"), i64(2), str("mike"), i64(3)))) + Want :: struct { + key: string, + value: i64, + } + wants := []Want{{"zulu", 1}, {"alpha", 2}, {"mike", 3}} + for w in wants { + v, ok := to_int(name_tree_lookup(nil, unsorted, transmute([]byte)w.key)) + testing.expect(t, ok) + testing.expect_value(t, v, w.value) + } + + typed := dict(&f, kv("Names", arr(&f, + Name("notastring"), i64(1), str("real"), i64(2)))) + v, ok := to_int(name_tree_lookup(nil, typed, transmute([]byte)string("real"))) + testing.expect(t, ok) + testing.expect_value(t, v, 2) +} + +@(test) +test_name_tree_cycle_terminates :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + // A node whose only kid is itself. + self := dict(&f) + dict_set(self, "Kids", arr(&f, self)) + testing.expect(t, name_tree_lookup(nil, self, transmute([]byte)string("x")) == nil) + + // A two-node cycle with a real leaf hanging off it: the lookup must still + // find the key, still answer a miss, and still terminate. + a := dict(&f) + b := dict(&f) + leaf := dict(&f, kv("Names", arr(&f, str("found"), i64(9)))) + dict_set(a, "Kids", arr(&f, b, leaf)) + dict_set(b, "Kids", arr(&f, a)) + v, ok := to_int(name_tree_lookup(nil, a, transmute([]byte)string("found"))) + testing.expect(t, ok) + testing.expect_value(t, v, 9) + testing.expect(t, name_tree_lookup(nil, a, transmute([]byte)string("missing")) == nil) + + keys, values := name_tree_collect(nil, a) + defer delete(keys) + defer delete(values) + testing.expect_value(t, len(keys), 1) + testing.expect_value(t, len(values), 1) +} + +@(test) +test_name_tree_deep_chain :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + // A chain far deeper than MAX_DEPTH with the payload at the bottom. The + // walk must stop rather than recurse, and must not crash. + deep := dict(&f, kv("Names", arr(&f, str("bottom"), i64(1)))) + for _ in 0 ..< 200 { + deep = dict(&f, kv("Kids", arr(&f, deep))) + } + testing.expect(t, name_tree_lookup(nil, deep, transmute([]byte)string("bottom")) == nil) + + keys, values := name_tree_collect(nil, deep) + defer delete(keys) + defer delete(values) + testing.expect_value(t, len(keys), 0) + testing.expect_value(t, len(values), 0) +} + +@(test) +test_name_tree_collect :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + root := build_name_tree(&f) + keys, values := name_tree_collect(nil, root) + defer delete(keys) + defer delete(values) + + testing.expect_value(t, len(keys), len(TREE_KEYS)) + testing.expect_value(t, len(values), len(TREE_KEYS)) + for w, i in TREE_KEYS { + if i >= len(keys) { + break + } + if string(keys[i]) != w { + testing.expectf(t, false, "collect[%d]: got %q, want %q", i, string(keys[i]), w) + } + if !name_is(values[i], fmt.aprintf("v%s", w, allocator = f.alloc)) { + testing.expectf(t, false, "collect[%d]: value %v", i, values[i]) + } + } +} + +@(test) +test_name_tree_collect_empty :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + keys, values := name_tree_collect(nil, nil) + testing.expect_value(t, len(keys), 0) + testing.expect_value(t, len(values), 0) + + keys2, values2 := name_tree_collect(nil, dict(&f)) + testing.expect_value(t, len(keys2), 0) + testing.expect_value(t, len(values2), 0) +} + +// The keys collect returns point into the objects, not at copies. +@(test) +test_name_tree_collect_borrows_keys :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + src := "borrowed" + root := dict(&f, kv("Names", arr(&f, String(transmute([]byte)src), i64(1)))) + keys, values := name_tree_collect(nil, root) + defer delete(keys) + defer delete(values) + + testing.expect_value(t, len(keys), 1) + testing.expect_value(t, len(values), 1) + testing.expect(t, raw_data(keys[0]) == raw_data(transmute([]byte)src)) +} + +@(test) +test_number_tree_lookup :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + // /PageLabels-shaped: three leaves, two of them under a branch. + l1 := dict(&f, + kv("Nums", arr(&f, i64(0), Name("a"), i64(4), Name("b"))), + kv("Limits", arr(&f, i64(0), i64(4)))) + l2 := dict(&f, + kv("Nums", arr(&f, i64(9), Name("c"), i64(16), Name("d"))), + kv("Limits", arr(&f, i64(9), i64(16)))) + l3 := dict(&f, + kv("Nums", arr(&f, i64(100), Name("e"))), + kv("Limits", arr(&f, i64(100), i64(100)))) + b1 := dict(&f, kv("Kids", arr(&f, l1, l2)), kv("Limits", arr(&f, i64(0), i64(16)))) + root := dict(&f, kv("Kids", arr(&f, b1, l3))) + + Case :: struct { + key: i64, + want: string, // "" for a miss + } + cases := []Case{ + {0, "a"}, {4, "b"}, {9, "c"}, {16, "d"}, {100, "e"}, + {-1, ""}, {1, ""}, {5, ""}, {17, ""}, {99, ""}, {101, ""}, + {max(i64), ""}, {min(i64), ""}, + } + for c in cases { + got := number_tree_lookup(nil, root, c.key) + if c.want == "" { + if got != nil { + testing.expectf(t, false, "number_tree_lookup(%d): got %v, want null", c.key, got) + } + } else if !name_is(got, c.want) { + testing.expectf(t, false, "number_tree_lookup(%d): got %v, want /%s", c.key, got, c.want) + } + } +} + +@(test) +test_number_tree_cycle_and_collect :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + a := dict(&f) + leaf := dict(&f, kv("Nums", arr(&f, i64(3), Name("x"), i64(7), Name("y")))) + dict_set(a, "Kids", arr(&f, a, leaf)) + testing.expect(t, name_is(number_tree_lookup(nil, a, 7), "y")) + testing.expect(t, number_tree_lookup(nil, a, 8) == nil) + + keys, values := number_tree_collect(nil, a) + defer delete(keys) + defer delete(values) + testing.expect_value(t, len(keys), 2) + testing.expect_value(t, keys[0], i64(3)) + testing.expect_value(t, keys[1], i64(7)) + testing.expect(t, name_is(values[1], "y")) +} + +// A /Limits entry of the wrong type, or with too few elements, is unusable; +// the node below it is searched anyway rather than silently losing its keys. +@(test) +test_tree_bad_limits :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + l1 := dict(&f, + kv("Names", arr(&f, str("key"), i64(1))), + kv("Limits", arr(&f, i64(0), i64(9)))) + l2 := dict(&f, + kv("Names", arr(&f, str("other"), i64(2))), + kv("Limits", arr(&f, str("other")))) + root := dict(&f, kv("Kids", arr(&f, l1, l2))) + + v1, ok1 := to_int(name_tree_lookup(nil, root, transmute([]byte)string("key"))) + testing.expect(t, ok1) + testing.expect_value(t, v1, 1) + v2, ok2 := to_int(name_tree_lookup(nil, root, transmute([]byte)string("other"))) + testing.expect(t, ok2) + testing.expect_value(t, v2, 2) +} + +// A /Kids array holding things that are not dictionaries. +@(test) +test_tree_junk_kids :: proc(t: ^testing.T) { + f: Fixture + fixture_init(&f) + defer fixture_destroy(&f) + + leaf := dict(&f, kv("Names", arr(&f, str("k"), i64(5)))) + root := dict(&f, kv("Kids", arr(&f, i64(1), Name("no"), nil, str("no"), leaf))) + v, ok := to_int(name_tree_lookup(nil, root, transmute([]byte)string("k"))) + testing.expect(t, ok) + testing.expect_value(t, v, 5) + + keys, values := name_tree_collect(nil, root) + defer delete(keys) + defer delete(values) + testing.expect_value(t, len(keys), 1) + testing.expect_value(t, len(values), 1) +} + +// --------------------------------------------------------------------------- +// Fuzz-shaped +// +// The bytes reaching these two procedures came out of a file, so the property +// that matters is not "correct" but "never wrong in a way that hurts": the +// text always UTF-8, the date always in range, neither ever hanging. +// --------------------------------------------------------------------------- + +@(private = "file") +fuzz_next :: proc(state: ^u64) -> u64 { + x := state^ + x ~= x << 13 + x ~= x >> 7 + x ~= x << 17 + state^ = x + return x +} + +@(test) +test_text_string_output_is_always_utf8 :: proc(t: ^testing.T) { + state: u64 = 0x2545F4914F6CDD1D + buf: [40]byte + marks := [][]byte{{}, {0xFE, 0xFF}, {0xFF, 0xFE}, {0xEF, 0xBB, 0xBF}} + for _ in 0 ..< 6000 { + n := int(fuzz_next(&state) % u64(len(buf) + 1)) + for i in 0 ..< n { + buf[i] = byte(fuzz_next(&state)) + } + // Steer a quarter of the cases onto each decoder rather than leaving + // it to chance, which would almost always pick PDFDocEncoding. + mark := marks[fuzz_next(&state) % u64(len(marks))] + if n >= len(mark) { + copy(buf[:], mark) + } + got := decode_text_string(buf[:n]) + defer delete(got) + if !utf8.valid_string(got) { + testing.expectf(t, false, "invalid UTF-8 out of %x: %x", buf[:n], transmute([]byte)got) + return + } + } +} + +@(test) +test_parse_date_output_is_always_in_range :: proc(t: ^testing.T) { + state: u64 = 0x9E3779B97F4A7C15 + buf: [24]byte + alphabet := "0123456789D:+-Z' \x00zx" + for _ in 0 ..< 6000 { + n := int(fuzz_next(&state) % u64(len(buf) + 1)) + for i in 0 ..< n { + buf[i] = alphabet[fuzz_next(&state) % u64(len(alphabet))] + } + d, ok := parse_date(buf[:n]) + if !ok { + continue + } + in_range := + d.year >= 0 && d.year <= 9999 && + d.month >= 1 && d.month <= 12 && + d.day >= 1 && d.day <= date_days_in_month(d.year, d.month) && + d.hour >= 0 && d.hour <= 23 && + d.minute >= 0 && d.minute <= 59 && + d.second >= 0 && d.second <= 59 && + d.tz_hour >= -23 && d.tz_hour <= 23 && + d.tz_minute >= -59 && d.tz_minute <= 59 + if !in_range { + testing.expectf(t, false, "out of range date %v from %q", d, string(buf[:n])) + return + } + } +} diff --git a/error.odin b/error.odin new file mode 100644 index 0000000..b22d525 --- /dev/null +++ b/error.odin @@ -0,0 +1,224 @@ +package pdf + +import "core:fmt" + +// Error is the single error type the library reports. +// +// Real PDFs are frequently malformed, and a reader that refuses a file every +// other viewer opens is not useful. So most of these are only returned when +// recovery has already been attempted and failed; a file that merely has a +// broken cross-reference table, a wrong /Length, or a truncated stream opens +// with no error at all. Consult `Document.repairs` to see what was worked +// around. +Error :: enum u32 { + None = 0, + + // -- I/O and file structure ------------------------------------------- + Cant_Open_File, + Read_Failed, + Not_A_PDF, // no %PDF- header and no recoverable objects + Truncated, // file ends inside a construct that must complete + No_Xref, // no xref and object reconstruction found nothing + Bad_Xref, + Bad_Trailer, + No_Catalog, + No_Pages, + Page_Out_Of_Range, + Object_Not_Found, + Cyclic_Reference, + Recursion_Limit, // nesting deeper than the library will follow + Object_Limit, // file claims more objects than we will allocate + + // -- Lexing and object parsing ---------------------------------------- + Syntax_Error, + Unexpected_Token, + Bad_Number, + Bad_Name, + Bad_String, + Bad_Dict, + Bad_Array, + Bad_Stream, + Type_Mismatch, // object was not the type the caller asked for + + // -- Filters ---------------------------------------------------------- + Unknown_Filter, + Filter_Failed, + Bad_Predictor, + Bad_Flate_Data, + Bad_Lzw_Data, + Bad_Ascii85_Data, + Bad_Ascii_Hex_Data, + Bad_Run_Length_Data, + Bad_Ccitt_Data, + Bad_Jpeg_Data, + Bad_Jbig2_Data, + Unsupported_Jpx, // JPEG 2000 is passed through, never decoded + + // -- Encryption ------------------------------------------------------- + Encrypted, // needs a password the caller has not supplied + Wrong_Password, + Unsupported_Encryption, + Bad_Encrypt_Dict, + + // -- Content, colour and function ------------------------------------- + Bad_Content_Stream, + Bad_Colorspace, + Bad_Function, + Bad_Shading, + Bad_Pattern, + Bad_Image, + Unsupported_Image, + + // -- Fonts ------------------------------------------------------------ + Bad_Font, + Bad_Cmap, + Bad_Cff, + Bad_TrueType, + Bad_Type1, + No_Glyph, + + // -- Rendering -------------------------------------------------------- + Bitmap_Too_Large, + Out_Of_Memory, + + // -- Caller error ----------------------------------------------------- + Invalid_Argument, + Not_Implemented, +} + +// A Repair records a malformation that was worked around while reading, so a +// caller can tell "opened cleanly" from "opened, but this file is damaged". +// Repairs never fail an open; they are recorded on the Document and can be +// ignored entirely. +Repair :: struct { + kind: Repair_Kind, + // Byte offset in the file the problem was noticed at, or 0 if not tied to + // a position. + offset: i64, + // Object the repair applies to, or {0,0}. + ref: Ref, +} + +Repair_Kind :: enum u32 { + None = 0, + Rebuilt_Xref, // xref was unusable; objects found by scanning + Bad_Startxref, // startxref pointed somewhere that isn't an xref + Xref_Entry_Mismatch, // xref said obj N, the file had obj M there + Missing_Object, // a reference resolved to nothing; read as null + Recursion_Limit_Hit, // object nesting too deep to follow; read as null + Bad_Stream_Length, // /Length wrong; endstream located by scanning + Missing_Endstream, + Unterminated_String, + Unterminated_Dict, + Unterminated_Array, + Bad_Filter_Data, // a stream failed to decode; treated as empty + Missing_Root, // catalog located by scanning for /Type /Catalog + Missing_Page_Node, + Cyclic_Page_Tree, + Bad_Page_Object, + Bad_Font_Object, + Bad_Resource, + Unbalanced_Q, // content stream had stray q/Q + Content_Syntax, // operator soup skipped inside a content stream +} + +// error_string returns a short human-readable description. Suitable for a log +// line or a UI message; not stable enough to parse. +error_string :: proc(err: Error) -> string { + switch err { + case .None: return "no error" + case .Cant_Open_File: return "cannot open file" + case .Read_Failed: return "read failed" + case .Not_A_PDF: return "not a PDF file" + case .Truncated: return "file is truncated" + case .No_Xref: return "no usable cross-reference information" + case .Bad_Xref: return "malformed cross-reference table" + case .Bad_Trailer: return "malformed trailer" + case .No_Catalog: return "document has no catalog" + case .No_Pages: return "document has no pages" + case .Page_Out_Of_Range: return "page index out of range" + case .Object_Not_Found: return "object not found" + case .Cyclic_Reference: return "cyclic object reference" + case .Recursion_Limit: return "nesting too deep" + case .Object_Limit: return "too many objects" + case .Syntax_Error: return "syntax error" + case .Unexpected_Token: return "unexpected token" + case .Bad_Number: return "malformed number" + case .Bad_Name: return "malformed name" + case .Bad_String: return "malformed string" + case .Bad_Dict: return "malformed dictionary" + case .Bad_Array: return "malformed array" + case .Bad_Stream: return "malformed stream" + case .Type_Mismatch: return "object has the wrong type" + case .Unknown_Filter: return "unknown stream filter" + case .Filter_Failed: return "stream filter failed" + case .Bad_Predictor: return "malformed predictor parameters" + case .Bad_Flate_Data: return "malformed Flate data" + case .Bad_Lzw_Data: return "malformed LZW data" + case .Bad_Ascii85_Data: return "malformed ASCII85 data" + case .Bad_Ascii_Hex_Data: return "malformed ASCIIHex data" + case .Bad_Run_Length_Data: return "malformed RunLength data" + case .Bad_Ccitt_Data: return "malformed CCITT fax data" + case .Bad_Jpeg_Data: return "malformed JPEG data" + case .Bad_Jbig2_Data: return "malformed JBIG2 data" + case .Unsupported_Jpx: return "JPEG 2000 images are not decoded" + case .Encrypted: return "document is encrypted; a password is required" + case .Wrong_Password: return "wrong password" + case .Unsupported_Encryption: return "unsupported encryption scheme" + case .Bad_Encrypt_Dict: return "malformed encryption dictionary" + case .Bad_Content_Stream: return "malformed content stream" + case .Bad_Colorspace: return "malformed colour space" + case .Bad_Function: return "malformed function" + case .Bad_Shading: return "malformed shading" + case .Bad_Pattern: return "malformed pattern" + case .Bad_Image: return "malformed image" + case .Unsupported_Image: return "unsupported image format" + case .Bad_Font: return "malformed font" + case .Bad_Cmap: return "malformed CMap" + case .Bad_Cff: return "malformed CFF font program" + case .Bad_TrueType: return "malformed TrueType font program" + case .Bad_Type1: return "malformed Type1 font program" + case .No_Glyph: return "glyph not present in font" + case .Bitmap_Too_Large: return "requested bitmap is too large" + case .Out_Of_Memory: return "out of memory" + case .Invalid_Argument: return "invalid argument" + case .Not_Implemented: return "not implemented" + } + return "unknown error" +} + +repair_string :: proc(kind: Repair_Kind) -> string { + switch kind { + case .None: return "none" + case .Rebuilt_Xref: return "cross-reference table rebuilt by scanning" + case .Bad_Startxref: return "startxref did not point at an xref" + case .Xref_Entry_Mismatch: return "cross-reference entry pointed at the wrong object" + case .Missing_Object: return "reference to a missing object read as null" + case .Recursion_Limit_Hit: return "object nesting too deep to follow" + case .Bad_Stream_Length: return "stream /Length was wrong" + case .Missing_Endstream: return "stream had no endstream keyword" + case .Unterminated_String: return "unterminated string" + case .Unterminated_Dict: return "unterminated dictionary" + case .Unterminated_Array: return "unterminated array" + case .Bad_Filter_Data: return "stream failed to decode" + case .Missing_Root: return "catalog located by scanning" + case .Missing_Page_Node: return "page tree node missing" + case .Cyclic_Page_Tree: return "cyclic page tree" + case .Bad_Page_Object: return "malformed page object" + case .Bad_Font_Object: return "malformed font object" + case .Bad_Resource: return "malformed resource" + case .Unbalanced_Q: return "unbalanced q/Q in content stream" + case .Content_Syntax: return "unparsable content stream fragment skipped" + } + return "unknown repair" +} + +// repair_describe formats a repair with its position, for logs. +// The returned string is allocated with `allocator`. +repair_describe :: proc(r: Repair, allocator := context.allocator) -> string { + if r.ref.num != 0 { + return fmt.aprintf("%s (object %d %d R, offset %d)", + repair_string(r.kind), r.ref.num, r.ref.gen, r.offset, allocator = allocator) + } + return fmt.aprintf("%s (offset %d)", repair_string(r.kind), r.offset, allocator = allocator) +} diff --git a/examples/pdfinfo/main.odin b/examples/pdfinfo/main.odin new file mode 100644 index 0000000..4215661 --- /dev/null +++ b/examples/pdfinfo/main.odin @@ -0,0 +1,100 @@ +// pdfinfo prints the structure of a PDF: version, page count, metadata, page +// geometry, and how much content each page carries. +// +// It exists as much to keep the library's public surface honest as to be +// useful. Everything here is reachable from outside the package, so if a +// procedure a consumer needs is accidentally @(private), this example stops +// compiling and scripts/check.sh catches it. +// +// odin run examples/pdfinfo -- file.pdf +package main + +import "core:fmt" +import "core:os" +import pdf "../.." + +main :: proc() { + if len(os.args) < 2 { + fmt.eprintfln("usage: %s [more.pdf ...]", os.args[0]) + os.exit(2) + } + + failures := 0 + for path in os.args[1:] { + if !report(path) { + failures += 1 + } + } + if failures > 0 { + os.exit(1) + } +} + +report :: proc(path: string) -> bool { + doc, err := pdf.open_file(path) + if err != .None { + fmt.eprintfln("%s: %s", path, pdf.error_string(err)) + return false + } + defer pdf.close(doc) + + major, minor := pdf.document_version(doc) + fmt.printfln("%s", path) + fmt.printfln(" PDF %d.%d, %d pages, %d objects", major, minor, + pdf.page_count(doc), pdf.object_count(doc)) + + print_info(doc) + + total := 0 + for i in 0 ..< pdf.page_count(doc) { + page, perr := pdf.page_get(doc, i) + if perr != .None { + fmt.printfln(" page %d: %s", i + 1, pdf.error_string(perr)) + continue + } + + content, cerr := pdf.page_content(&page) + defer delete(content) + if cerr == .None { + total += len(content) + } + + // Only the first few, or a thousand-page standard buries everything. + if i < 5 { + b := pdf.page_bounds(&page) + w, h := pdf.page_size(&page, 1) + fmt.printfln(" page %d: %.1f x %.1f pt, rotate %d, %d x %d px, %d bytes of content", + i + 1, pdf.rect_width(b), pdf.rect_height(b), page.rotate, w, h, len(content)) + } + } + fmt.printfln(" %d bytes of content stream decoded, %d KB parsed", + total, pdf.document_memory_used(doc) / 1024) + + // A file that needed repairs still opened; say what was worked around, so + // the difference between "clean" and "damaged but readable" is visible. + if doc.is_damaged { + fmt.printfln(" %d repairs:", len(doc.repairs)) + shown := 0 + for r in doc.repairs { + if shown >= 5 { break } + fmt.printfln(" %s", pdf.repair_string(r.kind)) + shown += 1 + } + } + return true +} + +// Every value in the document information dictionary is a text string, so it +// has to go through decode_text_string: the common encodings are UTF-16BE with +// a byte-order mark and PDFDocEncoding, and neither is UTF-8. +print_info :: proc(doc: ^pdf.Document) { + info := pdf.dict_dict(doc, doc.trailer, "Info") + if info == nil { return } + for key in ([]string{"Title", "Author", "Subject", "Creator", "Producer", "CreationDate", "ModDate"}) { + raw := pdf.dict_bytes(doc, info, key) + if len(raw) == 0 { continue } + text := pdf.decode_text_string(raw) + defer delete(text) + fmt.printfln(" %-13s %s", key, text) + } +} diff --git a/examples/pdfrender/main.odin b/examples/pdfrender/main.odin new file mode 100644 index 0000000..d81518e --- /dev/null +++ b/examples/pdfrender/main.odin @@ -0,0 +1,267 @@ +// pdfrender rasterises a page and writes it out as a binary PPM (P6) or PNG. +// +// It is the smallest program that exercises the whole library: open, page +// tree, content interpreter, every device below it, and back out as pixels. +// Like pdfinfo it also keeps the public surface honest -- it is compiled by +// scripts/check.sh, so a procedure a renderer needs that is accidentally +// @(private) breaks the build here rather than in a consumer's project. +// +// odin run examples/pdfrender -- file.pdf # page 1 -> out.ppm +// odin run examples/pdfrender -- file.pdf -page=7 -scale=2 -o=seven.png +// +// Options: +// -page=N 1-based page number (default 1) +// -scale=F device pixels per point (default 1) +// -o=PATH output file; .png writes a PNG, anything else a PPM +// -no-annots draw the content stream only +// -gray report ink coverage instead of writing a file +package main + +import "core:fmt" +import "core:hash" +import "core:os" +import "core:strconv" +import "core:strings" +import pdf "../.." + +Options :: struct { + path: string, + out: string, + page: int, + scale: f64, + no_annots: bool, + stat_only: bool, +} + +main :: proc() { + opts, ok := parse_args() + if !ok { + fmt.eprintfln("usage: %s [-page=N] [-scale=F] [-o=out.ppm|out.png] [-no-annots] [-gray]", + len(os.args) > 0 ? os.args[0] : "pdfrender") + os.exit(2) + } + if !render(opts) { + os.exit(1) + } +} + +parse_args :: proc() -> (opts: Options, ok: bool) { + opts = Options{page = 1, scale = 1, out = "out.ppm"} + for arg in os.args[1:] { + switch { + case strings.has_prefix(arg, "-page="): + n, nok := strconv.parse_int(arg[len("-page="):]) + if !nok || n < 1 { return opts, false } + opts.page = n + case strings.has_prefix(arg, "-scale="): + f, fok := strconv.parse_f64(arg[len("-scale="):]) + if !fok || f <= 0 { return opts, false } + opts.scale = f + case strings.has_prefix(arg, "-o="): + opts.out = arg[len("-o="):] + case arg == "-no-annots": + opts.no_annots = true + case arg == "-gray": + opts.stat_only = true + case strings.has_prefix(arg, "-"): + return opts, false + case: + if opts.path != "" { return opts, false } + opts.path = arg + } + } + return opts, opts.path != "" +} + +render :: proc(opts: Options) -> bool { + doc, err := pdf.open_file(opts.path) + if err != .None { + fmt.eprintfln("%s: %s", opts.path, pdf.error_string(err)) + return false + } + defer pdf.close(doc) + + if opts.page > pdf.page_count(doc) { + fmt.eprintfln("%s: page %d of %d", opts.path, opts.page, pdf.page_count(doc)) + return false + } + page, perr := pdf.page_get(doc, opts.page - 1) + if perr != .None { + fmt.eprintfln("%s: page %d: %s", opts.path, opts.page, pdf.error_string(perr)) + return false + } + + // The scale rides on top of the page flip and /Rotate, which + // page_base_matrix applies for us; the bitmap comes out page_size big. + bmp, rerr := pdf.render_page(&page, { + ctm = pdf.scale_matrix(opts.scale, opts.scale), + no_annotations = opts.no_annots, + }) + defer pdf.bitmap_destroy(&bmp) + + // A render that reports an error still hands back everything it managed to + // draw, so say so and write the pixels anyway rather than throwing them + // away -- that is the difference between "damaged" and "unreadable". + if rerr != .None { + fmt.eprintfln("%s: page %d rendered with %s", opts.path, opts.page, pdf.error_string(rerr)) + if bmp.pixels == nil { return false } + } + + ink := ink_fraction(&bmp) + fmt.printfln("%s page %d: %dx%d px, %.2f%% ink", opts.path, opts.page, + bmp.width, bmp.height, ink * 100) + if opts.stat_only { + return true + } + + data: []byte + defer delete(data) + if strings.has_suffix(strings.to_lower(opts.out, context.temp_allocator), ".png") { + data = encode_png(&bmp) + } else { + data = encode_ppm(&bmp) + } + if data == nil { + fmt.eprintfln("%s: could not encode the image", opts.out) + return false + } + if werr := os.write_entire_file(opts.out, data); werr != nil { + fmt.eprintfln("%s: %v", opts.out, werr) + return false + } + fmt.printfln("wrote %s (%d bytes)", opts.out, len(data)) + return true +} + +// ink_fraction is the share of pixels that are not the untouched background. +// A page that renders pure white is nearly always a bug, so this is the one +// number worth printing without opening the image. +ink_fraction :: proc(bmp: ^pdf.Bitmap) -> f64 { + if bmp.width <= 0 || bmp.height <= 0 { return 0 } + inked := 0 + for y in 0 ..< bmp.height { + row := pdf.bitmap_row(bmp, y) + for x in 0 ..< bmp.width { + p := row[x * bmp.n:] + if p[0] != 255 || (bmp.n >= 3 && (p[1] != 255 || p[2] != 255)) { + inked += 1 + } + } + } + return f64(inked) / f64(bmp.width * bmp.height) +} + +// --------------------------------------------------------------------------- +// Writers +// +// PPM because it is four lines and every image tool reads it; PNG because +// nothing else does without one. Both drop alpha: render_page starts the page +// opaque white unless asked otherwise, so there is none to keep. +// --------------------------------------------------------------------------- + +encode_ppm :: proc(bmp: ^pdf.Bitmap) -> []byte { + b: strings.Builder + strings.builder_init(&b) + fmt.sbprintf(&b, "P6\n%d %d\n255\n", bmp.width, bmp.height) + for y in 0 ..< bmp.height { + row := pdf.bitmap_row(bmp, y) + for x in 0 ..< bmp.width { + r, g, bl := pixel_rgb(bmp, row, x) + strings.write_byte(&b, r) + strings.write_byte(&b, g) + strings.write_byte(&b, bl) + } + } + return b.buf[:] +} + +encode_png :: proc(bmp: ^pdf.Bitmap) -> []byte { + raw: [dynamic]byte + defer delete(raw) + for y in 0 ..< bmp.height { + row := pdf.bitmap_row(bmp, y) + append(&raw, 0) // filter type 0: none + for x in 0 ..< bmp.width { + r, g, bl := pixel_rgb(bmp, row, x) + append(&raw, r, g, bl) + } + } + + out: [dynamic]byte + append(&out, 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a) + + ihdr: [13]byte + put_u32(ihdr[0:], u32(bmp.width)) + put_u32(ihdr[4:], u32(bmp.height)) + ihdr[8] = 8 // bit depth + ihdr[9] = 2 // colour type 2: truecolour + png_chunk(&out, "IHDR", ihdr[:]) + + idat := zlib_stored(raw[:]) + defer delete(idat) + png_chunk(&out, "IDAT", idat) + png_chunk(&out, "IEND", nil) + return out[:] +} + +// zlib_stored wraps bytes in a zlib stream of uncompressed deflate blocks. +// +// core:compress has an inflater and no deflater, and a PNG's IDAT must be a +// zlib stream whatever is inside it -- so this is the honest way to write one +// without a compressor: stored blocks are a legal encoding that every decoder +// accepts. The file is the size of the pixels; PPM is the better choice if +// that matters, and this exists so the example can hand over something a +// browser opens. +zlib_stored :: proc(data: []byte) -> []byte { + MAX_BLOCK :: 65535 + + out: [dynamic]byte + append(&out, 0x78, 0x01) // CM=8, CINFO=7, FCHECK making it a multiple of 31 + + remaining := data + for { + n := min(len(remaining), MAX_BLOCK) + last: byte = len(remaining) - n == 0 ? 1 : 0 + append(&out, last, byte(n), byte(n >> 8), byte(~u16(n)), byte(~u16(n) >> 8)) + append(&out, ..remaining[:n]) + remaining = remaining[n:] + if last == 1 { break } + } + + sum := hash.adler32(data) + m := len(out) + resize(&out, m + 4) + put_u32(out[m:], sum) + return out[:] +} + +png_chunk :: proc(out: ^[dynamic]byte, tag: string, data: []byte) { + n := len(out) + resize(out, n + 4) + put_u32(out[n:], u32(len(data))) + append(out, tag) + append(out, ..data) + + // The CRC covers the tag and the data but not the length. + crc := hash.crc32(out[n + 4:]) + m := len(out) + resize(out, m + 4) + put_u32(out[m:], crc) +} + +put_u32 :: proc(dst: []byte, v: u32) { + dst[0] = byte(v >> 24) + dst[1] = byte(v >> 16) + dst[2] = byte(v >> 8) + dst[3] = byte(v) +} + +// A rendered page is RGBA8, but render_page_to accepts any component count, so +// handle the grey and RGB cases rather than assuming. +pixel_rgb :: proc(bmp: ^pdf.Bitmap, row: []byte, x: int) -> (r, g, b: byte) { + p := row[x * bmp.n:] + if bmp.n >= 3 { + return p[0], p[1], p[2] + } + return p[0], p[0], p[0] +} diff --git a/examples/pdftext/main.odin b/examples/pdftext/main.odin new file mode 100644 index 0000000..1953582 --- /dev/null +++ b/examples/pdftext/main.odin @@ -0,0 +1,119 @@ +// pdftext prints the text of a page, or of a whole document. +// +// The text comes off the same interpreter that draws the page, so what it +// prints is what a renderer would have painted: invisible OCR text under a +// scan comes out, text drawn as vector paths does not, and the reading order +// is the order the glyphs were placed in. Like the other examples it is +// compiled by scripts/check.sh, which is what keeps the public API honest. +// +// odin run examples/pdftext -- file.pdf # page 1 +// odin run examples/pdftext -- file.pdf -page=all # every page +// odin run examples/pdftext -- file.pdf -page=3 -stats +// +// Options: +// -page=N|all 1-based page number, or every page (default 1) +// -stats print per-page character counts instead of the text +package main + +import "core:fmt" +import "core:os" +import "core:strconv" +import "core:strings" +import "core:unicode/utf8" +import pdf "../.." + +main :: proc() { + path, page, stats, ok := parse_args() + if !ok { + fmt.eprintfln("usage: %s [-page=N|all] [-stats]", + len(os.args) > 0 ? os.args[0] : "pdftext") + os.exit(2) + } + + doc, err := pdf.open_file(path) + if err != .None { + fmt.eprintfln("%s: %s", path, pdf.error_string(err)) + os.exit(1) + } + defer pdf.close(doc) + + n := pdf.page_count(doc) + first, last := page, page + if page == ALL_PAGES { + first, last = 0, n - 1 + } + if first < 0 || last >= n { + fmt.eprintfln("%s: page %d of %d", path, first + 1, n) + os.exit(1) + } + + total, with_text, replacements := 0, 0, 0 + for i in first ..= last { + p, perr := pdf.page_get(doc, i) + if perr != .None { + fmt.eprintfln("page %d: %s", i + 1, pdf.error_string(perr)) + continue + } + + // extract_text hands back a string the caller owns. + text, terr := pdf.extract_text(&p) + defer delete(text) + if terr != .None { + fmt.eprintfln("page %d: %s", i + 1, pdf.error_string(terr)) + } + + total += len(text) + if len(text) > 0 { with_text += 1 } + replacements += count_replacements(text) + + if stats { + fmt.printfln("page %d: %d bytes, %d runes, %d U+FFFD", + i + 1, len(text), utf8.rune_count(text), count_replacements(text)) + continue + } + if first != last { + fmt.printfln("--- page %d ---", i + 1) + } + fmt.println(text) + } + + if stats || first != last { + fmt.printfln("%d of %d pages yielded text, %d bytes, %d U+FFFD", + with_text, last - first + 1, total, replacements) + } +} + +ALL_PAGES :: -1 + +parse_args :: proc() -> (path: string, page: int, stats: bool, ok: bool) { + for arg in os.args[1:] { + switch { + case arg == "-page=all": + page = ALL_PAGES + case strings.has_prefix(arg, "-page="): + n, nok := strconv.parse_int(arg[len("-page="):]) + if !nok || n < 1 { return "", 0, false, false } + page = n - 1 + case arg == "-stats": + stats = true + case strings.has_prefix(arg, "-"): + return "", 0, false, false + case: + if path != "" { return "", 0, false, false } + path = arg + } + } + return path, page, stats, path != "" +} + +// A code the font could not map to Unicode extracts as U+FFFD, so counting +// them is the cheapest measure of how well the /ToUnicode and encoding chain +// did on a file. A page that is mostly replacement characters has text on it +// that nothing can search or copy. +count_replacements :: proc(s: string) -> int { + n := 0 + for r in s { + if r == utf8.RUNE_ERROR { n += 1 } + } + return n +} diff --git a/filter_ascii.odin b/filter_ascii.odin new file mode 100644 index 0000000..f731f67 --- /dev/null +++ b/filter_ascii.odin @@ -0,0 +1,194 @@ +package pdf + +// ASCIIHexDecode and ASCII85Decode: the two filters that exist so a PDF can be +// mailed through a 7-bit channel. Both are self-delimiting -- the data ends at +// a marker, not at the end of the buffer -- so both are routinely handed more +// bytes than they will consume, and both must ignore whitespace anywhere. +// +// Neither ever fails. A character that belongs to neither alphabet is dropped: +// producers emit stray line-noise (a '\0' padding a stream, a lone '<' left +// over from a hex string) often enough that refusing the stream would lose a +// page over one byte. ISO 32000-2, 7.4.2 and 7.4.3 both make an unexpected +// character an error; treating it as a repair instead is the difference +// between a reader and a validator. + +// ascii_hex_decode decodes ASCIIHexDecode data. +// +// Whitespace is ignored, '>' ends the data, and an odd final digit is padded +// with '0' as the spec requires. Any other character is skipped. +// +// The returned slice is owned by the caller and allocated with `allocator`. +ascii_hex_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) { + n := ascii_hex_scan(data, nil) + if n == 0 { + return nil, .None + } + buf, alloc_err := make([]byte, n, allocator) + if alloc_err != nil { + return nil, .Out_Of_Memory + } + ascii_hex_scan(data, buf) + return buf, .None +} + +// ascii85_decode decodes ASCII85Decode data. +// +// "~>" ends the data, 'z' at the start of a group stands for four zero bytes, +// whitespace is ignored anywhere, and a truncated final group of n characters +// yields n-1 bytes. A leading "<~" -- which the filter does not define but +// which producers copying Adobe's own encoder emit -- is skipped. +// +// The returned slice is owned by the caller and allocated with `allocator`. +ascii85_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) { + n := ascii85_scan(data, nil) + if n == 0 { + return nil, .None + } + buf, alloc_err := make([]byte, n, allocator) + if alloc_err != nil { + return nil, .Out_Of_Memory + } + ascii85_scan(data, buf) + return buf, .None +} + +// --------------------------------------------------------------------------- +// Scanners +// +// Each decoder is one procedure run twice: once with `out == nil` to measure +// the result, once to fill it. Measuring first means the allocation is exactly +// the size of the output rather than an upper bound derived from an +// attacker-supplied length, and means the two passes cannot disagree about +// what the data says. +// --------------------------------------------------------------------------- + +// ISO 32000-2, Table 1: the six white-space characters. NUL counts, which +// matters here because it is what stream padding is usually made of. +@(private) +ascii_is_whitespace :: proc "contextless" (c: byte) -> bool { + switch c { + case 0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20: + return true + } + return false +} + +@(private) +ascii_hex_scan :: proc(data: []byte, out: []byte) -> int { + n := 0 + hi: byte + have_hi := false + for c in data { + if c == '>' { + break + } + // hex_value rejects whitespace and junk alike, and both are skipped, + // so there is nothing to tell apart here. + v, ok := hex_value(c) + if !ok { + continue + } + if !have_hi { + hi, have_hi = v, true + continue + } + if n < len(out) { + out[n] = hi << 4 | v + } + n += 1 + have_hi = false + } + // A final odd digit is padded with '0', i.e. shifted into the high nibble. + if have_hi { + if n < len(out) { + out[n] = hi << 4 + } + n += 1 + } + return n +} + +@(private) +ascii85_scan :: proc(data: []byte, out: []byte) -> int { + i := 0 + for i < len(data) && ascii_is_whitespace(data[i]) { + i += 1 + } + if i + 1 < len(data) && data[i] == '<' && data[i + 1] == '~' { + i += 2 + } + + n := 0 + group: [5]byte + ngroup := 0 + + for ; i < len(data); i += 1 { + c := data[i] + if ascii_is_whitespace(c) { + continue + } + // The terminator is "~>", but a '~' that is not followed by '>' is + // still the end of anything decodable, so do not look at the next byte. + if c == '~' { + break + } + if c == 'z' && ngroup == 0 { + n = ascii85_emit(out, n, 0, 4) + continue + } + // Everything outside the alphabet is dropped -- including a 'z' in the + // middle of a group, which has no defined meaning ('z' is above 'u'). + if c < '!' || c > 'u' { + continue + } + group[ngroup] = c - '!' + ngroup += 1 + if ngroup == 5 { + n = ascii85_emit(out, n, ascii85_value(group[:], 5), 4) + ngroup = 0 + } + } + + // A partial group of n characters encodes n-1 bytes. A single leftover + // character encodes nothing at all and is dropped. + if ngroup >= 2 { + n = ascii85_emit(out, n, ascii85_value(group[:], ngroup), ngroup - 1) + } + return n +} + +// ascii85_value folds a group of 1..5 digits, padding a short group with 'u' +// as ISO 32000-2, 7.4.3 requires. Five digits can encode up to 85^5-1, which +// is larger than a u32 can hold; such a group is out of spec, and clamping it +// keeps the bytes that were probably meant rather than dropping the group. +@(private) +ascii85_value :: proc(digits: []byte, count: int) -> u32 { + v: u64 = 0 + for k in 0 ..< 5 { + d: u64 = 84 // 'u' - '!' + if k < count && k < len(digits) { + d = u64(digits[k]) + } + v = v * 85 + d + } + if v > 0xFFFF_FFFF { + return 0xFFFF_FFFF + } + return u32(v) +} + +@(private) +ascii85_emit :: proc(out: []byte, n: int, v: u32, count: int) -> int { + b := [4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)} + m := n + for k in 0 ..< count { + if k >= len(b) { + break + } + if m < len(out) { + out[m] = b[k] + } + m += 1 + } + return m +} diff --git a/filter_ascii_test.odin b/filter_ascii_test.odin new file mode 100644 index 0000000..3d65620 --- /dev/null +++ b/filter_ascii_test.odin @@ -0,0 +1,355 @@ +package pdf + +import "core:mem/virtual" +import "core:slice" +import "core:testing" + +@(private = "file") +Ascii_Case :: struct { + name: string, + in_: string, + want: string, +} + +@(private = "file") +check_hex :: proc(t: ^testing.T, c: Ascii_Case) { + out, err := ascii_hex_decode(transmute([]byte)c.in_) + defer delete(out) + testing.expectf(t, err == .None, "%s: err = %v", c.name, err) + testing.expectf(t, slice.equal(out, transmute([]byte)c.want), + "%s: got %v, want %v", c.name, out, transmute([]byte)c.want) +} + +@(private = "file") +check_a85 :: proc(t: ^testing.T, c: Ascii_Case) { + out, err := ascii85_decode(transmute([]byte)c.in_) + defer delete(out) + testing.expectf(t, err == .None, "%s: err = %v", c.name, err) + testing.expectf(t, slice.equal(out, transmute([]byte)c.want), + "%s: got %v, want %v", c.name, out, transmute([]byte)c.want) +} + +// --------------------------------------------------------------------------- +// ASCIIHexDecode +// --------------------------------------------------------------------------- + +@(test) +test_ascii_hex_basic :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"hello", "48656C6C6F>", "Hello"}, + {"lowercase", "48656c6c6f>", "Hello"}, + {"mixed case", "48656C6c6F>", "Hello"}, + {"zero byte", "00>", "\x00"}, + {"all nibbles", "0123456789ABCDEF>", "\x01\x23\x45\x67\x89\xAB\xCD\xEF"}, + {"no terminator", "48656C6C6F", "Hello"}, + {"data after '>'", "4142>43444546", "AB"}, + } + for c in cases { + check_hex(t, c) + } +} + +@(test) +test_ascii_hex_odd_digit :: proc(t: ^testing.T) { + // ISO 32000-2, 7.4.2: an odd final digit is treated as if followed by '0'. + cases := []Ascii_Case{ + {"single digit", "4>", "\x40"}, + {"odd tail", "414>", "A\x40"}, + {"odd, unterminated", "41 4", "A\x40"}, + {"odd 'f'", "f>", "\xF0"}, + } + for c in cases { + check_hex(t, c) + } +} + +@(test) +test_ascii_hex_whitespace_and_junk :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"spaces", "48 65 6C 6C 6F >", "Hello"}, + {"newlines", "48\n65\r\n6C\t6C\x0C6F>", "Hello"}, + {"nul padding", "\x0048656C6C6F\x00>", "Hello"}, + {"leading ws", " \n48>", "H"}, + {"junk skipped", "4?8g6/5>", "H\x65"}, + {"all junk", "hello world>", "\xed"}, + } + for c in cases { + check_hex(t, c) + } +} + +@(test) +test_ascii_hex_degenerate :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"empty", "", ""}, + {"eod only", ">", ""}, + {"only ws", " \n\t ", ""}, + {"eod first", "> 4142", ""}, + {"only junk", "zz!!", ""}, + } + for c in cases { + check_hex_nothing(t, c) + } +} + +// A decode that produces nothing must also allocate nothing, or every empty +// stream in a document leaks a zero-length block. +@(private = "file") +check_hex_nothing :: proc(t: ^testing.T, c: Ascii_Case) { + out, err := ascii_hex_decode(transmute([]byte)c.in_) + defer delete(out) + testing.expectf(t, err == .None, "%s: err = %v", c.name, err) + testing.expectf(t, len(out) == 0, "%s: got %v, want empty", c.name, out) +} + +@(test) +test_ascii_hex_long_run :: proc(t: ^testing.T) { + // 512 bytes through the filter, to exercise the measure/fill passes on + // something longer than one cache line of hand-written input. + src := make([]byte, 512) + defer delete(src) + enc := make([dynamic]byte, 0, 1100) + defer delete(enc) + digits := "0123456789ABCDEF" + for i in 0 ..< len(src) { + src[i] = byte(i * 7) + append(&enc, digits[src[i] >> 4], digits[src[i] & 0xF]) + if i % 40 == 39 { + append(&enc, '\n') + } + } + append(&enc, '>') + + out, err := ascii_hex_decode(enc[:]) + defer delete(out) + testing.expect(t, err == .None) + testing.expect(t, slice.equal(out, src)) +} + +// --------------------------------------------------------------------------- +// ASCII85Decode +// --------------------------------------------------------------------------- + +@(test) +test_ascii85_basic :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"full group", "9jqo^~>", "Man "}, + {"three bytes", "9jqo~>", "Man"}, + {"two bytes", "9jn~>", "Ma"}, + {"one byte", "9`~>", "M"}, + {"six bytes", "F*2M7/c~>", "sure."}, + {"max u32", "s8W-!~>", "\xFF\xFF\xFF\xFF"}, + {"no terminator", "9jqo^", "Man "}, + {"partial, no terminator", "9jn", "Ma"}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii85_z_shorthand :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"z alone", "z~>", "\x00\x00\x00\x00"}, + {"z twice", "zz~>", "\x00\x00\x00\x00\x00\x00\x00\x00"}, + {"z then data", "zBOu!rDZ~>", "\x00\x00\x00\x00hello"}, + {"data then z", "9jqo^z~>", "Man \x00\x00\x00\x00"}, + // 'z' inside a group has no meaning; dropping it lets the surrounding + // group still decode, which is what a producer that emitted stray + // bytes meant. + {"z mid-group", "9jzqo^~>", "Man "}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii85_whitespace_and_prefix :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"prefix", "<~9jqo^~>", "Man "}, + {"prefix and ws", " \n<~9jqo^ ~>", "Man "}, + {"ws everywhere", "9 j\nq\ro\t^\x0C~>", "Man "}, + {"nul padding", "\x009jqo^\x00~>", "Man "}, + {"ws in prefix-less", " 9jqo^~>", "Man "}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii85_degenerate :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + {"empty", "", ""}, + {"eod only", "~>", ""}, + {"prefix and eod", "<~~>", ""}, + {"only ws", " \n\t", ""}, + // A final group of one character encodes nothing at all: it carries + // fewer than 8 bits. + {"single leftover", "9jqo^9~>", "Man "}, + {"lone leftover", "9~>", ""}, + // A lone '~' is not a legal terminator but is still the end of + // anything decodable. + {"bare tilde", "9jqo^~", "Man "}, + {"data after eod", "9jqo^~>9jqo^", "Man "}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii85_out_of_range_group :: proc(t: ^testing.T) { + // "uuuuu" is 85^5-1 = 4437053124, past 2^32-1. Clamping keeps the group; + // rejecting it would lose the rest of the stream with it. + out, err := ascii85_decode(transmute([]byte)string("uuuuu~>")) + defer delete(out) + testing.expect(t, err == .None) + testing.expect(t, slice.equal(out, []byte{0xFF, 0xFF, 0xFF, 0xFF})) + + // A short group padded with 'u' can overflow the same way. + out2, err2 := ascii85_decode(transmute([]byte)string("uuuu~>")) + defer delete(out2) + testing.expect(t, err2 == .None) + testing.expect(t, slice.equal(out2, []byte{0xFF, 0xFF, 0xFF})) +} + +@(test) +test_ascii85_out_of_alphabet :: proc(t: ^testing.T) { + cases := []Ascii_Case{ + // 'v'..'y' and DEL are outside '!'..'u' and are dropped. + {"high letters", "9jqvo^~>", "Man "}, + {"del", "9jq\x7Fo^~>", "Man "}, + {"high bit", "9jq\xFFo^~>", "Man "}, + {"below bang", "9jq\x1Fo^~>", "Man "}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii85_spec_vector :: proc(t: ^testing.T) { + // The canonical Ascii85 vector: Hamlet, II.ii. + enc := `9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKFCj@.4Gp$d7F!,L7@<6@)/0JDEF@3BB/F*&OCAfu2/AKYi(DIb9~>` + want := "Man is distinguished, not only by his reason, but by this singular " + + "passion from other animals, which is a lust of the mind," + out, err := ascii85_decode(transmute([]byte)enc) + defer delete(out) + testing.expect(t, err == .None) + testing.expectf(t, slice.equal(out, transmute([]byte)want), "got %q", string(out)) +} + +@(test) +test_ascii85_truncated_everywhere :: proc(t: ^testing.T) { + // Truncating the vector at every offset must decode a prefix of the + // plaintext and never crash: a stream cut short mid-group is the normal + // shape of a damaged file. + enc := `9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKF~>` + full := "Man is distinguished, not only by his reason, " + for cut in 0 ..= len(enc) { + out, err := ascii85_decode(transmute([]byte)enc[:cut]) + testing.expectf(t, err == .None, "cut %d: err = %v", cut, err) + testing.expectf(t, len(out) <= len(full), "cut %d: %d bytes", cut, len(out)) + if len(out) > 0 { + testing.expectf(t, slice.equal(out, transmute([]byte)full[:len(out)]), + "cut %d: got %q", cut, string(out)) + } + delete(out) + } +} + +@(test) +test_ascii_hex_truncated_everywhere :: proc(t: ^testing.T) { + enc := "48656C6C6F2C20776F726C64>" + full := "Hello, world" + for cut in 0 ..= len(enc) { + out, err := ascii_hex_decode(transmute([]byte)enc[:cut]) + testing.expectf(t, err == .None, "cut %d: err = %v", cut, err) + testing.expectf(t, len(out) <= len(full), "cut %d: %d bytes", cut, len(out)) + // An odd cut lands mid-byte, whose value is the high nibble alone. + n := cut / 2 + if n > 0 && n <= len(full) { + testing.expectf(t, slice.equal(out[:n], transmute([]byte)full[:n]), + "cut %d: got %q", cut, string(out)) + } + delete(out) + } +} + +@(test) +test_ascii85_cross_check :: proc(t: ^testing.T) { + // Vectors produced by an independent encoder (Python's base64.a85encode) + // over random bytes, at every length modulo 4, so the partial-group path + // is checked against something other than this file's own arithmetic. + cases := []Ascii_Case{ + {"n=1", "i;~>", "\xE1"}, + {"n=2", "3s,~>", ";\x03"}, + {"n=3", "/eKQ~>", ".\x11*"}, + {"n=4", "1:;B5~>", "2\xB5y\x08"}, + {"n=5", "%g]H7m/~>", "\x0F\x08\xB1\xF7\xED"}, + {"n=7", "9J0U3#Pii~>", "L.]:\x07\xF9\x7F"}, + {"n=11", "+mpR,(Q'?(p9f\\~>", "!\xEE#-\x17\x8A \x9A\xF6\xB5\x88"}, + {"n=16", "IqImN,Qau;o\"e", "\x7Ff\xE8\x09$\x02\xAAI\xF2\xC1U\x1B'\xFES&"}, + {"n=23", "DFk;B3(TS''\\/0W>)Q1E:[Jn9ZMgp~>", + "nI\x0D\xB18H\x9C\xE8\x14\xD5\x8D\x14Z\x8BO\x99O\xED\x15\xC5\xB2\xFD\xAE"}, + } + for c in cases { + check_a85(t, c) + } +} + +@(test) +test_ascii_filters_honour_allocator :: proc(t: ^testing.T) { + // Decoded stream data usually goes straight into the Document arena, which + // is never freed element by element, so the allocator parameter has to be + // the one that is actually used. + arena: virtual.Arena + testing.expect(t, virtual.arena_init_growing(&arena) == nil) + defer virtual.arena_destroy(&arena) + alloc := virtual.arena_allocator(&arena) + + hex, herr := ascii_hex_decode(transmute([]byte)string("48656C6C6F>"), alloc) + testing.expect(t, herr == .None) + testing.expect(t, slice.equal(hex, transmute([]byte)string("Hello"))) + + a85, aerr := ascii85_decode(transmute([]byte)string("9jqo^~>"), alloc) + testing.expect(t, aerr == .None) + testing.expect(t, slice.equal(a85, transmute([]byte)string("Man "))) +} + +// A tiny deterministic PRNG, so the fuzz tests are reproducible and do not +// depend on the state of the context's random generator. +@(private = "file") +ascii_fuzz_next :: proc(state: ^u64) -> u32 { + state^ = state^ * 6364136223846793005 + 1442695040888963407 + return u32(state^ >> 33) +} + +@(test) +test_ascii_filters_fuzz :: proc(t: ^testing.T) { + // Random bytes are not valid data for either filter, which is the point: + // neither may crash, hang, or claim more output than the input can hold. + state: u64 = 0xC0FFEE + buf: [256]byte + for round in 0 ..< 400 { + n := int(ascii_fuzz_next(&state) % u32(len(buf) + 1)) + for i in 0 ..< n { + buf[i] = byte(ascii_fuzz_next(&state)) + } + data := buf[:n] + + hex, herr := ascii_hex_decode(data) + testing.expectf(t, herr == .None, "round %d: hex err %v", round, herr) + testing.expectf(t, len(hex) <= n / 2 + 1, "round %d: hex grew to %d from %d", + round, len(hex), n) + delete(hex) + + a85, aerr := ascii85_decode(data) + testing.expectf(t, aerr == .None, "round %d: a85 err %v", round, aerr) + testing.expectf(t, len(a85) <= 4 * n, "round %d: a85 grew to %d from %d", + round, len(a85), n) + delete(a85) + } +} diff --git a/filter_ccitt.odin b/filter_ccitt.odin new file mode 100644 index 0000000..a4b54d9 --- /dev/null +++ b/filter_ccitt.odin @@ -0,0 +1,836 @@ +package pdf + +// CCITTFaxDecode -- ITU-T T.4 (Group 3) and T.6 (Group 4) facsimile coding. +// +// ISO 32000-2, 7.4.6. The filter reconstructs a bi-level image that was +// encoded with the one-dimensional modified Huffman code (MH), the +// two-dimensional modified READ code (MR), or the pure two-dimensional +// modified modified READ code (MMR / Group 4). +// +// OUTPUT CONVENTION -- image.odin depends on this, so it is stated precisely: +// +// * One bit per pixel, most significant bit first within each byte, so +// pixel `x` of a row lives in bit `7 - (x & 7)` of byte `x >> 3`. +// * Each row starts on a byte boundary; the trailing bits of the last byte +// of a row are padding and are set to the white value. +// * With `black_is_1 == false` (the default, /BlackIs1 false) a black pixel +// is a 0 bit and a white pixel is a 1 bit. With `black_is_1 == true` the +// two are exchanged. +// +// That is exactly the 1-bit-per-component sample array an image XObject +// expects *before* /Decode is applied, which is why /BlackIs1 lives in the +// filter's parameters rather than in the image dictionary. +// +// The returned buffer holds `len(out) / ((columns + 7) / 8)` rows. That may be +// fewer than `p.rows` when the data runs out early: the filter returns the +// rows it managed to decode rather than failing, so a truncated fax still +// shows what was received. Callers that need exactly `p.rows` rows must pad. + +// --------------------------------------------------------------------------- +// Limits +// +// Every one of these bounds a quantity that came out of the file. +// --------------------------------------------------------------------------- + +// A row wider than this is not an image anyone scanned. T.4 defines paper +// widths up to 2432 pixels and its make-up codes stop at 2560; a 600 dpi A3 +// scan reaches ~10000. 65536 is far past all of them and still keeps the two +// `columns + 8` changing-element lines to about a megabyte. +@(private) +MAX_CCITT_COLUMNS :: 1 << 16 +// Rows decoded before the filter gives up. A2 at 400 dpi is ~9000 rows, so +// this is only ever reached by a stream designed to be reached. +@(private) +MAX_CCITT_ROWS :: 1 << 20 +// Total decoded bytes. Matches MAX_BITMAP_PIXELS at one bit per pixel, so an +// image this filter accepts is one the rasteriser would also accept. +@(private) +MAX_CCITT_BYTES :: MAX_BITMAP_PIXELS / 8 +// Fill bits tolerated before an EOL. T.4 allows fill of any length; a real +// encoder uses it to pad a line out to the minimum transmission time, which +// at 2400 baud is a few hundred bits, never tens of thousands. +@(private) +MAX_CCITT_FILL_BITS :: 1 << 16 +// Make-up codes chained before a terminating code. T.4 needs at most one for +// widths up to 2623 and the extended codes let an encoder chain more for +// wider images; 64 of them already codes a run of 163840. +@(private) +MAX_CCITT_MAKEUP_CHAIN :: 64 + +// Ccitt_Params mirrors the CCITTFaxDecode parameter dictionary +// (ISO 32000-2, table 12). The zero value is *not* the PDF default: use +// `ccitt_default_params` or set `columns` and `end_of_block` yourself. +Ccitt_Params :: struct { + // /K. Negative: pure two-dimensional (Group 4, MMR). Zero: pure + // one-dimensional (Group 3 1-D, MH). Positive: mixed, each line tagged + // one- or two-dimensional after its EOL. + k: int, + // /Columns, pixels per row. Default 1728. + columns: int, + // /Rows. Zero means "unknown": decode until the data runs out or an RTC. + rows: int, + // /BlackIs1. False (the default) means a 0 bit is black. + black_is_1: bool, + // /EncodedByteAlign: every encoded line begins on a byte boundary. + byte_align: bool, + // /EndOfLine: the encoded data is expected to carry EOL codes. EOLs are + // accepted whether or not this is set -- producers set it inconsistently + // and an EOL is unambiguous in the bit stream -- so it only records the + // producer's intent. + end_of_line: bool, + // /EndOfBlock. Default true. When false the caller is asserting that + // `rows` is authoritative and that whatever follows the image is not + // ours to interpret. An RTC/EOFB still ends decoding either way, because + // no valid image data can follow one. + end_of_block: bool, +} + +// ccitt_default_params returns the parameter defaults of ISO 32000-2 table 12. +ccitt_default_params :: proc() -> Ccitt_Params { + return Ccitt_Params{k = 0, columns = 1728, rows = 0, black_is_1 = false, + byte_align = false, end_of_line = false, end_of_block = true} +} + +// --------------------------------------------------------------------------- +// Bit reader +// +// Reads MSB-first and pads with zero bits past the end of the data, so a +// peek near the end never has to be special-cased; `ccitt_bits_left` is what +// decides whether a code is really there. +// --------------------------------------------------------------------------- + +@(private) +Ccitt_Bits :: struct { + data: []byte, + pos: int, // bit offset from the start of `data` + end: int, // len(data) * 8 +} + +@(private) +ccitt_bits_make :: proc(data: []byte) -> Ccitt_Bits { + return Ccitt_Bits{data = data, pos = 0, end = len(data) * 8} +} + +@(private) +ccitt_bits_left :: proc(b: ^Ccitt_Bits) -> int { + return b.end - b.pos +} + +// ccitt_bits_peek returns the next `n` bits (n <= 24) without consuming them. +@(private) +ccitt_bits_peek :: proc(b: ^Ccitt_Bits, n: int) -> u32 { + if n <= 0 { + return 0 + } + n := n + if n > 24 { + n = 24 + } + // Four bytes cover any 24-bit read starting at any bit offset. + i := b.pos >> 3 + acc: u32 = 0 + for k in 0 ..< 4 { + v: u32 = 0 + if i + k < len(b.data) { + v = u32(b.data[i + k]) + } + acc = acc << 8 | v + } + acc <<= uint(b.pos & 7) + return acc >> uint(32 - n) +} + +@(private) +ccitt_bits_skip :: proc(b: ^Ccitt_Bits, n: int) { + b.pos += n + if b.pos > b.end { + b.pos = b.end + } +} + +// ccitt_bits_align moves to the next byte boundary (/EncodedByteAlign). +@(private) +ccitt_bits_align :: proc(b: ^Ccitt_Bits) { + b.pos = min((b.pos + 7) & ~int(7), b.end) +} + +// --------------------------------------------------------------------------- +// Run-length codes +// --------------------------------------------------------------------------- + +@(private) +Ccitt_Code :: struct { + bits: u8, // code length + code: u16, // the code, right-aligned in `bits` bits + run: u16, // run length it stands for +} + +// White run lengths: T.4 tables 1 and 3, plus the shared extended make-up codes of table 3. +@(private, rodata) +CCITT_WHITE := [104]Ccitt_Code{ + {bits = 4, code = 0b0111, run = 2}, + {bits = 4, code = 0b1000, run = 3}, + {bits = 4, code = 0b1011, run = 4}, + {bits = 4, code = 0b1100, run = 5}, + {bits = 4, code = 0b1110, run = 6}, + {bits = 4, code = 0b1111, run = 7}, + {bits = 5, code = 0b00111, run = 10}, + {bits = 5, code = 0b01000, run = 11}, + {bits = 5, code = 0b10010, run = 128}, + {bits = 5, code = 0b10011, run = 8}, + {bits = 5, code = 0b10100, run = 9}, + {bits = 5, code = 0b11011, run = 64}, + {bits = 6, code = 0b000011, run = 13}, + {bits = 6, code = 0b000111, run = 1}, + {bits = 6, code = 0b001000, run = 12}, + {bits = 6, code = 0b010111, run = 192}, + {bits = 6, code = 0b011000, run = 1664}, + {bits = 6, code = 0b101010, run = 16}, + {bits = 6, code = 0b101011, run = 17}, + {bits = 6, code = 0b110100, run = 14}, + {bits = 6, code = 0b110101, run = 15}, + {bits = 7, code = 0b0000011, run = 22}, + {bits = 7, code = 0b0000100, run = 23}, + {bits = 7, code = 0b0001000, run = 20}, + {bits = 7, code = 0b0001100, run = 19}, + {bits = 7, code = 0b0010011, run = 26}, + {bits = 7, code = 0b0010111, run = 21}, + {bits = 7, code = 0b0011000, run = 28}, + {bits = 7, code = 0b0100100, run = 27}, + {bits = 7, code = 0b0100111, run = 18}, + {bits = 7, code = 0b0101000, run = 24}, + {bits = 7, code = 0b0101011, run = 25}, + {bits = 7, code = 0b0110111, run = 256}, + {bits = 8, code = 0b00000010, run = 29}, + {bits = 8, code = 0b00000011, run = 30}, + {bits = 8, code = 0b00000100, run = 45}, + {bits = 8, code = 0b00000101, run = 46}, + {bits = 8, code = 0b00001010, run = 47}, + {bits = 8, code = 0b00001011, run = 48}, + {bits = 8, code = 0b00010010, run = 33}, + {bits = 8, code = 0b00010011, run = 34}, + {bits = 8, code = 0b00010100, run = 35}, + {bits = 8, code = 0b00010101, run = 36}, + {bits = 8, code = 0b00010110, run = 37}, + {bits = 8, code = 0b00010111, run = 38}, + {bits = 8, code = 0b00011010, run = 31}, + {bits = 8, code = 0b00011011, run = 32}, + {bits = 8, code = 0b00100100, run = 53}, + {bits = 8, code = 0b00100101, run = 54}, + {bits = 8, code = 0b00101000, run = 39}, + {bits = 8, code = 0b00101001, run = 40}, + {bits = 8, code = 0b00101010, run = 41}, + {bits = 8, code = 0b00101011, run = 42}, + {bits = 8, code = 0b00101100, run = 43}, + {bits = 8, code = 0b00101101, run = 44}, + {bits = 8, code = 0b00110010, run = 61}, + {bits = 8, code = 0b00110011, run = 62}, + {bits = 8, code = 0b00110100, run = 63}, + {bits = 8, code = 0b00110101, run = 0}, + {bits = 8, code = 0b00110110, run = 320}, + {bits = 8, code = 0b00110111, run = 384}, + {bits = 8, code = 0b01001010, run = 59}, + {bits = 8, code = 0b01001011, run = 60}, + {bits = 8, code = 0b01010010, run = 49}, + {bits = 8, code = 0b01010011, run = 50}, + {bits = 8, code = 0b01010100, run = 51}, + {bits = 8, code = 0b01010101, run = 52}, + {bits = 8, code = 0b01011000, run = 55}, + {bits = 8, code = 0b01011001, run = 56}, + {bits = 8, code = 0b01011010, run = 57}, + {bits = 8, code = 0b01011011, run = 58}, + {bits = 8, code = 0b01100100, run = 448}, + {bits = 8, code = 0b01100101, run = 512}, + {bits = 8, code = 0b01100111, run = 640}, + {bits = 8, code = 0b01101000, run = 576}, + {bits = 9, code = 0b010011000, run = 1472}, + {bits = 9, code = 0b010011001, run = 1536}, + {bits = 9, code = 0b010011010, run = 1600}, + {bits = 9, code = 0b010011011, run = 1728}, + {bits = 9, code = 0b011001100, run = 704}, + {bits = 9, code = 0b011001101, run = 768}, + {bits = 9, code = 0b011010010, run = 832}, + {bits = 9, code = 0b011010011, run = 896}, + {bits = 9, code = 0b011010100, run = 960}, + {bits = 9, code = 0b011010101, run = 1024}, + {bits = 9, code = 0b011010110, run = 1088}, + {bits = 9, code = 0b011010111, run = 1152}, + {bits = 9, code = 0b011011000, run = 1216}, + {bits = 9, code = 0b011011001, run = 1280}, + {bits = 9, code = 0b011011010, run = 1344}, + {bits = 9, code = 0b011011011, run = 1408}, + {bits = 11, code = 0b00000001000, run = 1792}, + {bits = 11, code = 0b00000001100, run = 1856}, + {bits = 11, code = 0b00000001101, run = 1920}, + {bits = 12, code = 0b000000010010, run = 1984}, + {bits = 12, code = 0b000000010011, run = 2048}, + {bits = 12, code = 0b000000010100, run = 2112}, + {bits = 12, code = 0b000000010101, run = 2176}, + {bits = 12, code = 0b000000010110, run = 2240}, + {bits = 12, code = 0b000000010111, run = 2304}, + {bits = 12, code = 0b000000011100, run = 2368}, + {bits = 12, code = 0b000000011101, run = 2432}, + {bits = 12, code = 0b000000011110, run = 2496}, + {bits = 12, code = 0b000000011111, run = 2560}, +} + +// Black run lengths: T.4 tables 2 and 3, plus the shared extended make-up codes. +@(private, rodata) +CCITT_BLACK := [104]Ccitt_Code{ + {bits = 2, code = 0b10, run = 3}, + {bits = 2, code = 0b11, run = 2}, + {bits = 3, code = 0b010, run = 1}, + {bits = 3, code = 0b011, run = 4}, + {bits = 4, code = 0b0010, run = 6}, + {bits = 4, code = 0b0011, run = 5}, + {bits = 5, code = 0b00011, run = 7}, + {bits = 6, code = 0b000100, run = 9}, + {bits = 6, code = 0b000101, run = 8}, + {bits = 7, code = 0b0000100, run = 10}, + {bits = 7, code = 0b0000101, run = 11}, + {bits = 7, code = 0b0000111, run = 12}, + {bits = 8, code = 0b00000100, run = 13}, + {bits = 8, code = 0b00000111, run = 14}, + {bits = 9, code = 0b000011000, run = 15}, + {bits = 10, code = 0b0000001000, run = 18}, + {bits = 10, code = 0b0000001111, run = 64}, + {bits = 10, code = 0b0000010111, run = 16}, + {bits = 10, code = 0b0000011000, run = 17}, + {bits = 10, code = 0b0000110111, run = 0}, + {bits = 11, code = 0b00000001000, run = 1792}, + {bits = 11, code = 0b00000001100, run = 1856}, + {bits = 11, code = 0b00000001101, run = 1920}, + {bits = 11, code = 0b00000010111, run = 24}, + {bits = 11, code = 0b00000011000, run = 25}, + {bits = 11, code = 0b00000101000, run = 23}, + {bits = 11, code = 0b00000110111, run = 22}, + {bits = 11, code = 0b00001100111, run = 19}, + {bits = 11, code = 0b00001101000, run = 20}, + {bits = 11, code = 0b00001101100, run = 21}, + {bits = 12, code = 0b000000010010, run = 1984}, + {bits = 12, code = 0b000000010011, run = 2048}, + {bits = 12, code = 0b000000010100, run = 2112}, + {bits = 12, code = 0b000000010101, run = 2176}, + {bits = 12, code = 0b000000010110, run = 2240}, + {bits = 12, code = 0b000000010111, run = 2304}, + {bits = 12, code = 0b000000011100, run = 2368}, + {bits = 12, code = 0b000000011101, run = 2432}, + {bits = 12, code = 0b000000011110, run = 2496}, + {bits = 12, code = 0b000000011111, run = 2560}, + {bits = 12, code = 0b000000100100, run = 52}, + {bits = 12, code = 0b000000100111, run = 55}, + {bits = 12, code = 0b000000101000, run = 56}, + {bits = 12, code = 0b000000101011, run = 59}, + {bits = 12, code = 0b000000101100, run = 60}, + {bits = 12, code = 0b000000110011, run = 320}, + {bits = 12, code = 0b000000110100, run = 384}, + {bits = 12, code = 0b000000110101, run = 448}, + {bits = 12, code = 0b000000110111, run = 53}, + {bits = 12, code = 0b000000111000, run = 54}, + {bits = 12, code = 0b000001010010, run = 50}, + {bits = 12, code = 0b000001010011, run = 51}, + {bits = 12, code = 0b000001010100, run = 44}, + {bits = 12, code = 0b000001010101, run = 45}, + {bits = 12, code = 0b000001010110, run = 46}, + {bits = 12, code = 0b000001010111, run = 47}, + {bits = 12, code = 0b000001011000, run = 57}, + {bits = 12, code = 0b000001011001, run = 58}, + {bits = 12, code = 0b000001011010, run = 61}, + {bits = 12, code = 0b000001011011, run = 256}, + {bits = 12, code = 0b000001100100, run = 48}, + {bits = 12, code = 0b000001100101, run = 49}, + {bits = 12, code = 0b000001100110, run = 62}, + {bits = 12, code = 0b000001100111, run = 63}, + {bits = 12, code = 0b000001101000, run = 30}, + {bits = 12, code = 0b000001101001, run = 31}, + {bits = 12, code = 0b000001101010, run = 32}, + {bits = 12, code = 0b000001101011, run = 33}, + {bits = 12, code = 0b000001101100, run = 40}, + {bits = 12, code = 0b000001101101, run = 41}, + {bits = 12, code = 0b000011001000, run = 128}, + {bits = 12, code = 0b000011001001, run = 192}, + {bits = 12, code = 0b000011001010, run = 26}, + {bits = 12, code = 0b000011001011, run = 27}, + {bits = 12, code = 0b000011001100, run = 28}, + {bits = 12, code = 0b000011001101, run = 29}, + {bits = 12, code = 0b000011010010, run = 34}, + {bits = 12, code = 0b000011010011, run = 35}, + {bits = 12, code = 0b000011010100, run = 36}, + {bits = 12, code = 0b000011010101, run = 37}, + {bits = 12, code = 0b000011010110, run = 38}, + {bits = 12, code = 0b000011010111, run = 39}, + {bits = 12, code = 0b000011011010, run = 42}, + {bits = 12, code = 0b000011011011, run = 43}, + {bits = 13, code = 0b0000001001010, run = 640}, + {bits = 13, code = 0b0000001001011, run = 704}, + {bits = 13, code = 0b0000001001100, run = 768}, + {bits = 13, code = 0b0000001001101, run = 832}, + {bits = 13, code = 0b0000001010010, run = 1280}, + {bits = 13, code = 0b0000001010011, run = 1344}, + {bits = 13, code = 0b0000001010100, run = 1408}, + {bits = 13, code = 0b0000001010101, run = 1472}, + {bits = 13, code = 0b0000001011010, run = 1536}, + {bits = 13, code = 0b0000001011011, run = 1600}, + {bits = 13, code = 0b0000001100100, run = 1664}, + {bits = 13, code = 0b0000001100101, run = 1728}, + {bits = 13, code = 0b0000001101100, run = 512}, + {bits = 13, code = 0b0000001101101, run = 576}, + {bits = 13, code = 0b0000001110010, run = 896}, + {bits = 13, code = 0b0000001110011, run = 960}, + {bits = 13, code = 0b0000001110100, run = 1024}, + {bits = 13, code = 0b0000001110101, run = 1088}, + {bits = 13, code = 0b0000001110110, run = 1152}, + {bits = 13, code = 0b0000001110111, run = 1216}, +} + +// ccitt_lookup reads one run-length code. The tables are sorted by code +// length, so peeking once per distinct length and walking them in order +// decodes the shortest matching code -- which, the code being prefix-free, is +// the only matching code. +@(private) +ccitt_lookup :: proc(b: ^Ccitt_Bits, table: []Ccitt_Code) -> (run: int, ok: bool) { + peeked_len := -1 + peeked: u32 = 0 + for e in table { + n := int(e.bits) + if n != peeked_len { + peeked_len = n + peeked = ccitt_bits_peek(b, n) + } + if u32(e.code) == peeked { + // The zero padding past the end of the data can complete a code + // that was never written; refuse it rather than invent pixels. + if ccitt_bits_left(b) < n { + return 0, false + } + b.pos += n + return int(e.run), true + } + } + return 0, false +} + +// ccitt_run reads a complete run: zero or more make-up codes followed by one +// terminating code. The total is clamped to `limit`, which is the row width: +// a run longer than the row is malformed and clamping it is cheaper than +// carrying an overflowing position around. +@(private) +ccitt_run :: proc(b: ^Ccitt_Bits, black: bool, limit: int) -> (run: int, ok: bool) { + table := CCITT_WHITE[:] + if black { + table = CCITT_BLACK[:] + } + total := 0 + for _ in 0 ..< MAX_CCITT_MAKEUP_CHAIN { + r, got := ccitt_lookup(b, table) + if !got { + return 0, false + } + total += r + if total > limit { + total = limit + } + if r < 64 { + return total, true + } + } + return 0, false +} + +// --------------------------------------------------------------------------- +// Two-dimensional coding modes (T.4 table 4 / T.6) +// --------------------------------------------------------------------------- + +@(private) +Ccitt_Mode :: enum u8 { + Pass, // 0001 + Horizontal, // 001 + V0, // 1 + VR1, // 011 + VR2, // 000011 + VR3, // 0000011 + VL1, // 010 + VL2, // 000010 + VL3, // 0000010 + Extension, // 0000001 + 3 bits + Error, +} + +@(private, rodata) +CCITT_VERTICAL_DELTA := [Ccitt_Mode]int{ + .Pass = 0, .Horizontal = 0, .Extension = 0, .Error = 0, + .V0 = 0, .VR1 = 1, .VR2 = 2, .VR3 = 3, .VL1 = -1, .VL2 = -2, .VL3 = -3, +} + +// ccitt_mode reads one 2-D mode code. All of them fit in seven bits, and no +// mode code has more than six leading zeros, so seven zero bits mean either an +// EOL or garbage -- either way, not a mode. +@(private) +ccitt_mode :: proc(b: ^Ccitt_Bits) -> (mode: Ccitt_Mode, ok: bool) { + v := ccitt_bits_peek(b, 7) + m: Ccitt_Mode + n: int + switch { + case v & 0b1000000 != 0: m, n = .V0, 1 + case v >> 4 == 0b011: m, n = .VR1, 3 + case v >> 4 == 0b010: m, n = .VL1, 3 + case v >> 4 == 0b001: m, n = .Horizontal, 3 + case v >> 3 == 0b0001: m, n = .Pass, 4 + case v >> 1 == 0b000011: m, n = .VR2, 6 + case v >> 1 == 0b000010: m, n = .VL2, 6 + case v == 0b0000011: m, n = .VR3, 7 + case v == 0b0000010: m, n = .VL3, 7 + case v == 0b0000001: m, n = .Extension, 7 + case: return .Error, false + } + if ccitt_bits_left(b) < n { + return .Error, false + } + b.pos += n + return m, true +} + +// ccitt_try_eol consumes an EOL (eleven or more zero bits then a one) together +// with any fill that precedes it, and reports whether it found one. On failure +// the reader is left exactly where it was: no code in either run-length table +// has eight leading zeros, so a genuine code can never be mistaken for fill. +@(private) +ccitt_try_eol :: proc(b: ^Ccitt_Bits) -> bool { + save := b.pos + zeros := 0 + for zeros < MAX_CCITT_FILL_BITS && ccitt_bits_left(b) > 0 && ccitt_bits_peek(b, 1) == 0 { + b.pos += 1 + zeros += 1 + } + if zeros >= 11 && ccitt_bits_left(b) > 0 && ccitt_bits_peek(b, 1) == 1 { + b.pos += 1 + return true + } + b.pos = save + return false +} + +// --------------------------------------------------------------------------- +// Rows +// +// A row is decoded into its changing elements: the positions, ascending, at +// which the colour changes, starting from white at pixel 0. Element 0, 2, 4 … +// are white-to-black transitions and 1, 3, 5 … are black-to-white ones. That +// is the form the 2-D coder needs for the *next* row's reference line, and it +// turns painting into a walk over black spans. +// --------------------------------------------------------------------------- + +// ccitt_find_b locates b1, the first changing element on the reference line to +// the right of a0 whose colour is opposite to the current colour, and b2, the +// element after it. `cursor` is carried between calls within a row: a0 never +// decreases, so the scan never has to restart. +@(private) +ccitt_find_b :: proc(ref: []int, ref_n, columns, a0, color: int, cursor: ^int) -> (b1: int, b2: int) { + i := cursor^ + if i < 0 { + i = 0 + } + for i < ref_n && ref[i] <= a0 { + i += 1 + } + cursor^ = i + // ref[i] has colour black for even i, white for odd i; we want the + // opposite of `color`, which is the same parity as `color` itself. + if (i & 1) != (color & 1) { + i += 1 + } + b1 = columns + if i < ref_n { + b1 = ref[i] + } + b2 = columns + if i + 1 < ref_n { + b2 = ref[i + 1] + } + return +} + +// ccitt_row_1d decodes one modified-Huffman row. `ok` is false when a code was +// unreadable, in which case the elements decoded so far are still returned. +@(private) +ccitt_row_1d :: proc(b: ^Ccitt_Bits, cur: []int, columns: int) -> (n: int, ok: bool) { + a0 := 0 + black := false + // A row cannot hold more than `columns` runs of non-zero length, and a + // zero-length run costs at least eight bits, so this only ever fires on + // deliberately hostile data. + fuel := 2 * columns + 64 + for a0 < columns && n < len(cur) && fuel > 0 { + fuel -= 1 + run, got := ccitt_run(b, black, columns) + if !got { + return n, false + } + a0 = min(a0 + run, columns) + cur[n] = a0 + n += 1 + black = !black + } + return n, true +} + +// ccitt_row_2d decodes one modified-READ row against reference line `ref`. +@(private) +ccitt_row_2d :: proc(b: ^Ccitt_Bits, cur: []int, ref: []int, ref_n, columns: int) -> (n: int, ok: bool) { + a0 := -1 // an imaginary white element just left of the row + black := false + cursor := 0 + fuel := 2 * columns + 64 + for a0 < columns && fuel > 0 { + fuel -= 1 + color := 0 + if black { + color = 1 + } + mode, got := ccitt_mode(b) + if !got { + return n, false + } + switch mode { + case .Pass: + // The run continues past b2; no changing element is created. + _, b2 := ccitt_find_b(ref, ref_n, columns, a0, color, &cursor) + a0 = b2 + + case .Horizontal: + start := max(a0, 0) + r1, ok1 := ccitt_run(b, black, columns) + if !ok1 { + return n, false + } + r2, ok2 := ccitt_run(b, !black, columns) + if !ok2 { + return n, false + } + if n + 2 > len(cur) { + return n, false + } + a1 := min(start + r1, columns) + a2 := min(a1 + r2, columns) + cur[n] = a1 + cur[n + 1] = a2 + n += 2 + a0 = a2 + + case .V0, .VR1, .VR2, .VR3, .VL1, .VL2, .VL3: + b1, _ := ccitt_find_b(ref, ref_n, columns, a0, color, &cursor) + a1 := clamp(b1 + CCITT_VERTICAL_DELTA[mode], 0, columns) + // A VL that would step left of a0 is malformed; pinning it to a0 + // keeps the element list sorted, which the next row relies on. + a1 = max(a1, a0) + if n >= len(cur) { + return n, false + } + cur[n] = a1 + n += 1 + a0 = a1 + black = !black + + case .Extension: + // 2-D extensions (T.4 4.2.1.3.5) carry uncompressed mode, which + // no PDF producer emits. Stop here rather than desynchronise. + return n, false + + case .Error: + return n, false + } + } + return n, true +} + +// ccitt_paint sets pixels [from, to) of a packed 1-bpp row. +@(private) +ccitt_paint :: proc(row: []byte, from, to: int, one: bool) { + lo := max(from, 0) + hi := min(to, len(row) * 8) + fill: u8 = one ? 0xFF : 0x00 + x := lo + for x < hi { + i := x >> 3 + off := uint(x & 7) + if off == 0 && hi - x >= 8 { + last := i + (hi - x) / 8 + for ; i < last; i += 1 { + row[i] = fill + } + x = last * 8 + continue + } + n := min(8 - int(off), hi - x) + mask := u8(0xFF >> off) & u8(0xFF << uint(8 - int(off) - n)) + if one { + row[i] |= mask + } else { + row[i] &~= mask + } + x += n + } +} + +// ccitt_emit_row paints one row of packed output from its changing elements. +// A colour that never changes back runs to the end of the row -- which is what +// pass mode at the right margin means, and also the least surprising way to +// pad a row whose data ran out early. +@(private) +ccitt_emit_row :: proc(row: []byte, cur: []int, n, columns: int, black_is_1: bool) { + white: u8 = black_is_1 ? 0x00 : 0xFF + for i in 0 ..< len(row) { + row[i] = white + } + for i := 0; i < n; i += 2 { + start := cur[i] + stop := columns + if i + 1 < n { + stop = cur[i + 1] + } + ccitt_paint(row, start, stop, black_is_1) + } +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +// ccitt_columns is the width `ccitt_decode` actually decodes at, and +// ccitt_row_bytes the stride it lays its output rows out at. +// +// /Columns comes out of a file, so it cannot be used as written: a +// non-positive value means the format's own default -- 1728, the width of a +// fax page -- and anything past MAX_CCITT_COLUMNS is clamped to what this +// decoder will allocate for. Both are exposed because the image layer has to +// read the buffer back at exactly this stride, and deriving it there from the +// raw /Columns instead is how the two silently disagree and every row after +// the first starts at the wrong byte. +@(private) +ccitt_columns :: proc(p: Ccitt_Params) -> int { + columns := p.columns + if columns <= 0 { + columns = 1728 + } + return min(columns, MAX_CCITT_COLUMNS) +} + +@(private) +ccitt_row_bytes :: proc(p: Ccitt_Params) -> int { + return (ccitt_columns(p) + 7) / 8 +} + +// ccitt_decode decodes CCITTFaxDecode data to packed 1-bit-per-pixel rows. +// See the convention described at the top of this file. +// +// The result is allocated with `allocator` and belongs to the caller; free it +// with `delete`. So is the row buffer it grows out of, which is sized from +// /Rows -- a claim rather than a measurement -- and released again when not +// one row decodes. Give it scratch, not a Document arena, whose `delete` is a +// silent no-op that would keep the whole claim until `close`; `apply_filter` +// in filters.odin is where that is arranged. +// +// An error is returned only when not a single row could be +// decoded -- anything less is reported by returning fewer rows than `p.rows`. +ccitt_decode :: proc(data: []byte, p: Ccitt_Params, allocator := context.allocator) -> (out: []byte, err: Error) { + columns := ccitt_columns(p) + row_bytes := ccitt_row_bytes(p) + + max_rows := MAX_CCITT_ROWS + if p.rows > 0 { + max_rows = min(max_rows, p.rows) + } + max_rows = min(max_rows, MAX_CCITT_BYTES / row_bytes) + // Every row consumes at least one bit (a lone V0), so the data itself caps + // the row count when /Rows is absent. + max_rows = min(max_rows, len(data) * 8) + if max_rows <= 0 { + return nil, .Bad_Ccitt_Data + } + + // Two changing-element lines. `columns + 8` slots leave room for the + // zero-length runs a horizontal code may legally contain without the + // pushes ever running off the end. + slots := columns + 8 + scratch, serr := make([]int, 2 * slots, context.allocator) + if serr != nil { + return nil, .Out_Of_Memory + } + defer delete(scratch, context.allocator) + ref := scratch[:slots] + cur := scratch[slots:] + ref_n := 0 + + init_rows := 8 + if p.rows > 0 { + init_rows = min(p.rows, max_rows) + } + buf, berr := make([dynamic]byte, 0, init_rows * row_bytes, allocator) + if berr != nil { + return nil, .Out_Of_Memory + } + + b := ccitt_bits_make(data) + two_d := p.k < 0 + rows_done := 0 + + for rows_done < max_rows { + if p.byte_align { + ccitt_bits_align(&b) + } + + // Fill, EOLs, and the 1-D/2-D tag bit that follows each EOL when + // /K > 0. Two EOLs with nothing between them are an RTC (G3) or an + // EOFB (G4): the image is over. + eols := 0 + for eols < 2 { + if !ccitt_try_eol(&b) { + break + } + eols += 1 + if p.k > 0 { + if ccitt_bits_left(&b) < 1 { + break + } + two_d = ccitt_bits_peek(&b, 1) == 0 + b.pos += 1 + } + } + if eols >= 2 || ccitt_bits_left(&b) <= 0 { + break + } + + n, ok := 0, false + if two_d { + n, ok = ccitt_row_2d(&b, cur, ref, ref_n, columns) + } else { + n, ok = ccitt_row_1d(&b, cur, columns) + } + if n == 0 { + // Not even one changing element: an unknown code or the end of + // the data, not a row. + break + } + + rerr := resize(&buf, len(buf) + row_bytes) + if rerr != nil { + break + } + ccitt_emit_row(buf[len(buf) - row_bytes:], cur, n, columns, p.black_is_1) + rows_done += 1 + + ref, cur = cur, ref + ref_n = n + + if !ok { + break + } + } + + if rows_done == 0 { + delete(buf) + return nil, .Bad_Ccitt_Data + } + _, _ = shrink(&buf) + return buf[:], .None +} diff --git a/filter_ccitt_test.odin b/filter_ccitt_test.odin new file mode 100644 index 0000000..05d1568 --- /dev/null +++ b/filter_ccitt_test.odin @@ -0,0 +1,1053 @@ +package pdf + +import "core:slice" +import "core:testing" + +// Test vectors come from two places. +// +// The Group 3 and Group 4 streams named G3_* and G4_* were produced by +// libtiff (through Pillow), so they are an independent encoder's idea of what +// these images look like; decoding them back to the source pixels checks the +// code tables and the 2-D coder against something that is not this file. +// The MH_* and MR_* streams are hand-assembled bit by bit from the code words +// in ITU-T T.4 tables 1-4, which is the only way to pin down EOL, fill, +// byte alignment, the /K > 0 tag bit and the malformed cases. + +// G4, 8 columns x 12 rows. Between them the rows use every 2-D mode: +// pass, horizontal, V0, VR1-VR3 and VL1-VL3, and end with an EOFB. +@(private="file", rodata) +G4_FULL := [?]byte{ + 0x93, 0x51, 0x59, 0x43, 0x94, 0x38, 0x4A, 0xAE, 0xC4, 0x38, 0x2A, 0x3D, 0x8C, 0xDB, 0x07, 0x09, + 0x46, 0x00, 0x20, 0x02, +} + +@(private="file", rodata) +G4_FULL_OUT := [?]byte{ + 0xFF, 0x00, 0xAA, 0x33, 0xFC, 0xE0, 0xCF, 0xFF, 0xF0, 0xFE, 0xF9, 0xFF, +} + +// The same image with /BlackIs1 true: every bit inverted. +@(private="file", rodata) +G4_FULL_OUT_INVERTED := [?]byte{ + 0x00, 0xFF, 0x55, 0xCC, 0x03, 0x1F, 0x30, 0x00, 0x0F, 0x01, 0x06, 0x00, +} + +// 200 columns x 5 rows: runs long enough to need make-up codes. +@(private="file", rodata) +G4_WIDE := [?]byte{ + 0x93, 0x50, 0xC9, 0x14, 0xEC, 0x54, 0x0F, 0x0D, 0x42, 0x6A, 0x07, 0x91, 0x4B, 0x03, 0xC8, 0x4C, + 0x9C, 0x37, 0x00, 0x10, 0x01, +} + +@(private="file", rodata) +G3_WIDE := [?]byte{ + 0x00, 0x15, 0xE6, 0x00, 0x26, 0xA1, 0x92, 0x28, 0x00, 0xEC, 0x54, 0x0F, 0x0D, 0x40, 0x01, 0x35, + 0x03, 0xC9, 0x2C, 0x0F, 0x20, 0x01, 0x5E, 0x60, +} + +@(private="file", rodata) +WIDE_OUT := [?]byte{ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +} + +// Group 3 1-D of the first four rows of G4_FULL, EOL-separated. +@(private="file", rodata) +G3_SMALL := [?]byte{ + 0x00, 0x19, 0x80, 0x09, 0xA8, 0xA0, 0x02, 0x3A, 0x1D, 0x0E, 0x87, 0x40, 0x02, 0x6B, 0xBE, 0xE0, +} + +@(private="file", rodata) +SMALL_OUT := [?]byte{ + 0xFF, 0x00, 0xAA, 0x33, +} + +// pass mode: the reference black run ends before the coding line changes. +@(private="file", rodata) +G4_PASS := [?]byte{ + 0x2F, 0xC6, 0x00, 0x20, 0x02, +} + +@(private="file", rodata) +G4_PASS_OUT := [?]byte{ + 0xCF, 0xFF, +} + +// VL1 then VR1. +@(private="file", rodata) +G4_VR1 := [?]byte{ + 0x2E, 0xE9, 0xC0, 0x04, 0x00, 0x40, +} + +@(private="file", rodata) +G4_VR1_OUT := [?]byte{ + 0xC3, 0x81, +} + +// VR2 then VL2. +@(private="file", rodata) +G4_VL2 := [?]byte{ + 0x23, 0x94, 0x30, 0xA0, 0x02, 0x00, 0x20, +} + +@(private="file", rodata) +G4_VL2_OUT := [?]byte{ + 0x81, 0xE7, +} + +// VL2, VL3. +@(private="file", rodata) +G4_VL3 := [?]byte{ + 0x0A, 0x0A, 0x00, 0x20, 0x02, +} + +@(private="file", rodata) +G4_VL3_OUT := [?]byte{ + 0xFC, 0xE0, +} + +// One 1-D row, eight white pixels, no EOL and no padding to speak of. +@(private="file", rodata) +MH_WHITE_ROW := [?]byte{ + 0x98, +} + +@(private="file", rodata) +MH_WHITE_ROW_OUT := [?]byte{ + 0xFF, +} + +// A black row still opens with a zero-length white run. +@(private="file", rodata) +MH_BLACK_ROW := [?]byte{ + 0x35, 0x14, +} + +@(private="file", rodata) +MH_BLACK_ROW_OUT := [?]byte{ + 0x00, +} + +// Alternating pixels: eight runs of one. +@(private="file", rodata) +MH_ALT_ROW := [?]byte{ + 0x1D, 0x0E, 0x87, 0x43, 0xA0, +} + +@(private="file", rodata) +MH_ALT_ROW_OUT := [?]byte{ + 0xAA, +} + +// EOLs with fill bits in front of them. +@(private="file", rodata) +MH_EOL_FILL := [?]byte{ + 0x00, 0x01, 0x98, 0x00, 0x26, 0xA2, 0x80, +} + +@(private="file", rodata) +MH_EOL_FILL_OUT := [?]byte{ + 0xFF, 0x00, +} + +// Two rows then a six-EOL return-to-control. +@(private="file", rodata) +MH_RTC := [?]byte{ + 0x00, 0x19, 0x80, 0x09, 0xA8, 0xA0, 0x02, 0x00, 0x20, 0x02, 0x00, 0x20, 0x02, 0x00, 0x20, +} + +@(private="file", rodata) +MH_RTC_OUT := [?]byte{ + 0xFF, 0x00, +} + +// A 100-pixel run in an 8-pixel row: truncated, not an error. +@(private="file", rodata) +MH_OVERLONG := [?]byte{ + 0xD8, 0xA8, +} + +@(private="file", rodata) +MH_OVERLONG_OUT := [?]byte{ + 0xFF, +} + +// A row that ends after three white pixels; the colour it changed to runs on. +@(private="file", rodata) +MH_SHORT := [?]byte{ + 0x80, +} + +@(private="file", rodata) +MH_SHORT_OUT := [?]byte{ + 0xE0, +} + +// 2000 white pixels: exercises the extended make-up codes (>= 1792). +@(private="file", rodata) +MH_WIDE_WHITE := [?]byte{ + 0x01, 0x2A, 0x80, +} + +@(private="file", rodata) +MH_WIDE_WHITE_OUT := [250]byte{0 ..= 249 = 0xFF} + +// 2000 black pixels: the extended make-up codes are shared by both colours. +@(private="file", rodata) +MH_WIDE_BLACK := [?]byte{ + 0x35, 0x01, 0x20, 0x5C, +} + +@(private="file", rodata) +MH_WIDE_BLACK_OUT := [250]byte{0 ..= 249 = 0x00} + +// /K > 0: EOL, 1-D tag, row; EOL, 2-D tag, horizontal-mode row. +@(private="file", rodata) +MR_MIXED := [?]byte{ + 0x00, 0x1C, 0xC0, 0x04, 0x4D, 0x45, +} + +@(private="file", rodata) +MR_MIXED_OUT := [?]byte{ + 0xFF, 0x00, +} + +// /K > 0 with a V0 row referring back to a 1-D row. +@(private="file", rodata) +MR_MIXED3 := [?]byte{ + 0x00, 0x1C, 0xC0, 0x05, 0x00, 0x11, 0x35, 0x14, +} + +@(private="file", rodata) +MR_MIXED3_OUT := [?]byte{ + 0xFF, 0xFF, 0x00, +} + +// /EncodedByteAlign: the same two rows, each starting on a byte boundary. +@(private="file", rodata) +MH_BYTE_ALIGN := [?]byte{ + 0x98, 0x35, 0x14, +} + +@(private="file", rodata) +MH_BYTE_ALIGN_OUT := [?]byte{ + 0xFF, 0x00, +} + + +// A page at the standard fax width, 1728 x 6, encoded by libtiff both +// ways. Solid rows and 900-pixel runs put the make-up codes, pass mode +// and the long-run paths to work at a size a real document uses. +@(private="file", rodata) +PAGE_G4 := [?]byte{ + 0x93, 0x50, 0x32, 0x86, 0xF2, 0x07, 0x26, 0xCD, 0x4B, 0x2D, 0x3B, 0x02, 0x60, 0x16, 0x19, 0x2D, + 0x01, 0xE6, 0x76, 0x96, 0x07, 0xE6, 0x4A, 0x41, 0x9C, 0xB7, 0x02, 0x0C, 0x8C, 0xC8, 0x60, 0x18, + 0x20, 0x60, 0x5C, 0xA7, 0x22, 0xDC, 0x5C, 0x32, 0x11, 0x32, 0x02, 0xC3, 0x31, 0x44, 0x4A, 0xA0, + 0x1E, 0x08, 0x24, 0xC0, 0x2C, 0x06, 0x40, 0xF0, 0xD0, 0xBF, 0x89, 0x04, 0xE0, 0x42, 0x49, 0x86, + 0xC3, 0x21, 0x20, 0x3C, 0x30, 0x76, 0x14, 0x06, 0x00, 0x04, 0x00, 0x40, +} + +@(private="file", rodata) +PAGE_G3 := [?]byte{ + 0x00, 0x14, 0xD9, 0xA8, 0x00, 0x9A, 0x81, 0x94, 0x37, 0x00, 0x13, 0x50, 0x39, 0x36, 0x6A, 0x58, + 0x00, 0xB4, 0xEC, 0x09, 0x80, 0x58, 0x00, 0x32, 0x5A, 0x03, 0xCF, 0x69, 0x60, 0x7F, 0x25, 0x20, + 0xCE, 0xDC, 0x08, 0x32, 0x39, 0x0C, 0x03, 0x00, 0x60, 0x5D, 0x39, 0x37, 0x17, 0x0C, 0x84, 0x00, + 0x26, 0x40, 0x59, 0x31, 0x4D, 0x85, 0xC0, 0xF0, 0x4D, 0x8D, 0x82, 0xC2, 0xE7, 0x03, 0xC3, 0x42, + 0xFE, 0x05, 0x9D, 0x92, 0x06, 0xC9, 0x09, 0x01, 0xE1, 0x8D, 0x85, 0x01, 0x80, +} + +@(private="file", rodata) +PAGE_OUT := [?]byte{ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, + 0x7F, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, +} + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +@(private="file") +expect_decode :: proc(t: ^testing.T, name: string, data: []byte, p: Ccitt_Params, want: []byte) { + got, err := ccitt_decode(data, p) + defer delete(got) + testing.expectf(t, err == .None, "%s: %v", name, error_string(err)) + testing.expectf(t, slice.equal(got, want), "%s: got % 02X, want % 02X", name, got, want) +} + +// --------------------------------------------------------------------------- +// Bit reader +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_bits_peek :: proc(t: ^testing.T) { + data := [?]byte{0b1011_0010, 0b0100_1111, 0b1000_0001} + b := ccitt_bits_make(data[:]) + testing.expect_value(t, ccitt_bits_left(&b), 24) + testing.expect_value(t, ccitt_bits_peek(&b, 1), u32(1)) + testing.expect_value(t, ccitt_bits_peek(&b, 4), u32(0b1011)) + testing.expect_value(t, ccitt_bits_peek(&b, 8), u32(0b1011_0010)) + testing.expect_value(t, ccitt_bits_peek(&b, 16), u32(0b1011_0010_0100_1111)) + testing.expect_value(t, ccitt_bits_peek(&b, 0), u32(0)) + + // A peek that straddles two bytes, from an unaligned position. + ccitt_bits_skip(&b, 5) + testing.expect_value(t, ccitt_bits_peek(&b, 6), u32(0b010_010)) + testing.expect_value(t, ccitt_bits_left(&b), 19) + + // Past the end reads as zero bits, and skipping past the end clamps. + ccitt_bits_skip(&b, 18) + testing.expect_value(t, ccitt_bits_left(&b), 1) + testing.expect_value(t, ccitt_bits_peek(&b, 8), u32(0b1000_0000)) + ccitt_bits_skip(&b, 1000) + testing.expect_value(t, ccitt_bits_left(&b), 0) + testing.expect_value(t, ccitt_bits_peek(&b, 12), u32(0)) +} + +@(test) +test_ccitt_bits_align :: proc(t: ^testing.T) { + data := [?]byte{0xFF, 0xFF, 0xFF} + b := ccitt_bits_make(data[:]) + ccitt_bits_align(&b) + testing.expect_value(t, b.pos, 0) // already aligned: no movement + ccitt_bits_skip(&b, 1) + ccitt_bits_align(&b) + testing.expect_value(t, b.pos, 8) + ccitt_bits_skip(&b, 7) + ccitt_bits_align(&b) + testing.expect_value(t, b.pos, 16) + ccitt_bits_skip(&b, 7) + ccitt_bits_align(&b) + testing.expect_value(t, b.pos, 24) // clamped to the end, not past it +} + +// --------------------------------------------------------------------------- +// Code tables +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_tables_are_prefix_free :: proc(t: ^testing.T) { + // The decoder walks the tables in length order and takes the first match, + // which is only the right answer because no code is a prefix of another. + // It also relies on no code having eight leading zeros, so that fill bits + // and EOLs can never be confused with data. + check :: proc(t: ^testing.T, table: []Ccitt_Code, name: string) { + for a, i in table { + testing.expectf(t, a.bits >= 2 && a.bits <= 13, "%s[%d]: bad length %d", name, i, a.bits) + testing.expectf(t, u32(a.code) < 1 << uint(a.bits), "%s[%d]: code wider than its length", name, i) + if a.bits >= 8 { + lead := u32(a.code) >> uint(int(a.bits) - 8) + testing.expectf(t, lead != 0, "%s[%d]: eight leading zeros collide with EOL", name, i) + } + if i > 0 { + prev := table[i - 1] + ordered := prev.bits < a.bits || (prev.bits == a.bits && prev.code < a.code) + testing.expectf(t, ordered, "%s[%d]: table is not sorted by (bits, code)", name, i) + } + for b, j in table { + if i == j || b.bits < a.bits { + continue + } + shifted := u32(b.code) >> uint(int(b.bits) - int(a.bits)) + testing.expectf(t, !(b.bits > a.bits && shifted == u32(a.code)), + "%s: run %d is a prefix of run %d", name, a.run, b.run) + testing.expectf(t, !(b.bits == a.bits && b.code == a.code), + "%s: runs %d and %d share a code", name, a.run, b.run) + } + } + } + check(t, CCITT_WHITE[:], "white") + check(t, CCITT_BLACK[:], "black") +} + +@(test) +test_ccitt_tables_are_complete :: proc(t: ^testing.T) { + // Terminating codes 0-63, make-up codes 64-1728 in steps of 64, and the + // extended make-up codes 1792-2560, in both tables. + check :: proc(t: ^testing.T, table: []Ccitt_Code, name: string) { + seen: map[int]bool + defer delete(seen) + for e in table { + seen[int(e.run)] = true + } + for r in 0 ..< 64 { + testing.expectf(t, seen[r], "%s: terminating run %d missing", name, r) + } + for r := 64; r <= 2560; r += 64 { + if r > 1728 && r < 1792 { + continue + } + testing.expectf(t, seen[r], "%s: make-up run %d missing", name, r) + } + testing.expectf(t, len(table) == 104, "%s: %d codes, want 104", name, len(table)) + } + check(t, CCITT_WHITE[:], "white") + check(t, CCITT_BLACK[:], "black") +} + +@(test) +test_ccitt_lookup :: proc(t: ^testing.T) { + Case :: struct { + bits: []byte, + avail: int, // bits of `bits` that are really there; 0 means all + black: bool, + run: int, + used: int, + ok: bool, + } + cases := []Case{ + // Terminating codes, shortest and longest of each colour. + {{0b0111_0000}, 0, false, 2, 4, true}, // white 2: 0111 + {{0b0011_0101}, 0, false, 0, 8, true}, // white 0: 00110101 + {{0b1100_0000}, 0, true, 2, 2, true}, // black 2: 11 + {{0b0000_1101, 0b1100_0000}, 0, true, 0, 10, true}, // black 0: 0000110111 + // Make-up codes. + {{0b1101_1000}, 0, false, 64, 5, true}, // white 64: 11011 + {{0b0100_1101, 0b1000_0000}, 0, false, 1728, 9, true}, // white 1728: 010011011 + {{0b0000_0011, 0b1100_0000}, 0, true, 64, 10, true}, // black 64: 0000001111 + // Extended make-up codes, shared by both colours. + {{0b0000_0001, 0b0000_0000}, 0, false, 1792, 11, true}, + {{0b0000_0001, 0b0000_0000}, 0, true, 1792, 11, true}, + {{0b0000_0001, 0b1111_0000}, 0, false, 2560, 12, true}, + {{0b0000_0001, 0b1111_0000}, 0, true, 2560, 12, true}, + // No code is all zeros, so fill and EOL never look like a run. + {{0x00, 0x00}, 0, false, 0, 0, false}, + {{0x00, 0x00}, 0, true, 0, 0, false}, + // Codes the zero padding would otherwise complete: only the first + // four bits of white 0 (00110101) and of black 0 (0000110111) are + // really present, and neither may be invented. + {{0b0011_0101}, 4, false, 0, 0, false}, + {{0b0000_1101, 0b1100_0000}, 4, true, 0, 0, false}, + } + for c, i in cases { + b := ccitt_bits_make(c.bits) + if c.avail > 0 { + b.end = c.avail + } + table := c.black ? CCITT_BLACK[:] : CCITT_WHITE[:] + run, ok := ccitt_lookup(&b, table) + testing.expectf(t, ok == c.ok, "case %d: ok %v, want %v", i, ok, c.ok) + if c.ok { + testing.expectf(t, run == c.run, "case %d: run %d, want %d", i, run, c.run) + testing.expectf(t, b.pos == c.used, "case %d: used %d bits, want %d", i, b.pos, c.used) + } + } +} + +@(test) +test_ccitt_run_chains_makeups :: proc(t: ^testing.T) { + // white 1728 (010011011) + white 63 (00110100) = 1791, the largest run + // T.4 can code without the extended table. + data := [?]byte{0b0100_1101, 0b1001_1010, 0b0000_0000} + b := ccitt_bits_make(data[:]) + run, ok := ccitt_run(&b, false, 4096) + testing.expect(t, ok) + testing.expect_value(t, run, 1791) + testing.expect_value(t, b.pos, 17) + + // The same run clamped by the row width. + b = ccitt_bits_make(data[:]) + run, ok = ccitt_run(&b, false, 100) + testing.expect(t, ok) + testing.expect_value(t, run, 100) +} + +// --------------------------------------------------------------------------- +// 2-D mode codes and EOL +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_mode :: proc(t: ^testing.T) { + Case :: struct { + byte: byte, + mode: Ccitt_Mode, + used: int, + } + cases := []Case{ + {0b1000_0000, .V0, 1}, + {0b0110_0000, .VR1, 3}, + {0b0100_0000, .VL1, 3}, + {0b0010_0000, .Horizontal, 3}, + {0b0001_0000, .Pass, 4}, + {0b0000_1100, .VR2, 6}, + {0b0000_1000, .VL2, 6}, + {0b0000_0110, .VR3, 7}, + {0b0000_0100, .VL3, 7}, + {0b0000_0010, .Extension, 7}, + } + for c, i in cases { + data := [?]byte{c.byte, 0xFF} + b := ccitt_bits_make(data[:]) + mode, ok := ccitt_mode(&b) + testing.expectf(t, ok, "case %d: not decoded", i) + testing.expectf(t, mode == c.mode, "case %d: %v, want %v", i, mode, c.mode) + testing.expectf(t, b.pos == c.used, "case %d: used %d bits, want %d", i, b.pos, c.used) + } + + // Seven zero bits is not a mode; it is an EOL or garbage. + zeros := [?]byte{0x00, 0x00} + b := ccitt_bits_make(zeros[:]) + _, ok := ccitt_mode(&b) + testing.expect(t, !ok) + testing.expect_value(t, b.pos, 0) + + // A mode the zero padding would complete is refused. + short := [?]byte{0b0100_0000} + b = ccitt_bits_make(short[:]) + b.end = 2 + _, ok = ccitt_mode(&b) + testing.expect(t, !ok) +} + +@(test) +test_ccitt_try_eol :: proc(t: ^testing.T) { + // A bare EOL. + eol := [?]byte{0b0000_0000, 0b0001_0000} + b := ccitt_bits_make(eol[:]) + testing.expect(t, ccitt_try_eol(&b)) + testing.expect_value(t, b.pos, 12) + + // Fill bits in front of it are absorbed. + filled := [?]byte{0x00, 0x00, 0b0000_0001, 0b0000_0000} + b = ccitt_bits_make(filled[:]) + testing.expect(t, ccitt_try_eol(&b)) + testing.expect_value(t, b.pos, 24) + + // A run code is not an EOL, and the reader must not have moved. + code := [?]byte{0b0000_0100, 0b0000_0000} // white 45: 00000100 + b = ccitt_bits_make(code[:]) + testing.expect(t, !ccitt_try_eol(&b)) + testing.expect_value(t, b.pos, 0) + + // Trailing zero fill at the end of the data is not an EOL either: there + // is no terminating one bit, so the position is restored. + tail := [?]byte{0x00, 0x00, 0x00} + b = ccitt_bits_make(tail[:]) + testing.expect(t, !ccitt_try_eol(&b)) + testing.expect_value(t, b.pos, 0) +} + +// --------------------------------------------------------------------------- +// Painting +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_paint :: proc(t: ^testing.T) { + row := [3]byte{} + for i in 0 ..< 3 { + row[i] = 0xFF + } + ccitt_paint(row[:], 3, 5, false) + testing.expect_value(t, row[0], byte(0b1110_0111)) + + ccitt_paint(row[:], 8, 24, false) + testing.expect_value(t, row[1], byte(0x00)) + testing.expect_value(t, row[2], byte(0x00)) + + ccitt_paint(row[:], 12, 20, true) + testing.expect_value(t, row[1], byte(0b0000_1111)) + testing.expect_value(t, row[2], byte(0b1111_0000)) + + // Out-of-range spans are clipped rather than trusted. + ccitt_paint(row[:], -100, 100, true) + testing.expect_value(t, row[0], byte(0xFF)) + testing.expect_value(t, row[2], byte(0xFF)) + ccitt_paint(row[:], 5, 5, false) + testing.expect_value(t, row[0], byte(0xFF)) +} + +// --------------------------------------------------------------------------- +// Whole images +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_g4 :: proc(t: ^testing.T) { + Case :: struct { + name: string, + data: []byte, + columns: int, + rows: int, + want: []byte, + } + cases := []Case{ + {"full", G4_FULL[:], 8, 12, G4_FULL_OUT[:]}, + {"full, rows unknown", G4_FULL[:], 8, 0, G4_FULL_OUT[:]}, + {"pass mode", G4_PASS[:], 8, 2, G4_PASS_OUT[:]}, + {"vr1", G4_VR1[:], 8, 2, G4_VR1_OUT[:]}, + {"vl2", G4_VL2[:], 8, 2, G4_VL2_OUT[:]}, + {"vl3", G4_VL3[:], 8, 2, G4_VL3_OUT[:]}, + {"wide, make-up codes", G4_WIDE[:], 200, 5, WIDE_OUT[:]}, + {"wide, rows unknown", G4_WIDE[:], 200, 0, WIDE_OUT[:]}, + } + for c in cases { + p := ccitt_default_params() + p.k = -1 + p.columns = c.columns + p.rows = c.rows + expect_decode(t, c.name, c.data, p, c.want) + } +} + +@(test) +test_ccitt_g3_1d :: proc(t: ^testing.T) { + Case :: struct { + name: string, + data: []byte, + columns: int, + rows: int, + want: []byte, + } + cases := []Case{ + {"white row", MH_WHITE_ROW[:], 8, 1, MH_WHITE_ROW_OUT[:]}, + {"black row", MH_BLACK_ROW[:], 8, 1, MH_BLACK_ROW_OUT[:]}, + {"alternating pixels", MH_ALT_ROW[:], 8, 1, MH_ALT_ROW_OUT[:]}, + {"eol and fill", MH_EOL_FILL[:], 8, 0, MH_EOL_FILL_OUT[:]}, + {"return to control", MH_RTC[:], 8, 0, MH_RTC_OUT[:]}, + {"libtiff group 3", G3_SMALL[:], 8, 0, SMALL_OUT[:]}, + {"libtiff group 3, wide", G3_WIDE[:], 200, 0, WIDE_OUT[:]}, + {"extended make-up, white", MH_WIDE_WHITE[:], 2000, 1, MH_WIDE_WHITE_OUT[:]}, + {"extended make-up, black", MH_WIDE_BLACK[:], 2000, 1, MH_WIDE_BLACK_OUT[:]}, + } + for c in cases { + p := ccitt_default_params() + p.k = 0 + p.columns = c.columns + p.rows = c.rows + expect_decode(t, c.name, c.data, p, c.want) + } +} + +@(test) +test_ccitt_fax_page :: proc(t: ^testing.T) { + // The same page encoded two different ways by libtiff must come back out + // of the two decoding paths identically. A mistake in one run-length + // table shows up here even when the 2-D coder is stepping over it. + p := ccitt_default_params() + p.columns = 1728 + p.rows = 6 + + p.k = -1 + expect_decode(t, "1728-wide page, group 4", PAGE_G4[:], p, PAGE_OUT[:]) + p.k = 0 + expect_decode(t, "1728-wide page, group 3", PAGE_G3[:], p, PAGE_OUT[:]) + + // /Rows absent: the EOFB and RTC that libtiff appends have to end it in + // the right place on their own. + p.rows = 0 + p.k = -1 + expect_decode(t, "group 4, rows unknown", PAGE_G4[:], p, PAGE_OUT[:]) +} + +@(test) +test_ccitt_max_changing_elements :: proc(t: ^testing.T) { + // 1728 alternating pixels is the most changing elements a standard fax + // row can hold: 1728 of them, against scratch lines sized for the row. + // Anything that miscounted those lines overruns here. + data: [dynamic]byte + defer delete(data) + acc: u32 = 0 + n_bits := 0 + put :: proc(data: ^[dynamic]byte, acc: ^u32, n_bits: ^int, code: u32, n: int) { + acc^ = acc^ << uint(n) | code + n_bits^ += n + for n_bits^ >= 8 { + append(data, byte(acc^ >> uint(n_bits^ - 8))) + n_bits^ -= 8 + } + } + for _ in 0 ..< 864 { + put(&data, &acc, &n_bits, 0b000111, 6) // white 1 + put(&data, &acc, &n_bits, 0b010, 3) // black 1 + } + if n_bits > 0 { + append(&data, byte(acc << uint(8 - n_bits))) + } + + p := ccitt_default_params() + p.k = 0 + p.columns = 1728 + p.rows = 1 + got, err := ccitt_decode(data[:], p) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 216) + for b, i in got { + testing.expectf(t, b == 0xAA, "byte %d is %02X, want AA", i, b) + } +} + +@(test) +test_ccitt_mixed_k :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.k = 4 + p.columns = 8 + expect_decode(t, "1-D then 2-D", MR_MIXED[:], p, MR_MIXED_OUT[:]) + expect_decode(t, "1-D, V0, horizontal", MR_MIXED3[:], p, MR_MIXED3_OUT[:]) +} + +@(test) +test_ccitt_byte_align :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.k = 0 + p.columns = 8 + p.byte_align = true + expect_decode(t, "byte aligned rows", MH_BYTE_ALIGN[:], p, MH_BYTE_ALIGN_OUT[:]) + + // Without the flag the second row is read starting at bit 5, where the + // padding of the first row is, so it decodes to something else entirely. + // The point is only that it still terminates and returns rows. + p.byte_align = false + got, err := ccitt_decode(MH_BYTE_ALIGN[:], p) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect(t, len(got) > 0) +} + +@(test) +test_ccitt_black_is_1 :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.k = -1 + p.columns = 8 + p.rows = 12 + p.black_is_1 = true + expect_decode(t, "black is 1", G4_FULL[:], p, G4_FULL_OUT_INVERTED[:]) +} + +@(test) +test_ccitt_default_columns :: proc(t: ^testing.T) { + // /Columns absent means 1728, so one all-white row is 216 bytes. + // 1728 = 1728 make-up + 0 terminating. + data := [?]byte{0b0100_1101, 0b1001_1010, 0b1000_0000} + p := ccitt_default_params() + p.columns = 0 + p.rows = 1 + got, err := ccitt_decode(data[:], p) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 216) + for b, i in got { + testing.expectf(t, b == 0xFF, "byte %d is %02X, want FF", i, b) + } +} + +// --------------------------------------------------------------------------- +// Tolerances +// --------------------------------------------------------------------------- + +@(test) +test_ccitt_row_length_tolerance :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.k = 0 + p.columns = 8 + + // A run longer than the row is clipped and the row is still produced. + expect_decode(t, "over-long run", MH_OVERLONG[:], p, MH_OVERLONG_OUT[:]) + + // A row whose data ends early keeps the colour it had reached. + expect_decode(t, "short row", MH_SHORT[:], p, MH_SHORT_OUT[:]) +} + +@(test) +test_ccitt_rows_limit :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.k = -1 + p.columns = 8 + + // Fewer rows than the data holds: decoding stops at /Rows. + p.rows = 3 + expect_decode(t, "rows = 3", G4_FULL[:], p, G4_FULL_OUT[:3]) + + // More rows than the data holds: the rows that decoded are returned + // rather than an error, and the caller can tell from the length. + p.rows = 500 + got, err := ccitt_decode(G4_FULL[:], p) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 12) + + // An absurd /Rows is a clamp, not an allocation. + p.rows = 1 << 40 + got2, err2 := ccitt_decode(G4_FULL[:], p) + defer delete(got2) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, len(got2), 12) +} + +@(test) +test_ccitt_absurd_columns :: proc(t: ^testing.T) { + // /Columns is attacker-controlled: it must be clamped before it sizes + // either the scratch lines or the output. + for columns in ([?]int{-1, 0, 1, 1 << 30, max(int)}) { + p := ccitt_default_params() + p.k = -1 + p.columns = columns + p.rows = 1 + got, err := ccitt_decode(G4_FULL[:], p) + defer delete(got) + testing.expectf(t, err == .None || err == .Bad_Ccitt_Data, + "columns %d: %v", columns, error_string(err)) + testing.expectf(t, len(got) <= MAX_CCITT_BYTES, "columns %d: %d bytes", columns, len(got)) + } +} + +@(test) +test_ccitt_no_data :: proc(t: ^testing.T) { + p := ccitt_default_params() + p.columns = 8 + + // Nothing at all. + got, err := ccitt_decode(nil, p) + testing.expect_value(t, err, Error.Bad_Ccitt_Data) + testing.expect_value(t, len(got), 0) + + // All zeros: no run code is all zeros, so not one row can be made. + zeros := [16]byte{} + got2, err2 := ccitt_decode(zeros[:], p) + defer delete(got2) + testing.expect_value(t, err2, Error.Bad_Ccitt_Data) + + // The same in Group 4, where seven zero bits is not a mode code. + p.k = -1 + got3, err3 := ccitt_decode(zeros[:], p) + defer delete(got3) + testing.expect_value(t, err3, Error.Bad_Ccitt_Data) +} + +@(test) +test_ccitt_truncated :: proc(t: ^testing.T) { + // Every prefix of a good image must decode to some prefix of the rows, + // never hang and never return more than the whole image. + for n in 0 ..= len(G4_FULL) { + p := ccitt_default_params() + p.k = -1 + p.columns = 8 + p.rows = 12 + got, err := ccitt_decode(G4_FULL[:n], p) + defer delete(got) + testing.expectf(t, err == .None || err == .Bad_Ccitt_Data, "prefix %d: %v", n, error_string(err)) + testing.expectf(t, len(got) <= 12, "prefix %d: %d rows", n, len(got)) + // Whatever survived is a prefix of the real image, except possibly + // the last row, which may have been cut off mid-way. + whole := max(len(got) - 1, 0) + testing.expectf(t, slice.equal(got[:whole], G4_FULL_OUT[:whole]), + "prefix %d: % 02X", n, got) + } +} + +@(test) +test_ccitt_bit_flips :: proc(t: ^testing.T) { + // Corrupt data must still terminate, must not read out of bounds and must + // not hand back more rows than were asked for. What it decodes to is not + // interesting; that it always answers is. + for k in ([?]int{-1, 0, 4}) { + for i in 0 ..< len(G4_FULL) * 8 { + data: [len(G4_FULL)]byte + copy(data[:], G4_FULL[:]) + data[i / 8] ~= 1 << uint(7 - i % 8) + + p := ccitt_default_params() + p.k = k + p.columns = 8 + p.rows = 12 + p.byte_align = i % 3 == 0 + got, err := ccitt_decode(data[:], p) + defer delete(got) + testing.expectf(t, err == .None || err == .Bad_Ccitt_Data, "k %d bit %d: %v", k, i, error_string(err)) + testing.expectf(t, len(got) <= 12, "k %d bit %d: %d bytes", k, i, len(got)) + } + } +} + +@(test) +test_ccitt_hostile_streams :: proc(t: ^testing.T) { + Case :: struct { + name: string, + data: []byte, + k: int, + } + // 0xFF is V0 eight times over, 0x11 is pass mode, 0x24 is horizontal + // mode with the shortest black codes, 0x08 is VL2/VL3 territory. Each of + // these repeated is a stream that only a fuel counter stops. + all_v0 := [64]byte{} + all_pass := [64]byte{} + all_h := [64]byte{} + all_vl := [64]byte{} + for i in 0 ..< 64 { + all_v0[i] = 0xFF + all_pass[i] = 0x11 + all_h[i] = 0x24 + all_vl[i] = 0x08 + } + cases := []Case{ + {"all V0", all_v0[:], -1}, + {"all pass", all_pass[:], -1}, + {"all horizontal", all_h[:], -1}, + {"all VL", all_vl[:], -1}, + {"all V0 as 1-D", all_v0[:], 0}, + {"all pass as mixed", all_pass[:], 4}, + } + for c in cases { + for columns in ([?]int{1, 8, 1728}) { + p := ccitt_default_params() + p.k = c.k + p.columns = columns + p.rows = 0 + got, err := ccitt_decode(c.data, p) + defer delete(got) + testing.expectf(t, err == .None || err == .Bad_Ccitt_Data, + "%s at %d columns: %v", c.name, columns, error_string(err)) + row_bytes := (columns + 7) / 8 + testing.expectf(t, len(got) <= len(c.data) * 8 * row_bytes, + "%s at %d columns: %d bytes from %d", c.name, columns, len(got), len(c.data)) + } + } +} + +@(test) +test_ccitt_extension_mode_stops :: proc(t: ^testing.T) { + // 0000001 is the 2-D extension escape (uncompressed mode). No PDF + // producer emits it, and guessing at what follows would desynchronise + // the whole image, so the row it appears in ends the image. + // Here: one V0 row, then an extension. + data := [?]byte{0b1000_0001, 0b0000_0000} + p := ccitt_default_params() + p.k = -1 + p.columns = 8 + got, err := ccitt_decode(data[:], p) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 1) + testing.expect_value(t, got[0], byte(0xFF)) +} + +@(test) +test_ccitt_allocator_is_honoured :: proc(t: ^testing.T) { + // The result must come from the allocator the caller named and not from + // context.allocator. filters.odin names a scratch allocator it then frees, + // so a result that quietly came from somewhere else would be a leak. + got, err := ccitt_decode(G4_FULL[:], {k = -1, columns = 8, rows = 12, end_of_block = true}, + context.allocator) + defer delete(got) + testing.expect_value(t, err, Error.None) + testing.expect(t, slice.equal(got, G4_FULL_OUT[:])) +} + +// /Rows is a claim, not a measurement: ccitt_decode reserves a buffer for the +// rows the dictionary promises and then decodes the rows the data actually +// holds. A file that promises twenty thousand and delivers twelve would leave +// the difference in the Document arena for good, since an arena cannot take a +// buffer back and its `delete` is a silent no-op. apply_filter is where that +// is prevented -- the decode runs against scratch and only the finished rows +// are copied across -- so the test sits on the boundary rather than on +// ccitt_decode, which is entitled to over-reserve as long as it does so with +// memory somebody can free. +@(test) +test_ccitt_row_buffer_stays_out_of_the_document_arena :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // The G4 image padded with trailing zeroes, so that the row cap the + // decoder derives from the data length does not clamp /Rows back down. + // The EOFB inside the image still ends it after twelve rows. + data := make([]byte, 4096) + defer delete(data) + copy(data, G4_FULL[:]) + + parms := new_dict(doc.allocator) + dict_set(parms, "K", i64(-1)) + dict_set(parms, "Columns", i64(8)) + dict_set(parms, "Rows", i64(20000)) + + before := document_memory_used(doc) + out, err := apply_filter(doc, "CCITTFaxDecode", data, parms, doc.allocator) + testing.expect_value(t, err, Error.None) + testing.expect(t, slice.equal(out, G4_FULL_OUT[:]), "the image did not decode") + growth := document_memory_used(doc) - before + testing.expectf(t, growth <= len(out) + FT_ARENA_SLACK, + "the arena grew by %d bytes for %d bytes of output", growth, len(out)) +} diff --git a/filter_dct.odin b/filter_dct.odin new file mode 100644 index 0000000..f217fc9 --- /dev/null +++ b/filter_dct.odin @@ -0,0 +1,1646 @@ +package pdf + +import "core:mem" + +// DCTDecode: a JPEG decoder (ISO/IEC 10918-1), sufficient for what PDF +// producers actually emit. +// +// ISO 32000-2, 7.4.8 defines DCTDecode by reference to the JPEG specification +// and then says almost nothing else, so the interesting question is which of +// JPEG's many modes turn up inside PDFs. In practice: baseline and extended +// sequential Huffman (SOF0/SOF1), progressive Huffman (SOF2 -- ordinary once a +// document is assembled out of web assets), one, three and four components, +// and the Adobe APP14 marker that tells CMYK from YCCK. Those are all +// implemented here. Arithmetic coding (SOF9-SOF11), the lossless mode (SOF3) +// and the hierarchical modes are not: no producer emits them into a PDF, and +// there is nothing sensible to substitute. +// +// The decoder is written to survive damage. A JPEG inside a PDF has usually +// been through at least one recompressor, and truncated scans, restart markers +// at the wrong interval and missing EOI are all routine. Every one of those +// yields as much of the image as could be decoded rather than an error. + +// --------------------------------------------------------------------------- +// Public surface +// --------------------------------------------------------------------------- + +// Jpeg_Image is one decoded JPEG. `data` belongs to the caller. +Jpeg_Image :: struct { + width, height: int, + // 1 (grey), 3 (RGB, after the YCbCr inverse transform) or 4 (CMYK, after + // the YCCK inverse transform when the Adobe marker asks for one). Two + // components is not a real JPEG colour model but does occur; it is passed + // through untransformed. + components: int, + // Bits per sample in `data`, always 8. A 12-bit JPEG -- rare, and rarer + // still in a PDF -- is decoded and scaled down so that `data` stays one + // byte per sample; `jpeg_info` reports the precision the file declares for + // a caller that wants to know. + bits: int, + // Samples, `components` per pixel, row-major, no padding between rows. + data: []byte, + // The Adobe APP14 colour transform: 0 none, 1 YCbCr, 2 YCCK; -1 when the + // marker is absent. + // + // This is reported rather than acted on beyond the inverse transform + // itself, because Adobe writes the CMYK JPEGs it embeds in PDFs with every + // sample inverted, and the inversion is expressed in the image + // dictionary's /Decode array, not in the JPEG. Inverting here as well + // would double-apply it. image.odin owns that decision. + adobe_transform: int, +} + +// An image larger than anything that can be rendered is not worth decoding, +// so this is the rasteriser's own limit from document.odin. +@(private) MAX_JPEG_PIXELS :: MAX_BITMAP_PIXELS +// Bytes one decode may allocate in total, across coefficient buffers, sample +// planes and the output. Every dimension in a JPEG header is attacker +// controlled and none of them can be checked against the compressed size -- +// a uniform image compresses without limit -- so a fixed cap is the only +// defence there is. +@(private) MAX_JPEG_ALLOC :: 1 << 30 +// Marker segments before the decoder gives up. A real file has a few dozen; +// this only bounds a stream of well-formed but empty ones. +@(private) MAX_JPEG_SEGMENTS :: 1 << 16 +// Scans decoded before the decoder stops and keeps what it has. This is a +// separate bound from MAX_JPEG_SEGMENTS because a scan is not cheap per byte: +// one EOB run skips up to 32767 blocks for 18 bits, so a few hundred bytes of +// entropy data drives the MCU loop across the whole component. Bounding only +// the segment count leaves 65536 sweeps of a frame that may hold 2^22 blocks +// -- about 2.7e11 block visits, which is minutes to an hour of CPU from a +// stream of a megabyte or two. libjpeg-turbo's D_MAX_SCANS is the same 100, +// and it costs nothing real: no producer emits more than a few dozen, and the +// worst file in this project's corpus has ten. +@(private) MAX_JPEG_SCANS :: 100 + +// jpeg_decode decodes a complete JPEG datastream. +// +// `img.data` is allocated with `allocator` and is the caller's to free; +// everything else the decoder needs is scratch taken from `context.allocator` +// and released before returning. +// +// A truncated or damaged scan is not an error: the image comes back with the +// blocks that were decoded and mid-grey everywhere else, which is what every +// other reader shows. An error means there was nothing to hand back at all -- +// no SOI, no frame header, impossible dimensions, or a coding mode this +// decoder does not implement. +jpeg_decode :: proc(data: []byte, allocator := context.allocator) -> (img: Jpeg_Image, err: Error) { + d, alloc_err := new(Jpeg_Decoder, context.allocator) + if alloc_err != nil { + return {}, .Out_Of_Memory + } + defer free(d, context.allocator) + + d.scratch = context.allocator + d.adobe_transform = -1 + defer jpeg_release(d) + + if perr := jpeg_run(d, data); perr != .None { + return {}, perr + } + + out := jpeg_emit(d, allocator) or_return + return Jpeg_Image{ + width = d.width, + height = d.height, + components = d.ncomp, + bits = 8, + data = out, + adobe_transform = d.saw_adobe ? d.adobe_transform : -1, + }, .None +} + +// jpeg_info reads the frame header only. It allocates nothing and does not +// touch the entropy-coded data, so it is cheap enough to call on every image +// in a document. +// +// `bits` is the precision the file declares (8, or 12 for the rare deep +// image), not the precision of what `jpeg_decode` hands back. +jpeg_info :: proc(data: []byte) -> (width, height, components, bits: int, err: Error) { + pos, found := jpeg_find_soi(data) + if !found { + return 0, 0, 0, 0, .Bad_Jpeg_Data + } + + for _ in 0 ..< MAX_JPEG_SEGMENTS { + marker, next, ok := jpeg_next_marker(data, pos) + if !ok { break } + pos = next + + switch marker { + case JPEG_SOI, JPEG_TEM, JPEG_RST0, JPEG_RST0 + 1, JPEG_RST0 + 2, JPEG_RST0 + 3, + JPEG_RST0 + 4, JPEG_RST0 + 5, JPEG_RST0 + 6, JPEG_RST0 + 7: + continue + case JPEG_EOI: + return 0, 0, 0, 0, .Bad_Jpeg_Data + } + + payload, after, pok := jpeg_segment(data, pos) + if !pok { break } + pos = after + + switch marker { + case JPEG_SOF0, JPEG_SOF1, JPEG_SOF2, JPEG_SOF3, JPEG_SOF5, JPEG_SOF6, JPEG_SOF7, + JPEG_SOF9, JPEG_SOF10, JPEG_SOF11, JPEG_SOF13, JPEG_SOF14, JPEG_SOF15: + if len(payload) < 6 { + return 0, 0, 0, 0, .Bad_Jpeg_Data + } + bits = int(payload[0]) + height = int(payload[1]) << 8 | int(payload[2]) + width = int(payload[3]) << 8 | int(payload[4]) + components = int(payload[5]) + if height == 0 { + // A frame header may leave the height at zero and supply it in + // a DNL marker after the first scan (ISO/IEC 10918-1, B.2.2). + if dnl, dok := jpeg_find_dnl(data, pos); dok { + height = dnl + } + } + if width <= 0 || height <= 0 || components <= 0 || components > 4 { + return width, height, components, bits, .Bad_Jpeg_Data + } + return width, height, components, bits, .None + case JPEG_SOS: + // Entropy data before a frame header: nothing to report. + return 0, 0, 0, 0, .Bad_Jpeg_Data + } + } + return 0, 0, 0, 0, .Bad_Jpeg_Data +} + +// --------------------------------------------------------------------------- +// Markers +// --------------------------------------------------------------------------- + +@(private) JPEG_SOF0 :: 0xC0 +@(private) JPEG_SOF1 :: 0xC1 +@(private) JPEG_SOF2 :: 0xC2 +@(private) JPEG_SOF3 :: 0xC3 +@(private) JPEG_DHT :: 0xC4 +@(private) JPEG_SOF5 :: 0xC5 +@(private) JPEG_SOF6 :: 0xC6 +@(private) JPEG_SOF7 :: 0xC7 +@(private) JPEG_SOF9 :: 0xC9 +@(private) JPEG_SOF10 :: 0xCA +@(private) JPEG_SOF11 :: 0xCB +@(private) JPEG_DAC :: 0xCC +@(private) JPEG_SOF13 :: 0xCD +@(private) JPEG_SOF14 :: 0xCE +@(private) JPEG_SOF15 :: 0xCF +@(private) JPEG_RST0 :: 0xD0 +@(private) JPEG_SOI :: 0xD8 +@(private) JPEG_EOI :: 0xD9 +@(private) JPEG_SOS :: 0xDA +@(private) JPEG_DQT :: 0xDB +@(private) JPEG_DNL :: 0xDC +@(private) JPEG_DRI :: 0xDD +@(private) JPEG_APP0 :: 0xE0 +@(private) JPEG_APP14 :: 0xEE +@(private) JPEG_COM :: 0xFE +@(private) JPEG_TEM :: 0x01 + +// jpeg_find_soi locates the start-of-image. Junk in front of it -- an HTTP +// response, an Exif wrapper that a rewriter half-removed, a stray newline -- +// is common enough in embedded streams that starting at byte 0 unconditionally +// would reject working images. +@(private) +jpeg_find_soi :: proc(data: []byte) -> (pos: int, ok: bool) { + for i := 0; i + 1 < len(data); i += 1 { + if data[i] == 0xFF && data[i + 1] == JPEG_SOI { + return i + 2, true + } + } + return 0, false +} + +// jpeg_next_marker scans forward to the next marker, skipping fill bytes, +// stuffed 0xFF 0x00 pairs and any non-marker bytes in between. Returning the +// marker code and the offset just past it. +@(private) +jpeg_next_marker :: proc(data: []byte, from: int) -> (marker: int, next: int, ok: bool) { + p := max(from, 0) + for p + 1 < len(data) { + if data[p] != 0xFF { + p += 1 + continue + } + q := p + 1 + for q < len(data) && data[q] == 0xFF { + q += 1 + } + if q >= len(data) { break } + if data[q] == 0x00 { + p = q + 1 + continue + } + return int(data[q]), q + 1, true + } + return 0, len(data), false +} + +// jpeg_segment reads a length-prefixed marker segment starting at `pos` (which +// points at the two length bytes) and returns its payload. +// +// A length that runs off the end of the data is clamped rather than rejected; +// a truncated final segment still tells us everything up to the truncation. +@(private) +jpeg_segment :: proc(data: []byte, pos: int) -> (payload: []byte, next: int, ok: bool) { + if pos + 2 > len(data) { + return nil, len(data), false + } + seglen := int(data[pos]) << 8 | int(data[pos + 1]) + if seglen < 2 { + // Nonsense length. Skip the two bytes we read so the caller still + // makes progress and cannot spin here. + return nil, pos + 2, true + } + end := pos + seglen + if end > len(data) { + end = len(data) + } + return data[pos + 2:end], end, true +} + +// jpeg_find_dnl looks ahead for a define-number-of-lines marker, used when the +// frame header left the height at zero. The scan is a plain forward search for +// a well-formed DNL segment; a false positive inside entropy data would need +// the four bytes FF DC 00 04, which the byte-stuffing rules make impossible +// for FF DC to be (0xDC is not a stuffed zero and not a restart marker), so +// this is safe in practice. +@(private) +jpeg_find_dnl :: proc(data: []byte, from: int) -> (lines: int, ok: bool) { + for i := max(from, 0); i + 5 < len(data); i += 1 { + if data[i] != 0xFF || data[i + 1] != JPEG_DNL { continue } + if data[i + 2] != 0x00 || data[i + 3] != 0x04 { continue } + n := int(data[i + 4]) << 8 | int(data[i + 5]) + if n > 0 { + return n, true + } + } + return 0, false +} + +// --------------------------------------------------------------------------- +// Decoder state +// --------------------------------------------------------------------------- + +@(private) +Jpeg_Transform :: enum u8 { + None, // components are already in their final colour space + Ycbcr, // three components, inverse YCbCr + Ycck, // four components, inverse YCbCr on the first three, then invert +} + +@(private) +Jpeg_Comp :: struct { + id: int, + h, v: int, // sampling factors, 1..4 + tq: int, // quantisation table selector + dc_tbl: int, + ac_tbl: int, + dw, dh: int, // component size in samples + bw, bh: int, // component size in blocks, covering dw x dh + bw_pad: int, // blocks per line once padded out to whole MCUs + bh_pad: int, + pix_w: int, // bw_pad * 8 + pix_h: int, // bh_pad * 8 + coeffs: []i16, + samples: []byte, + pred: int, // running DC predictor for the scan in progress +} + +@(private) +Jpeg_Scan :: struct { + comps: [4]int, + n: int, + ss, se: int, + ah, al: int, +} + +@(private) +Jpeg_Decoder :: struct { + data: []byte, + pos: int, + scratch: mem.Allocator, + allocated: int, + + width, height: int, + precision: int, + ncomp: int, + comps: [4]Jpeg_Comp, + hmax, vmax: int, + mcux, mcuy: int, + progressive: bool, + restart_interval: int, + + qt: [4][64]u16, + qt_defined: [4]bool, + dc: [4]Jpeg_Huff, + ac: [4]Jpeg_Huff, + + adobe_transform: int, + saw_adobe: bool, + saw_jfif: bool, + saw_sof: bool, + transform: Jpeg_Transform, + + eobrun: int, +} + +@(private) +jpeg_release :: proc(d: ^Jpeg_Decoder) { + for i in 0 ..< 4 { + if d.comps[i].coeffs != nil { + delete(d.comps[i].coeffs, d.scratch) + d.comps[i].coeffs = nil + } + if d.comps[i].samples != nil { + delete(d.comps[i].samples, d.scratch) + d.comps[i].samples = nil + } + } +} + +// jpeg_charge reserves `n` bytes against the decode-wide allocation budget. +@(private) +jpeg_charge :: proc(d: ^Jpeg_Decoder, n: int) -> bool { + if n < 0 || n > MAX_JPEG_ALLOC - d.allocated { + return false + } + d.allocated += n + return true +} + +// --------------------------------------------------------------------------- +// Header parsing and the top-level marker loop +// --------------------------------------------------------------------------- + +@(private) +jpeg_run :: proc(d: ^Jpeg_Decoder, data: []byte) -> Error { + d.data = data + pos, found := jpeg_find_soi(data) + if !found { + return .Bad_Jpeg_Data + } + d.pos = pos + + saw_scan := false + scans := 0 + + for _ in 0 ..< MAX_JPEG_SEGMENTS { + marker, next, ok := jpeg_next_marker(d.data, d.pos) + if !ok { break } + d.pos = next + + switch marker { + case JPEG_SOI, JPEG_TEM: + continue + case JPEG_RST0, JPEG_RST0 + 1, JPEG_RST0 + 2, JPEG_RST0 + 3, + JPEG_RST0 + 4, JPEG_RST0 + 5, JPEG_RST0 + 6, JPEG_RST0 + 7: + // A restart marker outside a scan is leftover damage; step over it. + continue + case JPEG_EOI: + return jpeg_finish(d, saw_scan) + } + + payload, after, pok := jpeg_segment(d.data, d.pos) + if !pok { break } + d.pos = after + + switch marker { + case JPEG_DQT: + jpeg_read_dqt(d, payload) + case JPEG_DHT: + jpeg_read_dht(d, payload) + case JPEG_DRI: + if len(payload) >= 2 { + d.restart_interval = int(payload[0]) << 8 | int(payload[1]) + } + case JPEG_APP0: + if len(payload) >= 5 && string(payload[:5]) == "JFIF\x00" { + d.saw_jfif = true + } + case JPEG_APP14: + // "Adobe", version, flags0, flags1, transform: 12 bytes. + if len(payload) >= 12 && string(payload[:5]) == "Adobe" { + d.saw_adobe = true + d.adobe_transform = int(payload[11]) + } + case JPEG_DNL: + if len(payload) >= 2 && d.height == 0 { + d.height = int(payload[0]) << 8 | int(payload[1]) + } + case JPEG_SOF0, JPEG_SOF1, JPEG_SOF2: + if d.saw_sof { + // A second frame header means a hierarchical or corrupt file. + // Keep the frame already decoded. + continue + } + d.progressive = marker == JPEG_SOF2 + if ferr := jpeg_read_sof(d, payload); ferr != .None { + return ferr + } + case JPEG_SOF3, JPEG_SOF5, JPEG_SOF6, JPEG_SOF7, + JPEG_SOF9, JPEG_SOF10, JPEG_SOF11, + JPEG_SOF13, JPEG_SOF14, JPEG_SOF15, JPEG_DAC: + // Lossless, differential and arithmetic-coded frames. There is + // nothing to fall back to, so say so rather than emitting noise -- + // unless a frame has already been decoded, in which case this is a + // corrupt byte that happened to look like a marker and the image + // in hand is worth more than the complaint. + if d.saw_sof { + continue + } + return .Unsupported_Image + case JPEG_SOS: + if !d.saw_sof { + return .Bad_Jpeg_Data + } + if scans >= MAX_JPEG_SCANS { + // Past this the file is spending the decoder's time rather + // than adding detail, so keep the image built so far and stop. + // Refinement scans only ever sharpen what is already there, so + // a legitimate file truncated here still looks right. + return jpeg_finish(d, saw_scan) + } + scan, serr := jpeg_read_sos(d, payload) + if serr != .None { + // An unusable scan header is not a reason to throw away the + // scans already decoded. + continue + } + saw_scan = true + scans += 1 + jpeg_decode_scan(d, scan) + case: + // COM, APPn, JPGn, anything unknown: already stepped over. + } + } + + return jpeg_finish(d, saw_scan) +} + +@(private) +jpeg_finish :: proc(d: ^Jpeg_Decoder, saw_scan: bool) -> Error { + if !d.saw_sof { + return .Bad_Jpeg_Data + } + // A frame with no scan at all decodes to a uniform mid-grey image, which + // is more useful to a page than a hole where the image should be, but a + // file that never reached its first scan is damaged past the point of + // guessing. + if !saw_scan { + return .Bad_Jpeg_Data + } + return .None +} + +@(private) +jpeg_read_sof :: proc(d: ^Jpeg_Decoder, payload: []byte) -> Error { + if len(payload) < 6 { + return .Bad_Jpeg_Data + } + d.precision = int(payload[0]) + d.height = int(payload[1]) << 8 | int(payload[2]) + d.width = int(payload[3]) << 8 | int(payload[4]) + d.ncomp = int(payload[5]) + + if d.height == 0 { + if lines, ok := jpeg_find_dnl(d.data, d.pos); ok { + d.height = lines + } + } + if d.width <= 0 || d.height <= 0 { + return .Bad_Jpeg_Data + } + if d.ncomp < 1 || d.ncomp > 4 { + return .Bad_Jpeg_Data + } + if d.width > MAX_JPEG_PIXELS / d.height { + return .Bitmap_Too_Large + } + // 8 and 12 are the only precisions JPEG defines; anything else is damage. + // Deeper samples are shifted down at dequantisation so the rest of the + // pipeline stays 8-bit. + if d.precision < 8 || d.precision > 16 { + return .Bad_Jpeg_Data + } + if len(payload) < 6 + 3 * d.ncomp { + return .Bad_Jpeg_Data + } + + d.hmax, d.vmax = 1, 1 + for i in 0 ..< d.ncomp { + c := &d.comps[i] + base := 6 + 3 * i + c.id = int(payload[base]) + c.h = int(payload[base + 1] >> 4) + c.v = int(payload[base + 1] & 15) + c.tq = int(payload[base + 2]) + // Sampling factors outside 1..4 are illegal; clamping keeps the block + // geometry consistent instead of producing a zero-size plane. + if c.h < 1 || c.h > 4 { c.h = 1 } + if c.v < 1 || c.v > 4 { c.v = 1 } + if c.tq < 0 || c.tq > 3 { c.tq = 0 } + d.hmax = max(d.hmax, c.h) + d.vmax = max(d.vmax, c.v) + } + + d.mcux = (d.width + 8 * d.hmax - 1) / (8 * d.hmax) + d.mcuy = (d.height + 8 * d.vmax - 1) / (8 * d.vmax) + + for i in 0 ..< d.ncomp { + c := &d.comps[i] + c.dw = (d.width * c.h + d.hmax - 1) / d.hmax + c.dh = (d.height * c.v + d.vmax - 1) / d.vmax + c.bw = (c.dw + 7) / 8 + c.bh = (c.dh + 7) / 8 + c.bw_pad = d.mcux * c.h + c.bh_pad = d.mcuy * c.v + c.pix_w = c.bw_pad * 8 + c.pix_h = c.bh_pad * 8 + + nblocks := c.bw_pad * c.bh_pad + if nblocks <= 0 || nblocks > MAX_JPEG_PIXELS / 64 { + return .Bitmap_Too_Large + } + if !jpeg_charge(d, nblocks * 64 * size_of(i16)) { return .Bitmap_Too_Large } + if !jpeg_charge(d, c.pix_w * c.pix_h) { return .Bitmap_Too_Large } + + coef_err, samp_err: mem.Allocator_Error + c.coeffs, coef_err = make([]i16, nblocks * 64, d.scratch) + if coef_err != nil { return .Out_Of_Memory } + c.samples, samp_err = make([]byte, c.pix_w * c.pix_h, d.scratch) + if samp_err != nil { return .Out_Of_Memory } + // Undecoded blocks read back as mid-grey rather than black, so a + // truncated scan degrades into a faded image instead of a dark band. + for j in 0 ..< len(c.samples) { + c.samples[j] = 128 + } + } + + d.transform = jpeg_pick_transform(d) + d.saw_sof = true + return .None +} + +// jpeg_pick_transform decides what colour transform to undo, following the +// same rules libjpeg does -- which is the de-facto standard, because every +// producer was tested against it. +@(private) +jpeg_pick_transform :: proc(d: ^Jpeg_Decoder) -> Jpeg_Transform { + switch d.ncomp { + case 3: + if d.saw_adobe { + // 0 means the components are already RGB. + return d.adobe_transform == 0 ? .None : .Ycbcr + } + if d.saw_jfif { + return .Ycbcr + } + // No marker to go on: component ids 'R','G','B' mean exactly that, + // and anything else (usually 1,2,3) is YCbCr. + if d.comps[0].id == 'R' && d.comps[1].id == 'G' && d.comps[2].id == 'B' { + return .None + } + return .Ycbcr + case 4: + if d.saw_adobe && d.adobe_transform == 2 { + return .Ycck + } + return .None + } + return .None +} + +@(private) +jpeg_read_dqt :: proc(d: ^Jpeg_Decoder, payload: []byte) { + p := 0 + for p < len(payload) { + pq := int(payload[p] >> 4) + tq := int(payload[p] & 15) + p += 1 + if tq > 3 { + // Unusable selector: the rest of the segment cannot be located + // either, since table sizes depend on it. + return + } + n := pq == 0 ? 64 : 128 + if p + n > len(payload) { + return + } + for k in 0 ..< 64 { + v: u16 + if pq == 0 { + v = u16(payload[p + k]) + } else { + v = u16(payload[p + 2 * k]) << 8 | u16(payload[p + 2 * k + 1]) + } + // A zero quantiser would erase the coefficient; treat it as 1, + // which is what a viewer that survives such a file must do. + if v == 0 { v = 1 } + d.qt[tq][JPEG_ZIGZAG[k]] = v + } + d.qt_defined[tq] = true + p += n + } +} + +@(private) +jpeg_read_dht :: proc(d: ^Jpeg_Decoder, payload: []byte) { + p := 0 + for p + 17 <= len(payload) { + tc := int(payload[p] >> 4) + th := int(payload[p] & 15) + p += 1 + counts := payload[p:p + 16] + p += 16 + total := 0 + for c in counts { + total += int(c) + } + if total > 256 || p + total > len(payload) { + return + } + vals := payload[p:p + total] + p += total + if th > 3 || tc > 1 { + continue + } + table := tc == 0 ? &d.dc[th] : &d.ac[th] + jpeg_huff_build(table, counts, vals) + } +} + +@(private) +jpeg_read_sos :: proc(d: ^Jpeg_Decoder, payload: []byte) -> (scan: Jpeg_Scan, err: Error) { + if len(payload) < 1 { + return {}, .Bad_Jpeg_Data + } + ns := int(payload[0]) + if ns < 1 || ns > 4 || len(payload) < 1 + 2 * ns + 3 { + return {}, .Bad_Jpeg_Data + } + + for i in 0 ..< ns { + cs := int(payload[1 + 2 * i]) + td := int(payload[2 + 2 * i] >> 4) + ta := int(payload[2 + 2 * i] & 15) + + idx := -1 + for j in 0 ..< d.ncomp { + if d.comps[j].id == cs { + idx = j + break + } + } + if idx < 0 { + // Component selectors that do not match the frame happen when a + // rewriter renumbers one header and not the other. Positional + // matching recovers those files. + if i < d.ncomp { + idx = i + } else { + return {}, .Bad_Jpeg_Data + } + } + scan.comps[i] = idx + d.comps[idx].dc_tbl = td > 3 ? 0 : td + d.comps[idx].ac_tbl = ta > 3 ? 0 : ta + } + scan.n = ns + + tail := 1 + 2 * ns + scan.ss = int(payload[tail]) + scan.se = int(payload[tail + 1]) + scan.ah = int(payload[tail + 2] >> 4) + scan.al = int(payload[tail + 2] & 15) + + if !d.progressive { + // A sequential scan covers the whole block whatever its header says. + scan.ss, scan.se, scan.ah, scan.al = 0, 63, 0, 0 + } else { + if scan.ss > 63 { scan.ss = 63 } + if scan.se > 63 { scan.se = 63 } + if scan.se < scan.ss { scan.se = scan.ss } + if scan.al > 13 { scan.al = 13 } + if scan.ah > 13 { scan.ah = 13 } + // A DC scan is the only kind that may be interleaved, and it always + // covers coefficient 0 alone. + if scan.ss == 0 { + scan.se = 0 + } else if ns != 1 { + // An interleaved AC scan is illegal; take its first component. + scan.n = 1 + } + } + return scan, .None +} + +// --------------------------------------------------------------------------- +// Bit reader +// +// Entropy-coded data is a bit stream with 0xFF escaped as 0xFF 0x00. Anything +// else after an 0xFF is a marker and ends the segment; from there on the +// reader hands out zero bits, which lets a truncated scan run to the end of +// its MCU loop and produce a partial image instead of failing. +// --------------------------------------------------------------------------- + +@(private) +Jpeg_Bits :: struct { + data: []byte, + pos: int, + buf: u32, + cnt: int, + // Set once the reader has stopped at a marker. This is not the same as + // running out of data: the buffer is topped up several bits ahead of what + // the block decoders have consumed, so a scan that ends exactly on its + // last MCU still trips this while decoding perfectly. + marker_hit: bool, + marker_pos: int, // offset of the 0xFF that begins the pending marker + // Set when a request could not be met from real bits and was padded with + // zeroes. *This* is the "the data ran out" signal, and everything after it + // in the scan is guesswork. + starved: bool, +} + +@(private) +jpeg_next_data_byte :: proc(b: ^Jpeg_Bits) -> (c: byte, ok: bool) { + if b.marker_hit { + return 0, false + } + if b.pos >= len(b.data) { + b.marker_hit = true + b.marker_pos = len(b.data) + return 0, false + } + v := b.data[b.pos] + if v != 0xFF { + b.pos += 1 + return v, true + } + j := b.pos + 1 + for j < len(b.data) && b.data[j] == 0xFF { + j += 1 + } + if j >= len(b.data) { + b.marker_hit = true + b.marker_pos = b.pos + return 0, false + } + if b.data[j] == 0x00 { + b.pos = j + 1 + return 0xFF, true + } + // Leave pos where it is: the scan loop needs to find this marker. + b.marker_hit = true + b.marker_pos = j - 1 + return 0, false +} + +// jpeg_fill_soft tops the bit buffer up to `n` bits if it can, and says how +// many it managed. It never reports starvation, because the Huffman decoder +// speculatively asks for a whole lookahead window and must not be told the +// data ran out merely because the last few codes of a scan are shorter than +// that window. +@(private) +jpeg_fill_soft :: proc(b: ^Jpeg_Bits, n: int) -> int { + for b.cnt < n { + c, ok := jpeg_next_data_byte(b) + if !ok { break } + b.buf = b.buf << 8 | u32(c) + b.cnt += 8 + } + return b.cnt +} + +// jpeg_get_bits consumes n bits, padding with zeroes and raising `starved` +// when the entropy data really has run out. +@(private) +jpeg_get_bits :: proc(b: ^Jpeg_Bits, n: int) -> i32 { + if n <= 0 { return 0 } + nn := min(n, 16) + if jpeg_fill_soft(b, nn) < nn { + b.buf <<= uint(nn - b.cnt) + b.cnt = nn + b.starved = true + } + b.cnt -= nn + return i32((b.buf >> uint(b.cnt)) & (1 << uint(nn) - 1)) +} + +// jpeg_take_restart consumes the RSTn marker the stream is sitting on. +// +// Only the caller knows when a marker is due, and it must only call this +// there. "The reader is parked on an FF Dn" is not by itself evidence of a +// segment boundary: the reader buffers whole bytes ahead of the bits that have +// been consumed and stops on the marker, so the last few blocks of an interval +// are routinely decoded while `pos` already points at the marker that follows +// them. Only the declared restart interval says which of those positions is +// the real boundary, which is why the DRI is what gates the call. +// +// The `cnt >= 8` test is a consistency check, not that evidence: with intact +// data every whole byte of the segment has been consumed by the time the +// interval is up, so all that can be left in the buffer is the encoder's +// byte-alignment padding. More than that means the block decoders and the +// encoder disagree about this segment, and the caller's forward search for the +// next marker is the better recovery. +@(private) +jpeg_take_restart :: proc(b: ^Jpeg_Bits) -> bool { + if b.cnt >= 8 { + return false + } + p := b.marker_hit ? b.marker_pos : b.pos + if p < 0 || p + 1 >= len(b.data) || b.data[p] != 0xFF { + return false + } + q := p + 1 + for q < len(b.data) && b.data[q] == 0xFF { + q += 1 + } + if q >= len(b.data) { + return false + } + m := b.data[q] + if m < JPEG_RST0 || m > JPEG_RST0 + 7 { + return false + } + b.pos = q + 1 + b.marker_hit = false + b.marker_pos = 0 + b.starved = false + b.buf = 0 + b.cnt = 0 + return true +} + +// jpeg_bits_restart resynchronises at the next RSTn marker. +// +// It searches forward rather than insisting the marker is at the current +// position, because a restart interval that disagrees with the markers +// actually present is a common form of damage and the whole point of restart +// markers is that a decoder can find its place again. The search only ever +// moves forward, so the total cost across a scan stays linear. +@(private) +jpeg_bits_restart :: proc(b: ^Jpeg_Bits) -> bool { + b.buf = 0 + b.cnt = 0 + p := b.pos + for p + 1 < len(b.data) { + if b.data[p] != 0xFF { + p += 1 + continue + } + m := b.data[p + 1] + if m == 0x00 || m == 0xFF { + p += 1 + continue + } + if m >= JPEG_RST0 && m <= JPEG_RST0 + 7 { + b.pos = p + 2 + b.marker_hit = false + b.marker_pos = 0 + b.starved = false + return true + } + b.marker_hit = true + b.marker_pos = p + return false + } + b.pos = len(b.data) + b.marker_hit = true + b.marker_pos = len(b.data) + return false +} + +// --------------------------------------------------------------------------- +// Huffman tables +// --------------------------------------------------------------------------- + +@(private) JPEG_LOOKAHEAD :: 8 + +@(private) +Jpeg_Huff :: struct { + // Canonical code bounds per code length, as in ISO/IEC 10918-1 F.2.2.3. + mincode: [17]i32, + maxcode: [17]i32, // -1 where no code of that length exists + valptr: [17]i32, + vals: [256]byte, + nvals: int, + // Codes of JPEG_LOOKAHEAD bits or fewer resolve in one table read; longer + // ones fall back to the bit-at-a-time loop. Short codes dominate, so this + // is where nearly all the decoding time goes. + look_nbits: [1 << JPEG_LOOKAHEAD]u8, + look_sym: [1 << JPEG_LOOKAHEAD]byte, + defined: bool, +} + +@(private) +jpeg_huff_build :: proc(h: ^Jpeg_Huff, counts: []byte, vals: []byte) { + h^ = {} + if len(counts) < 16 { return } + + total := 0 + for i in 0 ..< 16 { + total += int(counts[i]) + } + if total == 0 || total > 256 || total > len(vals) { + return + } + copy(h.vals[:], vals[:total]) + h.nvals = total + + code := i32(0) + p := i32(0) + for l in 1 ..= 16 { + n := i32(counts[l - 1]) + h.valptr[l] = p + h.mincode[l] = code + code += n + p += n + h.maxcode[l] = n == 0 ? -1 : code - 1 + code <<= 1 + } + + idx := 0 + for l in 1 ..= JPEG_LOOKAHEAD { + n := int(counts[l - 1]) + for i in 0 ..< n { + if idx >= total { break } + base := int(h.mincode[l]) + i + shift := uint(JPEG_LOOKAHEAD - l) + lo := base << shift + hi := lo + (1 << shift) + // An over-subscribed table (more codes than the length allows) + // pushes entries past the end of the lookup window; drop those + // rather than refusing the whole table. + if lo >= 0 && hi <= 1 << JPEG_LOOKAHEAD { + for k in lo ..< hi { + h.look_nbits[k] = u8(l) + h.look_sym[k] = h.vals[idx] + } + } + idx += 1 + } + } + h.defined = true +} + +// jpeg_huff_decode reads one Huffman-coded symbol. +// +// An undefined table, or a code that no length matches, yields 0. For an AC +// table 0 is end-of-block and for a DC table it is a zero difference, so +// corrupt data ends the block quietly instead of running away. +@(private) +jpeg_huff_decode :: proc(b: ^Jpeg_Bits, h: ^Jpeg_Huff) -> int { + if !h.defined { + return 0 + } + if jpeg_fill_soft(b, JPEG_LOOKAHEAD) >= JPEG_LOOKAHEAD { + look := int((b.buf >> uint(b.cnt - JPEG_LOOKAHEAD)) & (1 << JPEG_LOOKAHEAD - 1)) + if nb := int(h.look_nbits[look]); nb != 0 { + b.cnt -= nb + return int(h.look_sym[look]) + } + } + // Either a code longer than the lookahead window, or the last few bits of + // a scan. Both fall back to reading a bit at a time. + code := i32(0) + for l in 1 ..= 16 { + code = code << 1 | jpeg_get_bits(b, 1) + if code <= h.maxcode[l] { + idx := int(h.valptr[l] + code - h.mincode[l]) + if idx < 0 || idx >= h.nvals { + return 0 + } + return int(h.vals[idx]) + } + } + return 0 +} + +// jpeg_extend turns an s-bit magnitude into the signed difference it stands +// for (ISO/IEC 10918-1 F.2.2.1). +@(private) +jpeg_extend :: #force_inline proc "contextless" (v: i32, s: int) -> i32 { + if s <= 0 { return 0 } + return v < 1 << uint(s - 1) ? v - (1 << uint(s)) + 1 : v +} + +@(private) +jpeg_receive_extend :: #force_inline proc (b: ^Jpeg_Bits, s: int) -> i32 { + if s <= 0 || s > 16 { return 0 } + return jpeg_extend(jpeg_get_bits(b, s), s) +} + +// --------------------------------------------------------------------------- +// Scan decoding +// --------------------------------------------------------------------------- + +@(private) +jpeg_reset_scan_state :: proc(d: ^Jpeg_Decoder, scan: Jpeg_Scan) { + for i in 0 ..< scan.n { + d.comps[scan.comps[i]].pred = 0 + } + d.eobrun = 0 +} + +@(private) +jpeg_decode_scan :: proc(d: ^Jpeg_Decoder, scan: Jpeg_Scan) { + if d.pos >= len(d.data) { return } + + b := Jpeg_Bits{data = d.data, pos = d.pos} + jpeg_reset_scan_state(d, scan) + + mcus_x, mcus_y: int + if scan.n == 1 { + c := &d.comps[scan.comps[0]] + mcus_x, mcus_y = c.bw, c.bh + } else { + mcus_x, mcus_y = d.mcux, d.mcuy + } + total := mcus_x * mcus_y + if total <= 0 { + d.pos = b.pos + return + } + + restart_left := d.restart_interval + + // `mcu` is stepped by hand rather than with a range so the restart paths + // can be taken before the block decoders run. It only ever increases, so + // the loop is bounded by the MCU count whatever the data does. + for mcu := 0; mcu < total; mcu += 1 { + // The declared interval, not the bit reader's position, is what says a + // marker is due. The reader buffers whole bytes ahead of the bits the + // block decoders have consumed and stops dead on a marker, so from the + // moment the last byte of a segment is in the buffer it is already + // "sitting on" the FF Dn while that segment's final blocks are still + // undecoded -- which for anything low-entropy (a scanned page, or the + // one-bit-per-block DC refinement scan every progressive file has) is + // most of the segment. Taking the marker on that evidence resets the + // DC predictors and the EOB run a block or more early and decodes the + // rest of the interval out of the next segment's bits. + due := d.restart_interval > 0 && restart_left <= 0 + if due || b.starved { + // A marker is due, or the bits ran out mid-segment because the + // entropy data is corrupt or the DRI disagrees with the markers + // the file actually carries. Consume the marker where it is due; + // otherwise search forward for the next one, which is recoverable + // if any remains. If none does, everything from here stays at + // zero, which reconstructs as flat mid-grey. + if !(due && jpeg_take_restart(&b)) { + if !jpeg_bits_restart(&b) { break } + } + jpeg_reset_scan_state(d, scan) + restart_left = d.restart_interval + } + restart_left -= 1 + + my := mcu / mcus_x + mx := mcu % mcus_x + + if scan.n == 1 { + ci := scan.comps[0] + jpeg_decode_block(d, &b, scan, ci, my, mx) + } else { + for i in 0 ..< scan.n { + ci := scan.comps[i] + comp := &d.comps[ci] + for by in 0 ..< comp.v { + for bx in 0 ..< comp.h { + jpeg_decode_block(d, &b, scan, ci, my * comp.v + by, mx * comp.h + bx) + } + } + } + } + } + + d.pos = b.marker_hit ? b.marker_pos : b.pos + if d.pos < 0 || d.pos > len(d.data) { + d.pos = len(d.data) + } +} + +@(private) +jpeg_decode_block :: proc(d: ^Jpeg_Decoder, b: ^Jpeg_Bits, scan: Jpeg_Scan, ci, brow, bcol: int) { + comp := &d.comps[ci] + if brow < 0 || bcol < 0 || brow >= comp.bh_pad || bcol >= comp.bw_pad { + return + } + base := (brow * comp.bw_pad + bcol) * 64 + if base < 0 || base + 64 > len(comp.coeffs) { + return + } + coef := comp.coeffs[base:base + 64] + + if !d.progressive { + jpeg_decode_baseline_block(d, b, comp, coef) + return + } + if scan.ss == 0 { + if scan.ah == 0 { + jpeg_decode_dc_first(d, b, comp, coef, scan.al) + } else { + jpeg_decode_dc_refine(b, coef, scan.al) + } + } else { + if scan.ah == 0 { + jpeg_decode_ac_first(d, b, comp, coef, scan) + } else { + jpeg_decode_ac_refine(d, b, comp, coef, scan) + } + } +} + +@(private) +jpeg_store_dc :: #force_inline proc "contextless" (coef: []i16, v: i32) { + coef[0] = i16(clamp(v, i32(min(i16)), i32(max(i16)))) +} + +@(private) +jpeg_decode_baseline_block :: proc(d: ^Jpeg_Decoder, b: ^Jpeg_Bits, comp: ^Jpeg_Comp, coef: []i16) { + s := jpeg_huff_decode(b, &d.dc[comp.dc_tbl]) + diff := jpeg_receive_extend(b, s) + comp.pred = int(clamp(i32(comp.pred) + diff, -1 << 20, 1 << 20)) + jpeg_store_dc(coef, i32(comp.pred)) + + ac := &d.ac[comp.ac_tbl] + k := 1 + for k < 64 { + rs := jpeg_huff_decode(b, ac) + r := rs >> 4 + s2 := rs & 15 + if s2 == 0 { + if r != 15 { break } + k += 16 + continue + } + k += r + if k > 63 { break } + coef[JPEG_ZIGZAG[k]] = i16(clamp(jpeg_receive_extend(b, s2), i32(min(i16)), i32(max(i16)))) + k += 1 + } +} + +@(private) +jpeg_decode_dc_first :: proc( + d: ^Jpeg_Decoder, b: ^Jpeg_Bits, comp: ^Jpeg_Comp, coef: []i16, al: int, +) { + s := jpeg_huff_decode(b, &d.dc[comp.dc_tbl]) + diff := jpeg_receive_extend(b, s) + comp.pred = int(clamp(i32(comp.pred) + diff, -1 << 20, 1 << 20)) + jpeg_store_dc(coef, i32(comp.pred) << uint(al)) +} + +@(private) +jpeg_decode_dc_refine :: proc(b: ^Jpeg_Bits, coef: []i16, al: int) { + if jpeg_get_bits(b, 1) != 0 { + coef[0] |= i16(1) << uint(al) + } +} + +@(private) +jpeg_decode_ac_first :: proc( + d: ^Jpeg_Decoder, b: ^Jpeg_Bits, comp: ^Jpeg_Comp, coef: []i16, scan: Jpeg_Scan, +) { + if d.eobrun > 0 { + d.eobrun -= 1 + return + } + ac := &d.ac[comp.ac_tbl] + k := scan.ss + for k <= scan.se { + rs := jpeg_huff_decode(b, ac) + r := rs >> 4 + s := rs & 15 + if s == 0 { + if r != 15 { + d.eobrun = 1 << uint(r) + if r > 0 { + d.eobrun += int(jpeg_get_bits(b, r)) + } + d.eobrun -= 1 + break + } + k += 16 + continue + } + k += r + if k > scan.se || k > 63 { break } + v := jpeg_receive_extend(b, s) << uint(scan.al) + coef[JPEG_ZIGZAG[k]] = i16(clamp(v, i32(min(i16)), i32(max(i16)))) + k += 1 + } +} + +// jpeg_decode_ac_refine appends correction bits to the AC coefficients of one +// block (ISO/IEC 10918-1 G.1.2.3). This is the fiddliest part of progressive +// JPEG: the bit stream interleaves newly-nonzero coefficients with correction +// bits for coefficients that earlier scans already made nonzero, and the run +// length counts only the ones that are still zero. +@(private) +jpeg_decode_ac_refine :: proc( + d: ^Jpeg_Decoder, b: ^Jpeg_Bits, comp: ^Jpeg_Comp, coef: []i16, scan: Jpeg_Scan, +) { + p1 := i16(1) << uint(scan.al) + m1 := -(i16(1) << uint(scan.al)) + ac := &d.ac[comp.ac_tbl] + k := scan.ss + + if d.eobrun == 0 { + for k <= scan.se { + rs := jpeg_huff_decode(b, ac) + r := rs >> 4 + s := rs & 15 + newval := i16(0) + if s != 0 { + // The magnitude of a newly nonzero coefficient is always 1 in + // a refinement scan; a larger one is corrupt, and taking the + // sign bit anyway keeps the bit stream aligned. + newval = jpeg_get_bits(b, 1) != 0 ? p1 : m1 + } else if r != 15 { + d.eobrun = 1 << uint(r) + if r > 0 { + d.eobrun += int(jpeg_get_bits(b, r)) + } + break + } + + for { + pos := int(JPEG_ZIGZAG[k]) + if coef[pos] != 0 { + if jpeg_get_bits(b, 1) != 0 && coef[pos] & p1 == 0 { + coef[pos] += coef[pos] >= 0 ? p1 : m1 + } + } else { + r -= 1 + if r < 0 { break } + } + k += 1 + if k > scan.se { break } + } + + if newval != 0 && k <= scan.se { + coef[JPEG_ZIGZAG[k]] = newval + } + k += 1 + } + } + + if d.eobrun > 0 { + for ; k <= scan.se; k += 1 { + pos := int(JPEG_ZIGZAG[k]) + if coef[pos] != 0 { + if jpeg_get_bits(b, 1) != 0 && coef[pos] & p1 == 0 { + coef[pos] += coef[pos] >= 0 ? p1 : m1 + } + } + } + d.eobrun -= 1 + } +} + +// --------------------------------------------------------------------------- +// Inverse DCT +// +// The accurate integer IDCT from ISO/IEC 10918-1's own reference software, +// after Loeffler, Ligtenberg and Moschytz: 11 multiplies and 29 adds per +// one-dimensional pass, in 13-bit fixed point. Two passes, columns then rows. +// A separable pair of one-dimensional transforms is O(n^3) for the block +// against the O(n^4) of the definition, and this one is a further constant +// factor faster again. +// --------------------------------------------------------------------------- + +@(private) JPEG_CONST_BITS :: 13 +@(private) JPEG_PASS1_BITS :: 2 + +@(private) JPEG_FIX_0_298631336 :: 2446 +@(private) JPEG_FIX_0_390180644 :: 3196 +@(private) JPEG_FIX_0_541196100 :: 4433 +@(private) JPEG_FIX_0_765366865 :: 6270 +@(private) JPEG_FIX_0_899976223 :: 7373 +@(private) JPEG_FIX_1_175875602 :: 9633 +@(private) JPEG_FIX_1_501321110 :: 12299 +@(private) JPEG_FIX_1_847759065 :: 15137 +@(private) JPEG_FIX_1_961570560 :: 16069 +@(private) JPEG_FIX_2_053119869 :: 16819 +@(private) JPEG_FIX_2_562915447 :: 20995 +@(private) JPEG_FIX_3_072711026 :: 25172 + +// Dequantised coefficients are clamped to this. Valid 8-bit data never gets +// near it (the DCT of an 8-bit block bounds every coefficient by 1024), and +// the clamp is what keeps a corrupt coefficient from overflowing the 32-bit +// fixed-point arithmetic below into visible noise. +@(private) JPEG_COEF_LIMIT :: 1 << 14 + +@(private) +jpeg_descale :: #force_inline proc "contextless" (x: i32, n: uint) -> i32 { + return (x + (1 << (n - 1))) >> n +} + +@(private) +jpeg_clamp_sample :: #force_inline proc "contextless" (x: i32) -> byte { + if x < 0 { return 0 } + if x > 255 { return 255 } + return byte(x) +} + +@(private) +jpeg_idct_block :: proc(coef: []i16, qt: ^[64]u16, dst: []byte, off, stride: int, shift: uint) { + dq :: #force_inline proc "contextless" (c: i16, q: u16, sh: uint) -> i32 { + return clamp((i32(c) * i32(q)) >> sh, -JPEG_COEF_LIMIT, JPEG_COEF_LIMIT) + } + + ws: [64]i32 + + for col in 0 ..< 8 { + if coef[col + 8] == 0 && coef[col + 16] == 0 && coef[col + 24] == 0 && + coef[col + 32] == 0 && coef[col + 40] == 0 && coef[col + 48] == 0 && + coef[col + 56] == 0 { + // An all-zero AC column is the common case in flat areas. + dcval := dq(coef[col], qt[col], shift) << JPEG_PASS1_BITS + for row in 0 ..< 8 { + ws[row * 8 + col] = dcval + } + continue + } + + z2 := dq(coef[col + 16], qt[col + 16], shift) + z3 := dq(coef[col + 48], qt[col + 48], shift) + z1 := (z2 + z3) * JPEG_FIX_0_541196100 + tmp2 := z1 + z3 * -JPEG_FIX_1_847759065 + tmp3 := z1 + z2 * JPEG_FIX_0_765366865 + + z2 = dq(coef[col], qt[col], shift) + z3 = dq(coef[col + 32], qt[col + 32], shift) + tmp0 := (z2 + z3) << JPEG_CONST_BITS + tmp1 := (z2 - z3) << JPEG_CONST_BITS + + tmp10 := tmp0 + tmp3 + tmp13 := tmp0 - tmp3 + tmp11 := tmp1 + tmp2 + tmp12 := tmp1 - tmp2 + + tmp0 = dq(coef[col + 56], qt[col + 56], shift) + tmp1 = dq(coef[col + 40], qt[col + 40], shift) + tmp2 = dq(coef[col + 24], qt[col + 24], shift) + tmp3 = dq(coef[col + 8], qt[col + 8], shift) + + z1 = tmp0 + tmp3 + z2 = tmp1 + tmp2 + z3 = tmp0 + tmp2 + z4 := tmp1 + tmp3 + z5 := (z3 + z4) * JPEG_FIX_1_175875602 + + tmp0 *= JPEG_FIX_0_298631336 + tmp1 *= JPEG_FIX_2_053119869 + tmp2 *= JPEG_FIX_3_072711026 + tmp3 *= JPEG_FIX_1_501321110 + z1 *= -JPEG_FIX_0_899976223 + z2 *= -JPEG_FIX_2_562915447 + z3 *= -JPEG_FIX_1_961570560 + z4 *= -JPEG_FIX_0_390180644 + z3 += z5 + z4 += z5 + + tmp0 += z1 + z3 + tmp1 += z2 + z4 + tmp2 += z2 + z3 + tmp3 += z1 + z4 + + ws[col] = jpeg_descale(tmp10 + tmp3, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[56 + col] = jpeg_descale(tmp10 - tmp3, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[8 + col] = jpeg_descale(tmp11 + tmp2, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[48 + col] = jpeg_descale(tmp11 - tmp2, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[16 + col] = jpeg_descale(tmp12 + tmp1, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[40 + col] = jpeg_descale(tmp12 - tmp1, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[24 + col] = jpeg_descale(tmp13 + tmp0, JPEG_CONST_BITS - JPEG_PASS1_BITS) + ws[32 + col] = jpeg_descale(tmp13 - tmp0, JPEG_CONST_BITS - JPEG_PASS1_BITS) + } + + for row in 0 ..< 8 { + w := ws[row * 8:row * 8 + 8] + out := off + row * stride + if out < 0 || out + 8 > len(dst) { + continue + } + + z2 := w[2] + z3 := w[6] + z1 := (z2 + z3) * JPEG_FIX_0_541196100 + tmp2 := z1 + z3 * -JPEG_FIX_1_847759065 + tmp3 := z1 + z2 * JPEG_FIX_0_765366865 + + tmp0 := (w[0] + w[4]) << JPEG_CONST_BITS + tmp1 := (w[0] - w[4]) << JPEG_CONST_BITS + + tmp10 := tmp0 + tmp3 + tmp13 := tmp0 - tmp3 + tmp11 := tmp1 + tmp2 + tmp12 := tmp1 - tmp2 + + tmp0 = w[7] + tmp1 = w[5] + tmp2 = w[3] + tmp3 = w[1] + + z1 = tmp0 + tmp3 + z2 = tmp1 + tmp2 + z3 = tmp0 + tmp2 + z4 := tmp1 + tmp3 + z5 := (z3 + z4) * JPEG_FIX_1_175875602 + + tmp0 *= JPEG_FIX_0_298631336 + tmp1 *= JPEG_FIX_2_053119869 + tmp2 *= JPEG_FIX_3_072711026 + tmp3 *= JPEG_FIX_1_501321110 + z1 *= -JPEG_FIX_0_899976223 + z2 *= -JPEG_FIX_2_562915447 + z3 *= -JPEG_FIX_1_961570560 + z4 *= -JPEG_FIX_0_390180644 + z3 += z5 + z4 += z5 + + tmp0 += z1 + z3 + tmp1 += z2 + z4 + tmp2 += z2 + z3 + tmp3 += z1 + z4 + + // The level shift of ISO/IEC 10918-1 A.3.1: samples are coded centred + // on zero and stored centred on 128. + final :: JPEG_CONST_BITS + JPEG_PASS1_BITS + 3 + dst[out] = jpeg_clamp_sample(jpeg_descale(tmp10 + tmp3, final) + 128) + dst[out + 7] = jpeg_clamp_sample(jpeg_descale(tmp10 - tmp3, final) + 128) + dst[out + 1] = jpeg_clamp_sample(jpeg_descale(tmp11 + tmp2, final) + 128) + dst[out + 6] = jpeg_clamp_sample(jpeg_descale(tmp11 - tmp2, final) + 128) + dst[out + 2] = jpeg_clamp_sample(jpeg_descale(tmp12 + tmp1, final) + 128) + dst[out + 5] = jpeg_clamp_sample(jpeg_descale(tmp12 - tmp1, final) + 128) + dst[out + 3] = jpeg_clamp_sample(jpeg_descale(tmp13 + tmp0, final) + 128) + dst[out + 4] = jpeg_clamp_sample(jpeg_descale(tmp13 - tmp0, final) + 128) + } +} + +// --------------------------------------------------------------------------- +// Output: inverse DCT, upsampling, colour transform +// --------------------------------------------------------------------------- + +@(private) +jpeg_idct_planes :: proc(d: ^Jpeg_Decoder) { + // Precisions above 8 bits are shifted down here rather than carried + // through the whole pipeline: the output is bytes either way, and doing it + // at dequantisation keeps the fixed-point IDCT inside its designed range. + shift := uint(max(d.precision - 8, 0)) + + for i in 0 ..< d.ncomp { + c := &d.comps[i] + qt := &d.qt[c.tq] + for brow in 0 ..< c.bh_pad { + for bcol in 0 ..< c.bw_pad { + base := (brow * c.bw_pad + bcol) * 64 + if base + 64 > len(c.coeffs) { continue } + jpeg_idct_block( + c.coeffs[base:base + 64], qt, c.samples, + brow * 8 * c.pix_w + bcol * 8, c.pix_w, shift, + ) + } + } + } +} + +@(private) +jpeg_ycc_to_rgb :: #force_inline proc "contextless" (y, cb, cr: i32) -> (r, g, b: byte) { + // Fixed point at 16 bits, matching the coefficients in ITU-T T.871 (JFIF) + // 7: R = Y + 1.402 Cr, G = Y - 0.344136 Cb - 0.714136 Cr, B = Y + 1.772 Cb. + cbv := cb - 128 + crv := cr - 128 + r = jpeg_clamp_sample(y + ((91881 * crv + 32768) >> 16)) + g = jpeg_clamp_sample(y + ((-22554 * cbv - 46802 * crv + 32768) >> 16)) + b = jpeg_clamp_sample(y + ((116130 * cbv + 32768) >> 16)) + return +} + +@(private) +jpeg_emit :: proc(d: ^Jpeg_Decoder, allocator: mem.Allocator) -> (out: []byte, err: Error) { + w, h, n := d.width, d.height, d.ncomp + if w <= 0 || h <= 0 || n <= 0 || n > 4 { + return nil, .Bad_Jpeg_Data + } + if w > MAX_JPEG_PIXELS / h { + return nil, .Bitmap_Too_Large + } + total := w * h * n + if !jpeg_charge(d, total) { + return nil, .Bitmap_Too_Large + } + + jpeg_idct_planes(d) + + out_err: mem.Allocator_Error + out, out_err = make([]byte, total, allocator) + if out_err != nil { + return nil, .Out_Of_Memory + } + + // Component samples are replicated to full resolution rather than + // interpolated. Box upsampling is what ISO/IEC 10918-1's own informative + // decoder does; the smoother triangular filter libjpeg defaults to would + // change every subsampled pixel by a couple of levels and buys nothing at + // the scales a page is rasterised at. + if !jpeg_charge(d, w * n * size_of(i32)) { + delete(out, allocator) + return nil, .Bitmap_Too_Large + } + xmap, xerr := make([]i32, w * n, d.scratch) + if xerr != nil { + delete(out, allocator) + return nil, .Out_Of_Memory + } + defer delete(xmap, d.scratch) + + for ci in 0 ..< n { + c := &d.comps[ci] + last := max(c.dw - 1, 0) + for x in 0 ..< w { + sx := x * c.h / d.hmax + xmap[ci * w + x] = i32(min(sx, last)) + } + } + + rows: [4][]byte + o := 0 + for y in 0 ..< h { + for ci in 0 ..< n { + c := &d.comps[ci] + sy := min(y * c.v / d.vmax, max(c.dh - 1, 0)) + start := sy * c.pix_w + if start < 0 || start + c.pix_w > len(c.samples) { + start = 0 + } + rows[ci] = c.samples[start:start + c.pix_w] + } + + switch { + case n == 1: + r0 := rows[0] + m0 := xmap[:w] + for x in 0 ..< w { + out[o] = r0[m0[x]] + o += 1 + } + case n == 3 && d.transform == .Ycbcr: + r0, r1, r2 := rows[0], rows[1], rows[2] + m0 := xmap[:w] + m1 := xmap[w:2 * w] + m2 := xmap[2 * w:3 * w] + for x in 0 ..< w { + cr, cg, cb := jpeg_ycc_to_rgb(i32(r0[m0[x]]), i32(r1[m1[x]]), i32(r2[m2[x]])) + out[o], out[o + 1], out[o + 2] = cr, cg, cb + o += 3 + } + case n == 4 && d.transform == .Ycck: + r0, r1, r2, r3 := rows[0], rows[1], rows[2], rows[3] + m0 := xmap[:w] + m1 := xmap[w:2 * w] + m2 := xmap[2 * w:3 * w] + m3 := xmap[3 * w:4 * w] + for x in 0 ..< w { + cr, cg, cb := jpeg_ycc_to_rgb(i32(r0[m0[x]]), i32(r1[m1[x]]), i32(r2[m2[x]])) + // YCCK stores the complement of CMY as if it were RGB, so the + // inverse transform ends by complementing back. This is not + // the separate whole-image inversion Adobe applies to the CMYK + // it puts in PDFs -- that one belongs to /Decode. + out[o] = 255 - cr + out[o + 1] = 255 - cg + out[o + 2] = 255 - cb + out[o + 3] = r3[m3[x]] + o += 4 + } + case: + for x in 0 ..< w { + for ci in 0 ..< n { + out[o] = rows[ci][xmap[ci * w + x]] + o += 1 + } + } + } + } + return out, .None +} + +// --------------------------------------------------------------------------- +// Tables +// --------------------------------------------------------------------------- + +// Zig-zag order to natural (row-major) order, ISO/IEC 10918-1 Figure A.6. +@(private, rodata) +JPEG_ZIGZAG := [64]u8{ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, +} diff --git a/filter_dct_test.odin b/filter_dct_test.odin new file mode 100644 index 0000000..a02695f --- /dev/null +++ b/filter_dct_test.odin @@ -0,0 +1,1534 @@ +package pdf + +import "core:slice" +import "core:testing" +import "core:time" + +// Test vectors for the DCTDecode filter. +// +// The embedded files were produced with a throwaway Python script during +// development. `grey8`, `grey16`, `greyprog`, `rgb444`, `rgb420`, `rgbprog`, +// `cmyk`, `rst` and `norst` are Pillow (that is, libjpeg-turbo) encodings of +// the synthetic images described beside each one; `dcflat` and `oddsamp` were +// assembled byte by byte by a small encoder in the same script, because no +// ordinary encoder will emit the sampling factors and single-coefficient +// blocks they need. Everything else -- the YCCK file, the DNL file, the wrong +// restart interval, the truncations -- is patched out of those at run time by +// the helpers below, so that what makes each variant malformed is visible in +// the test rather than buried in an opaque blob. +// +// Where a value is asserted exactly it was cross-checked against libjpeg: the +// grey, 4:4:4 and CMYK files decode byte-for-byte identically to Pillow. The +// chroma-subsampled files deliberately do not, because this decoder replicates +// chroma samples where libjpeg interpolates them, so those are checked against +// the source image with a tolerance instead. + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +@(private) +jpeg_t_copy :: proc(src: []byte, extra := 0) -> []byte { + out := make([]byte, len(src) + extra, context.allocator) + copy(out, src) + return out +} + +// jpeg_t_seg walks the marker segments and returns the offset of the 0xFF that +// starts the first segment with this marker code, or -1. +@(private) +jpeg_t_seg :: proc(data: []byte, marker: byte) -> int { + p := 2 + for p + 3 < len(data) { + if data[p] != 0xFF { return -1 } + m := data[p + 1] + if m == marker { return p } + if m == 0xD8 || m == 0xD9 || (m >= 0xD0 && m <= 0xD7) || m == 0x01 { + p += 2 + continue + } + seglen := int(data[p + 2]) << 8 | int(data[p + 3]) + if seglen < 2 { return -1 } + if m == 0xDA { return -1 } + p += 2 + seglen + } + return -1 +} + +@(private) +jpeg_t_decode :: proc(t: ^testing.T, data: []byte, loc := #caller_location) -> Jpeg_Image { + img, err := jpeg_decode(data, context.allocator) + testing.expectf(t, err == .None, "decode failed: %v", err, loc = loc) + return img +} + +// jpeg_t_max_dev is the largest absolute difference between two same-length +// sample buffers. +@(private) +jpeg_t_max_dev :: proc(a, b: []byte) -> int { + worst := 0 + n := min(len(a), len(b)) + for i in 0 ..< n { + d := int(a[i]) - int(b[i]) + worst = max(worst, abs(d)) + } + return worst +} + +// --------------------------------------------------------------------------- +// Frame headers +// --------------------------------------------------------------------------- + +@(test) +test_jpeg_info :: proc(t: ^testing.T) { + Case :: struct { + name: string, + data: []byte, + w, h, n, bits: int, + } + cases := []Case{ + {"grey8", JPEG_T_GREY8[:], 8, 8, 1, 8}, + {"grey16", JPEG_T_GREY16[:], 16, 16, 1, 8}, + {"greyprog", JPEG_T_GREYPROG[:], 16, 16, 1, 8}, + {"rgb444", JPEG_T_RGB444[:], 16, 16, 3, 8}, + {"rgb420", JPEG_T_RGB420[:], 16, 16, 3, 8}, + {"rgbprog", JPEG_T_RGBPROG[:], 16, 16, 3, 8}, + {"cmyk", JPEG_T_CMYK[:], 16, 16, 4, 8}, + {"rst", JPEG_T_RST[:], 32, 16, 1, 8}, + {"dcflat", JPEG_T_DCFLAT[:], 8, 8, 1, 8}, + {"oddsamp", JPEG_T_ODDSAMP[:], 32, 8, 3, 8}, + } + for c in cases { + w, h, n, bits, err := jpeg_info(c.data) + testing.expectf(t, err == .None, "%s: %v", c.name, err) + testing.expectf(t, w == c.w && h == c.h, + "%s: size %dx%d, want %dx%d", c.name, w, h, c.w, c.h) + testing.expectf(t, n == c.n, "%s: %d components, want %d", c.name, n, c.n) + testing.expectf(t, bits == c.bits, "%s: %d bits, want %d", c.name, bits, c.bits) + } +} + +// --------------------------------------------------------------------------- +// Baseline sequential +// --------------------------------------------------------------------------- + +// A single 8x8 block whose only nonzero coefficient is the DC one, with an +// all-ones quantisation table. The inverse DCT of such a block is flat at +// S00/8 plus the level shift of 128 (ISO/IEC 10918-1, A.3.3 and A.3.1), so +// S00 = 40 must reconstruct as exactly 133 everywhere. This pins the IDCT's +// scaling and rounding against arithmetic rather than against another decoder. +@(test) +test_jpeg_dc_only_block :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_DCFLAT[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 8 && img.height == 8 && img.components == 1) + testing.expect(t, img.bits == 8) + testing.expect(t, img.adobe_transform == -1) + for v, i in img.data { + testing.expectf(t, v == 133, "sample %d is %d, want 133", i, v) + } +} + +// An 8x8 grey ramp. These are the exact bytes libjpeg produces for the same +// file, which they should be: both decoders use the same integer IDCT. +@(test) +test_jpeg_grey_baseline_exact :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_GREY8[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 8 && img.height == 8 && img.components == 1) + testing.expect(t, len(img.data) == 64) + testing.expect(t, slice.equal(img.data, JPEG_T_GREY8_PIXELS[:])) +} + +// 16x16, four blocks, a hard-edged 4x4 checkerboard: plenty of high-frequency +// coefficients, so this exercises the AC path and the ringing around an edge. +@(test) +test_jpeg_grey_baseline_blocks :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_GREY16[:]) + defer delete(img.data, context.allocator) + testing.expect(t, len(img.data) == 256) + worst := 0 + for y in 0 ..< 16 { + for x in 0 ..< 16 { + want := (x / 4 + y / 4) % 2 == 0 ? 255 : 0 + worst = max(worst, abs(int(img.data[y * 16 + x]) - want)) + } + } + // Quality 85 on an image that is nothing but block-aligned edges: the + // measured worst case is 6 levels. + testing.expectf(t, worst <= 8, "worst deviation from the source image is %d", worst) +} + +@(test) +test_jpeg_colour_444 :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_RGB444[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 16 && img.height == 16 && img.components == 3) + // Source: R = 16x, G = 16y, B = 128, encoded at quality 90 with no + // subsampling, so every channel round-trips within a few levels. + worst := 0 + for y in 0 ..< 16 { + for x in 0 ..< 16 { + o := (y * 16 + x) * 3 + worst = max(worst, abs(int(img.data[o]) - x * 16)) + worst = max(worst, abs(int(img.data[o + 1]) - y * 16)) + worst = max(worst, abs(int(img.data[o + 2]) - 128)) + } + } + testing.expectf(t, worst <= 6, "worst deviation from the source image is %d", worst) +} + +@(test) +test_jpeg_colour_420_subsampled :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_RGB420[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 16 && img.height == 16 && img.components == 3) + worst := 0 + for y in 0 ..< 16 { + for x in 0 ..< 16 { + o := (y * 16 + x) * 3 + worst = max(worst, abs(int(img.data[o]) - x * 16)) + worst = max(worst, abs(int(img.data[o + 1]) - y * 16)) + worst = max(worst, abs(int(img.data[o + 2]) - 128)) + } + } + // Half-resolution chroma on a 16-levels-per-pixel ramp cannot do better + // than about half a chroma sample of error; measured worst case is 16. + testing.expectf(t, worst <= 20, "worst deviation from the source image is %d", worst) +} + +// Sampling factors that no ordinary encoder emits: luma 4x1, one chroma plane +// at 1x1 and the other at 2x1, so the two chroma planes are upsampled by +// different, non-power-of-two-per-axis ratios. Every block carries only a DC +// coefficient, which makes each 8x8 region a flat colour that can be predicted +// from the YCbCr definition alone. +@(test) +test_jpeg_odd_sampling_factors :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_ODDSAMP[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 32 && img.height == 8 && img.components == 3) + + // Luma is flat at 28/108/148/228 across four 8-pixel bands, Cb is flat at + // 108 across the whole row, and Cr is 158 over the left half and 98 over + // the right. Converting with the JFIF coefficients gives: + want := [4][3]byte{ + {70, 13, 0}, {150, 93, 73}, {106, 176, 113}, {186, 255, 193}, + } + for y in 0 ..< 8 { + for x in 0 ..< 32 { + o := (y * 32 + x) * 3 + w := want[x / 8] + testing.expectf(t, + img.data[o] == w[0] && img.data[o + 1] == w[1] && img.data[o + 2] == w[2], + "pixel %d,%d is %d,%d,%d, want %d,%d,%d", + x, y, img.data[o], img.data[o + 1], img.data[o + 2], w[0], w[1], w[2]) + } + } +} + +// --------------------------------------------------------------------------- +// Progressive +// --------------------------------------------------------------------------- + +// A progressive encoding carries the same quantised coefficients as its +// baseline twin, just spread over several scans, so the two must reconstruct +// to identical bytes. That makes this an exact test of spectral selection and +// successive approximation, on both the DC and AC scans, without needing any +// expected pixels of its own. +@(test) +test_jpeg_progressive_matches_baseline :: proc(t: ^testing.T) { + grey_prog := jpeg_t_decode(t, JPEG_T_GREYPROG[:]) + defer delete(grey_prog.data, context.allocator) + grey_base := jpeg_t_decode(t, JPEG_T_GREY16[:]) + defer delete(grey_base.data, context.allocator) + testing.expect(t, slice.equal(grey_prog.data, grey_base.data), + "progressive grey does not match its baseline twin") + + rgb_prog := jpeg_t_decode(t, JPEG_T_RGBPROG[:]) + defer delete(rgb_prog.data, context.allocator) + rgb_base := jpeg_t_decode(t, JPEG_T_RGB420[:]) + defer delete(rgb_base.data, context.allocator) + testing.expect(t, slice.equal(rgb_prog.data, rgb_base.data), + "progressive colour does not match its baseline twin") +} + +// A progressive file cut off after its first scans still has a usable DC pass, +// so it must decode to a recognisable, blocky version of the image rather than +// to an error. +@(test) +test_jpeg_progressive_truncated :: proc(t: ^testing.T) { + full := jpeg_t_decode(t, JPEG_T_GREYPROG[:]) + defer delete(full.data, context.allocator) + + for cut in 1 ..= 8 { + n := len(JPEG_T_GREYPROG) * cut / 9 + img, err := jpeg_decode(JPEG_T_GREYPROG[:n], context.allocator) + defer delete(img.data, context.allocator) + if err != .None { + // Cutting inside the frame header legitimately leaves nothing. + continue + } + testing.expect(t, img.width == 16 && img.height == 16) + testing.expect(t, len(img.data) == 256) + } +} + +// --------------------------------------------------------------------------- +// Four components: CMYK and YCCK +// --------------------------------------------------------------------------- + +// The source image was CMYK (16x, 16y, 200, 30). Adobe writes the CMYK JPEGs +// it embeds in PDFs with every sample complemented, and the image dictionary +// carries /Decode [1 0 1 0 1 0 1 0] to undo it. The filter must therefore hand +// back the complemented samples untouched -- if it "helpfully" inverted them +// the /Decode array would invert them right back. +@(test) +test_jpeg_cmyk_is_not_inverted :: proc(t: ^testing.T) { + img := jpeg_t_decode(t, JPEG_T_CMYK[:]) + defer delete(img.data, context.allocator) + testing.expect(t, img.components == 4) + testing.expectf(t, img.adobe_transform == 0, + "adobe_transform is %d, want 0", img.adobe_transform) + + worst := 0 + for y in 0 ..< 16 { + for x in 0 ..< 16 { + o := (y * 16 + x) * 4 + worst = max(worst, abs(int(img.data[o]) - (255 - x * 16))) + worst = max(worst, abs(int(img.data[o + 1]) - (255 - y * 16))) + worst = max(worst, abs(int(img.data[o + 2]) - 55)) + worst = max(worst, abs(int(img.data[o + 3]) - 225)) + } + } + testing.expectf(t, worst <= 4, "worst deviation from the stored samples is %d", worst) +} + +// The same file with the Adobe transform byte changed from 0 to 2. Nothing +// else about the bit stream changes, so the component samples are identical +// and only the colour transform differs: the first three channels go through +// the inverse YCbCr transform and are then complemented, which is part of the +// YCCK definition and not the separate Adobe inversion above. +@(test) +test_jpeg_ycck :: proc(t: ^testing.T) { + app14 := jpeg_t_seg(JPEG_T_CMYK[:], 0xEE) + testing.expect(t, app14 > 0, "no APP14 segment") + if app14 < 0 { return } + + patched := jpeg_t_copy(JPEG_T_CMYK[:]) + defer delete(patched, context.allocator) + patched[app14 + 4 + 11] = 2 + + plain := jpeg_t_decode(t, JPEG_T_CMYK[:]) + defer delete(plain.data, context.allocator) + ycck := jpeg_t_decode(t, patched) + defer delete(ycck.data, context.allocator) + + testing.expectf(t, ycck.adobe_transform == 2, + "adobe_transform is %d, want 2", ycck.adobe_transform) + testing.expect(t, len(ycck.data) == len(plain.data)) + + worst := 0 + for i := 0; i + 3 < len(plain.data); i += 4 { + y := f64(plain.data[i]) + cb := f64(plain.data[i + 1]) - 128 + cr := f64(plain.data[i + 2]) - 128 + r := 255 - clamp(y + 1.402 * cr, 0, 255) + g := 255 - clamp(y - 0.344136 * cb - 0.714136 * cr, 0, 255) + b := 255 - clamp(y + 1.772 * cb, 0, 255) + worst = max(worst, abs(int(ycck.data[i]) - int(r + 0.5))) + worst = max(worst, abs(int(ycck.data[i + 1]) - int(g + 0.5))) + worst = max(worst, abs(int(ycck.data[i + 2]) - int(b + 0.5))) + // K is never transformed. + testing.expect(t, ycck.data[i + 3] == plain.data[i + 3]) + } + testing.expectf(t, worst <= 1, "YCCK transform differs from the definition by %d", worst) +} + +// --------------------------------------------------------------------------- +// Restart markers +// --------------------------------------------------------------------------- + +// The same image encoded twice at the same quality, once with a restart marker +// after every MCU and once without. Restart markers carry no information, so +// the two must decode identically. +@(test) +test_jpeg_restart_markers :: proc(t: ^testing.T) { + with_rst := jpeg_t_decode(t, JPEG_T_RST[:]) + defer delete(with_rst.data, context.allocator) + without := jpeg_t_decode(t, JPEG_T_NORST[:]) + defer delete(without.data, context.allocator) + testing.expect(t, jpeg_t_seg(JPEG_T_RST[:], 0xDD) > 0, "test file has no DRI segment") + testing.expect(t, jpeg_t_seg(JPEG_T_NORST[:], 0xDD) < 0, "control file has a DRI segment") + testing.expect(t, slice.equal(with_rst.data, without.data), + "restart markers changed the decoded image") +} + +// The same pair again, but over content whose MCUs cost only a few bits each: +// horizontal bars at quality 90, which is what a scanned page of text looks +// like to an encoder. That is the case a decoder gets wrong when it decides a +// restart marker is due from the position of its byte reader instead of from +// the declared interval. The reader buffers whole bytes ahead of the bits the +// block decoders have taken and stops dead on a marker, so once an interval's +// remaining blocks code to fewer than eight bits -- routine here, and certain +// in the one-bit-per-block DC refinement scan that every progressive file has +// -- the marker is already under the reader while blocks of that interval are +// still undecoded. Taking it there resets the DC predictors and the EOB run +// early and decodes the rest of the interval out of the next segment's bits. +// +// Both files decode byte-for-byte identically to libjpeg-turbo; before the +// interval gated the take, the baseline pair differed in 192 of 4096 samples +// and the progressive pair in 960, by up to 190. +@(test) +test_jpeg_restart_low_entropy_mcus :: proc(t: ^testing.T) { + Case :: struct { + name: string, + with, wro: []byte, + } + cases := []Case{ + {"baseline", JPEG_T_LOWENT_RST[:], JPEG_T_LOWENT[:]}, + {"progressive", JPEG_T_LOWENTP_RST[:], JPEG_T_LOWENTP[:]}, + } + for c in cases { + testing.expectf(t, jpeg_t_seg(c.with, 0xDD) > 0, "%s: test file has no DRI segment", c.name) + testing.expectf(t, jpeg_t_seg(c.wro, 0xDD) < 0, "%s: control file has a DRI segment", c.name) + + with_rst := jpeg_t_decode(t, c.with) + defer delete(with_rst.data, context.allocator) + without := jpeg_t_decode(t, c.wro) + defer delete(without.data, context.allocator) + + testing.expectf(t, with_rst.width == 64 && with_rst.height == 64, + "%s: %dx%d", c.name, with_rst.width, with_rst.height) + testing.expectf(t, slice.equal(with_rst.data, without.data), + "%s: restart markers changed the decoded image (max deviation %d)", + c.name, jpeg_t_max_dev(with_rst.data, without.data)) + } +} + +// A DRI that disagrees with the markers actually in the stream. +// +// This is damage, and unlike a truncation it is not repairable. The declared +// interval is the only thing that says how many MCUs a segment covers, so once +// it is wrong the decoder cannot tell which of the markers it can see ends the +// segment it is in; believing the markers instead is what breaks every intact +// file with a low-entropy interval. libjpeg does not recover these either -- +// patching a real file's DRI from 1 to 3 leaves it reporting "extraneous bytes +// before marker 0xd0" with 92% of its samples wrong. +// +// What is required is that the damage stays damage. The decode terminates, +// hands back an image of the size the frame header declared rather than an +// error, and does not corrupt what it had already decoded correctly: the first +// MCU is ahead of the first restart marker, so no disagreement about the +// interval can reach it. +@(test) +test_jpeg_restart_interval_wrong :: proc(t: ^testing.T) { + dri := jpeg_t_seg(JPEG_T_RST[:], 0xDD) + testing.expect(t, dri > 0) + if dri < 0 { return } + + good := jpeg_t_decode(t, JPEG_T_RST[:]) + defer delete(good.data, context.allocator) + + for claim in ([]int{0, 3, 7, 65535}) { + patched := jpeg_t_copy(JPEG_T_RST[:]) + defer delete(patched, context.allocator) + patched[dri + 4] = byte(claim >> 8) + patched[dri + 5] = byte(claim) + + img, err := jpeg_decode(patched, context.allocator) + defer delete(img.data, context.allocator) + testing.expectf(t, err == .None, "DRI %d: %v", claim, err) + testing.expectf(t, img.width == 32 && img.height == 16, + "DRI %d: %dx%d", claim, img.width, img.height) + if len(img.data) != len(good.data) { continue } + + // The 8x8 block at the origin is MCU 0, decoded before the stream + // reaches any restart marker at all. + for y in 0 ..< 8 { + for x in 0 ..< 8 { + i := y * 32 + x + testing.expectf(t, img.data[i] == good.data[i], + "DRI %d: sample (%d,%d) of the first MCU is %d, want %d", + claim, x, y, img.data[i], good.data[i]) + } + } + } +} + +// Restart markers are also a recovery point: with the entropy data of one +// segment corrupted, everything after the next marker still decodes. +@(test) +test_jpeg_restart_resync_after_corruption :: proc(t: ^testing.T) { + sos := -1 + p := 2 + for p + 3 < len(JPEG_T_RST) { + if JPEG_T_RST[p] != 0xFF { break } + m := JPEG_T_RST[p + 1] + seglen := int(JPEG_T_RST[p + 2]) << 8 | int(JPEG_T_RST[p + 3]) + if m == 0xDA { + sos = p + 2 + seglen + break + } + p += 2 + seglen + } + testing.expect(t, sos > 0) + if sos < 0 { return } + + good := jpeg_t_decode(t, JPEG_T_RST[:]) + defer delete(good.data, context.allocator) + + patched := jpeg_t_copy(JPEG_T_RST[:]) + defer delete(patched, context.allocator) + // Corrupt the first entropy-coded segment only. + patched[sos] = 0x5A + + img, err := jpeg_decode(patched, context.allocator) + defer delete(img.data, context.allocator) + testing.expectf(t, err == .None, "%v", err) + testing.expect(t, img.width == 32 && img.height == 16) + + // The bottom half of the image comes after several restart markers and + // must be untouched by the damage at the top. + testing.expect(t, slice.equal(img.data[32 * 8:], good.data[32 * 8:]), + "damage in the first restart interval leaked past a restart marker") +} + +// --------------------------------------------------------------------------- +// Tolerated malformations +// --------------------------------------------------------------------------- + +@(test) +test_jpeg_garbage_before_soi :: proc(t: ^testing.T) { + junk := "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\n\r\n" + data := make([]byte, len(junk) + len(JPEG_T_GREY8), context.allocator) + defer delete(data, context.allocator) + copy(data, transmute([]byte)junk) + copy(data[len(junk):], JPEG_T_GREY8[:]) + + img := jpeg_t_decode(t, data) + defer delete(img.data, context.allocator) + testing.expect(t, slice.equal(img.data, JPEG_T_GREY8_PIXELS[:])) +} + +@(test) +test_jpeg_missing_eoi :: proc(t: ^testing.T) { + trimmed := JPEG_T_GREY8[:len(JPEG_T_GREY8) - 2] + testing.expect(t, JPEG_T_GREY8[len(JPEG_T_GREY8) - 1] == 0xD9, "test file does not end in EOI") + img := jpeg_t_decode(t, trimmed) + defer delete(img.data, context.allocator) + testing.expect(t, slice.equal(img.data, JPEG_T_GREY8_PIXELS[:]), + "dropping EOI changed the image") +} + +// Fill bytes between segments are legal (ISO/IEC 10918-1, B.1.1.2) and +// stray bytes are not, but both turn up. Neither may derail the parse. +@(test) +test_jpeg_extra_bytes_between_markers :: proc(t: ^testing.T) { + sof := jpeg_t_seg(JPEG_T_GREY8[:], 0xC0) + testing.expect(t, sof > 0) + if sof < 0 { return } + + data := make([]byte, len(JPEG_T_GREY8) + 4, context.allocator) + defer delete(data, context.allocator) + copy(data, JPEG_T_GREY8[:sof]) + data[sof], data[sof + 1], data[sof + 2], data[sof + 3] = 0xFF, 0xFF, 0xFF, 0xFF + copy(data[sof + 4:], JPEG_T_GREY8[sof:]) + + img := jpeg_t_decode(t, data) + defer delete(img.data, context.allocator) + testing.expect(t, slice.equal(img.data, JPEG_T_GREY8_PIXELS[:])) +} + +// A frame header may leave the height at zero and let a DNL marker after the +// first scan supply it (ISO/IEC 10918-1, B.2.2). Rare, but the format allows +// it and a stream that was being generated on the fly may well use it. +@(test) +test_jpeg_dnl_height :: proc(t: ^testing.T) { + sof := jpeg_t_seg(JPEG_T_GREY16[:], 0xC0) + testing.expect(t, sof > 0) + if sof < 0 { return } + + n := len(JPEG_T_GREY16) + data := make([]byte, n + 6, context.allocator) + defer delete(data, context.allocator) + copy(data, JPEG_T_GREY16[:n - 2]) + hi, lo := data[sof + 5], data[sof + 6] + data[sof + 5], data[sof + 6] = 0, 0 + copy(data[n - 2:], []byte{0xFF, 0xDC, 0x00, 0x04, hi, lo, 0xFF, 0xD9}) + + w, h, _, _, ierr := jpeg_info(data) + testing.expect(t, ierr == .None) + testing.expectf(t, w == 16 && h == 16, "jpeg_info reports %dx%d", w, h) + + img := jpeg_t_decode(t, data) + defer delete(img.data, context.allocator) + reference := jpeg_t_decode(t, JPEG_T_GREY16[:]) + defer delete(reference.data, context.allocator) + testing.expect(t, img.height == 16) + testing.expect(t, slice.equal(img.data, reference.data)) +} + +// A quantisation value of zero would erase its coefficient and, in a decoder +// that divided rather than multiplied, divide by it. Treating it as 1 keeps +// the image and cannot fault. +@(test) +test_jpeg_zero_quant_value :: proc(t: ^testing.T) { + dqt := jpeg_t_seg(JPEG_T_GREY8[:], 0xDB) + testing.expect(t, dqt > 0) + if dqt < 0 { return } + + patched := jpeg_t_copy(JPEG_T_GREY8[:]) + defer delete(patched, context.allocator) + for i in 0 ..< 64 { + patched[dqt + 5 + i] = 0 + } + img, err := jpeg_decode(patched, context.allocator) + defer delete(img.data, context.allocator) + testing.expectf(t, err == .None, "%v", err) + testing.expect(t, len(img.data) == 64) +} + +// A scan that stops in the middle must yield the part that decoded. The rows +// that were fully covered before the cut have to be bit-exact: a decoder that +// resynchronised by guessing would corrupt them too. +@(test) +test_jpeg_truncated_scan :: proc(t: ^testing.T) { + good := jpeg_t_decode(t, JPEG_T_NORST[:]) + defer delete(good.data, context.allocator) + + intact_rows := 0 + for cut := len(JPEG_T_NORST) - 3; cut > 0; cut -= 1 { + img, err := jpeg_decode(JPEG_T_NORST[:cut], context.allocator) + defer delete(img.data, context.allocator) + if err != .None { continue } + testing.expectf(t, img.width == 32 && img.height == 16, + "cut at %d: %dx%d", cut, img.width, img.height) + testing.expectf(t, len(img.data) == 512, "cut at %d: %d bytes", cut, len(img.data)) + + rows := 0 + for rows < 16 { + lo, hi := rows * 32, (rows + 1) * 32 + if !slice.equal(img.data[lo:hi], good.data[lo:hi]) { break } + rows += 1 + } + intact_rows = max(intact_rows, rows) + } + // Trimming a byte or two off the end costs at most the last MCU row. + testing.expectf(t, intact_rows >= 8, + "a nearly-complete truncation only reproduced %d rows", intact_rows) +} + +// --------------------------------------------------------------------------- +// Refusals +// --------------------------------------------------------------------------- + +@(test) +test_jpeg_malformed :: proc(t: ^testing.T) { + Case :: struct { + name: string, + data: []byte, + want: Error, + } + // 12-bit-per-sample and unusual-but-legal headers belong in the tolerated + // set; these are the ones with genuinely nothing to hand back. + cases := []Case{ + {"empty", {}, .Bad_Jpeg_Data}, + {"no soi", {0x00, 0x01, 0x02, 0x03}, .Bad_Jpeg_Data}, + {"soi only", {0xFF, 0xD8}, .Bad_Jpeg_Data}, + {"soi eoi", {0xFF, 0xD8, 0xFF, 0xD9}, .Bad_Jpeg_Data}, + {"truncated sof", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00}, .Bad_Jpeg_Data}, + {"zero size", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xD9}, .Bad_Jpeg_Data}, + {"no components", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x08, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x00, 0xFF, 0xD9}, .Bad_Jpeg_Data}, + {"5 components", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x08, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x05, 0xFF, 0xD9}, .Bad_Jpeg_Data}, + // 65535 x 65535 is 4.3 gigapixels. + {"absurd size", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0xFF, 0xFF, + 0xFF, 0xFF, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xD9}, .Bitmap_Too_Large}, + // SOF3 is lossless, SOF9 is arithmetic-coded: implementable, but no + // producer emits either into a PDF. + {"lossless", {0xFF, 0xD8, 0xFF, 0xC3, 0x00, 0x0B, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xD9}, .Unsupported_Image}, + {"arithmetic", {0xFF, 0xD8, 0xFF, 0xC9, 0x00, 0x0B, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xD9}, .Unsupported_Image}, + // A frame header with no scan at all. + {"frame no scan", {0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xD9}, .Bad_Jpeg_Data}, + } + for c in cases { + img, err := jpeg_decode(c.data, context.allocator) + defer delete(img.data, context.allocator) + testing.expectf(t, err == c.want, "%s: got %v, want %v", c.name, err, c.want) + testing.expectf(t, img.data == nil, "%s: returned data despite %v", c.name, err) + } +} + +// --------------------------------------------------------------------------- +// Scan count +// --------------------------------------------------------------------------- + +// jpeg_t_bits is the bit writer the two synthetic files below share. It escapes +// 0xFF the way an encoder must, so the entropy data can never be mistaken for a +// marker. +@(private = "file") +Jpeg_T_Bits :: struct { + out: [dynamic]byte, + acc: u32, + n: uint, +} + +@(private = "file") +jpeg_t_put :: proc(w: ^Jpeg_T_Bits, value: u32, count: uint) { + for i := count; i > 0; i -= 1 { + w.acc = w.acc << 1 | (value >> (i - 1)) & 1 + w.n += 1 + if w.n == 8 { + b := byte(w.acc) + append(&w.out, b) + if b == 0xFF { append(&w.out, 0x00) } + w.acc, w.n = 0, 0 + } + } +} + +// jpeg_t_flush pads to a byte boundary with 1 bits, which is what ISO/IEC +// 10918-1 F.1.2.3 requires of an encoder. +@(private = "file") +jpeg_t_flush :: proc(w: ^Jpeg_T_Bits) { + for w.n != 0 { jpeg_t_put(w, 1, 1) } +} + +@(private = "file") +jpeg_t_seg_append :: proc(out: ^[dynamic]byte, marker: byte, payload: []byte) { + n := len(payload) + 2 + append(out, 0xFF, marker, byte(n >> 8), byte(n)) + append(out, ..payload) +} + +// jpeg_t_dc_scan_file builds a progressive JPEG of a single 8x8 block whose +// scans are all DC-first scans of the same coefficient. Every scan overwrites +// what the one before it wrote, so the sample the block reconstructs to names +// the last scan that actually ran -- which is how the scan limit is observed +// without timing anything. +// +// All the scans but the last code a DC of 8; the last codes 120. With an +// all-ones quantisation table the block reconstructs flat at S00/8 + 128 +// (ISO/IEC 10918-1, A.3.3), so the image is 129 everywhere if the last scan was +// refused and 143 everywhere if it ran. +@(private = "file") +jpeg_t_dc_scan_file :: proc(nscans: int, allocator := context.allocator) -> []byte { + out := make([dynamic]byte, allocator) + append(&out, 0xFF, 0xD8) + + dqt: [65]byte + for i in 1 ..< 65 { dqt[i] = 1 } + jpeg_t_seg_append(&out, 0xDB, dqt[:]) + + // SOF2: 8 bits, 8x8, one 1x1 component using quantisation table 0. + jpeg_t_seg_append(&out, 0xC2, []byte{8, 0, 8, 0, 8, 1, 1, 0x11, 0}) + + // DHT, DC table 0: eight codes of length 4, so magnitude category s is the + // 4-bit code s. + dht: [17]byte + dht[0] = 0x00 + dht[4] = 8 + tbl := make([]byte, 25, context.temp_allocator) + copy(tbl, dht[:]) + for s in 0 ..< 8 { tbl[17 + s] = byte(s) } + jpeg_t_seg_append(&out, 0xC4, tbl) + + for i in 0 ..< nscans { + jpeg_t_seg_append(&out, 0xDA, []byte{1, 1, 0x00, 0, 0, 0x00}) + w := Jpeg_T_Bits{out = make([dynamic]byte, context.temp_allocator)} + if i == nscans - 1 { + jpeg_t_put(&w, 7, 4) // category 7: 64..127 + jpeg_t_put(&w, 120, 7) + } else { + jpeg_t_put(&w, 4, 4) // category 4: 8..15 + jpeg_t_put(&w, 8, 4) + } + jpeg_t_flush(&w) + append(&out, ..w.out[:]) + } + + append(&out, 0xFF, 0xD9) + return out[:] +} + +// The scan count is bounded on its own, not just by the segment count. +// +// A progressive scan is not cheap per byte: one EOB run costs 18 bits and skips +// up to 32767 blocks, so a scan whose entropy data is a handful of bytes still +// drives the MCU loop over the whole component. Bounding only the number of +// marker segments therefore bounds the work at 65536 sweeps of a frame that may +// hold millions of blocks, which is minutes of CPU from a stream of a megabyte. +@(test) +test_jpeg_scan_count_is_capped :: proc(t: ^testing.T) { + over := jpeg_t_dc_scan_file(MAX_JPEG_SCANS + 5, context.allocator) + defer delete(over, context.allocator) + img := jpeg_t_decode(t, over) + defer delete(img.data, context.allocator) + testing.expect(t, img.width == 8 && img.height == 8 && img.components == 1) + for v, i in img.data { + testing.expectf(t, v == 129, + "sample %d is %d: a scan past the limit was decoded", i, v) + } + + // The same construction inside the limit must decode every scan, so that + // the assertion above is about the limit and not about the file. + under := jpeg_t_dc_scan_file(3, context.allocator) + defer delete(under, context.allocator) + ok := jpeg_t_decode(t, under) + defer delete(ok.data, context.allocator) + for v, i in ok.data { + testing.expectf(t, v == 143, "sample %d is %d: the last scan did not run", i, v) + } +} + +// jpeg_t_eob_dos_file is the shape that makes the scan count expensive: a large +// single-component progressive frame whose every scan covers it with EOB runs. +// Nine bytes of entropy data per scan visit 65536 blocks. +@(private = "file") +jpeg_t_eob_dos_file :: proc(side, nscans: int, allocator := context.allocator) -> []byte { + out := make([dynamic]byte, allocator) + append(&out, 0xFF, 0xD8) + + dqt: [65]byte + for i in 1 ..< 65 { dqt[i] = 1 } + jpeg_t_seg_append(&out, 0xDB, dqt[:]) + jpeg_t_seg_append(&out, 0xC2, []byte{ + 8, byte(side >> 8), byte(side), byte(side >> 8), byte(side), 1, 1, 0x11, 0, + }) + // DHT, AC table 0: two codes of length 4 for the symbols 0x00 (EOB0) and + // 0xE0 (EOB14), so EOB14 is the 4-bit code 0b0001. + dht: [17]byte + dht[0] = 0x10 + dht[4] = 2 + tbl := make([]byte, 19, context.temp_allocator) + copy(tbl, dht[:]) + tbl[17], tbl[18] = 0x00, 0xE0 + jpeg_t_seg_append(&out, 0xC4, tbl) + + blocks := ((side + 7) / 8) * ((side + 7) / 8) + w := Jpeg_T_Bits{out = make([dynamic]byte, context.temp_allocator)} + for left := blocks; left > 0; { + // EOB14 covers 1<<14 blocks plus its 14 extra bits. + run := min(left, 1 << 14) + jpeg_t_put(&w, 1, 4) + jpeg_t_put(&w, u32(run - (1 << 14)) & 0x3FFF, 14) + left -= run + } + jpeg_t_flush(&w) + body := w.out[:] + + for _ in 0 ..< nscans { + jpeg_t_seg_append(&out, 0xDA, []byte{1, 1, 0x00, 1, 63, 0x00}) + append(&out, ..body) + } + append(&out, 0xFF, 0xD9) + return out[:] +} + +@(test) +test_jpeg_many_scans_terminate :: proc(t: ^testing.T) { + data := jpeg_t_eob_dos_file(2048, 20000, context.allocator) + defer delete(data, context.allocator) + + started := time.now() + img, err := jpeg_decode(data, context.allocator) + defer delete(img.data, context.allocator) + ms := time.duration_milliseconds(time.since(started)) + + testing.expectf(t, err == .None, "%v", err) + testing.expect(t, img.width == 2048 && img.height == 2048) + // Not a performance bound: with the scan count unbounded this same file + // takes about fifteen seconds, and at the largest frame the header may + // declare it is closer to an hour. The limit is loose enough that a loaded + // machine cannot trip it. + testing.expectf(t, ms < 5000, "%d scans took %.0f ms", 20000, ms) +} + +// --------------------------------------------------------------------------- +// Fuzz-shaped +// --------------------------------------------------------------------------- + +@(private) +jpeg_t_corpus :: proc() -> [10][]byte { + return [10][]byte{ + JPEG_T_GREY8[:], JPEG_T_GREY16[:], JPEG_T_GREYPROG[:], JPEG_T_RGB420[:], + JPEG_T_RGBPROG[:], JPEG_T_CMYK[:], JPEG_T_PROGRST[:], JPEG_T_ODDSAMP[:], + JPEG_T_LOWENT_RST[:], JPEG_T_LOWENTP_RST[:], + } +} + +// Every prefix of every test file. The decoder must return either an error or +// a correctly-sized image, and must not fault, hang or leak on any of them. +@(test) +test_jpeg_fuzz_truncation :: proc(t: ^testing.T) { + corpus := jpeg_t_corpus() + for src in corpus { + w, h, n, _, ierr := jpeg_info(src) + testing.expect(t, ierr == .None) + for cut in 0 ..= len(src) { + img, err := jpeg_decode(src[:cut], context.allocator) + defer delete(img.data, context.allocator) + if err != .None { + testing.expect(t, img.data == nil) + continue + } + testing.expectf(t, img.width == w && img.height == h && img.components == n, + "cut at %d: %dx%dx%d, want %dx%dx%d", + cut, img.width, img.height, img.components, w, h, n) + testing.expect(t, len(img.data) == w * h * n) + } + } +} + +// One flipped bit at every byte offset. Corruption in a header can legitimately +// change the reported size or make the file unreadable; what it must never do +// is produce a buffer whose length disagrees with the header the decoder acted +// on, or take unbounded time. +@(test) +test_jpeg_fuzz_bitflip :: proc(t: ^testing.T) { + corpus := jpeg_t_corpus() + for src in corpus { + scratch := jpeg_t_copy(src) + defer delete(scratch, context.allocator) + for i in 0 ..< len(src) { + for bit in ([]uint{0, 3, 7}) { + scratch[i] = src[i] ~ (1 << bit) + img, err := jpeg_decode(scratch, context.allocator) + if err == .None { + testing.expectf(t, + len(img.data) == img.width * img.height * img.components, + "offset %d bit %d: %d bytes for %dx%dx%d", + i, bit, len(img.data), img.width, img.height, img.components) + testing.expect(t, img.width > 0 && img.height > 0) + testing.expect(t, img.components >= 1 && img.components <= 4) + } else { + testing.expect(t, img.data == nil) + } + delete(img.data, context.allocator) + } + scratch[i] = src[i] + } + } +} + +// A single decode of a few-hundred-byte fuzz case that runs longer than this +// has stopped being slow and started being stuck. +@(private = "file") +JPEG_T_FUZZ_HANG_MS :: 5000.0 + +// Two bytes of a marker segment length turned into something degenerate -- too +// short to hold the segment's own length field, or far longer than the file -- +// at every segment in turn. The parser has to clamp to the data it has rather +// than reading past the end of the buffer. +// +// The outcome is not predictable per mutation: a length that swallows the rest +// of the file may still leave a decodable image, and a mangled SOF may not. +// What is predictable is the shape of whatever comes back, so that is what is +// asserted -- an error hands back nothing to free, and a success hands back a +// buffer that matches the dimensions it reports. A decoder that allocated for +// the old header and then wrote for a new one would fail here. +@(test) +test_jpeg_fuzz_segment_lengths :: proc(t: ^testing.T) { + corpus := jpeg_t_corpus() + for src in corpus { + for i := 2; i + 3 < len(src); i += 1 { + if src[i] != 0xFF || src[i + 1] == 0x00 || src[i + 1] == 0xFF { continue } + for value in ([]int{0, 1, 2, 3, 0xFFFF}) { + scratch := jpeg_t_copy(src) + defer delete(scratch, context.allocator) + scratch[i + 2] = byte(value >> 8) + scratch[i + 3] = byte(value) + + started := time.now() + img, err := jpeg_decode(scratch, context.allocator) + defer delete(img.data, context.allocator) + + // Not a performance bound: the inputs are a few hundred bytes, + // so a decode measured in seconds is a loop whose bound came + // out of the mutated length field. The limit is loose enough + // that a loaded machine cannot trip it. + ms := time.duration_milliseconds(time.since(started)) + testing.expectf(t, ms < JPEG_T_FUZZ_HANG_MS, + "offset %d length %#x: decode took %.0f ms", i, value, ms) + + if err != .None { + testing.expectf(t, img.data == nil, + "offset %d length %#x: failed decode returned %d bytes", + i, value, len(img.data)) + continue + } + // Every dimension the decoder reports has to be one it + // allocated for: this is the check that a segment length is + // never able to make the buffer and the header disagree. + testing.expectf(t, + img.width > 0 && img.height > 0 && + img.components >= 1 && img.components <= 4, + "offset %d length %#x: decoded %dx%dx%d", + i, value, img.width, img.height, img.components) + testing.expectf(t, img.width <= MAX_JPEG_PIXELS / max(img.height, 1), + "offset %d length %#x: %dx%d is past the pixel cap", + i, value, img.width, img.height) + testing.expectf(t, + len(img.data) == img.width * img.height * img.components, + "offset %d length %#x: %d bytes for %dx%dx%d", + i, value, len(img.data), img.width, img.height, img.components) + testing.expectf(t, img.bits == 8, + "offset %d length %#x: decoded image claims %d bits", i, value, img.bits) + } + } + } +} + +// Progressive and restart markers together: a progressive file whose scans are +// each broken into restart intervals. The end-of-band run counter is scan +// state like the DC predictors and has to be cleared at every restart, which +// is the thing this combination catches. +@(test) +test_jpeg_progressive_with_restarts :: proc(t: ^testing.T) { + testing.expect(t, jpeg_t_seg(JPEG_T_PROGRST[:], 0xDD) > 0, "test file has no DRI segment") + img := jpeg_t_decode(t, JPEG_T_PROGRST[:]) + defer delete(img.data, context.allocator) + plain := jpeg_t_decode(t, JPEG_T_GREY16[:]) + defer delete(plain.data, context.allocator) + testing.expect(t, slice.equal(img.data, plain.data), + "progressive with restarts does not match the baseline encoding") +} + +// 12-bit samples. The format allows them; almost nothing produces them, and a +// PDF's /BitsPerComponent has nowhere to say so. `jpeg_info` reports the +// precision the file declares so a caller can complain about it, while +// `jpeg_decode` scales the samples down to the byte-per-sample buffer it +// always returns. The file is one block with DC = 640 against an all-ones +// quantisation table, which after the 4-bit downshift is the same flat 133 as +// the 8-bit version. +@(test) +test_jpeg_12_bit :: proc(t: ^testing.T) { + _, _, _, bits, err := jpeg_info(JPEG_T_BITS12[:]) + testing.expect(t, err == .None) + testing.expectf(t, bits == 12, "jpeg_info reports %d bits, want 12", bits) + + img := jpeg_t_decode(t, JPEG_T_BITS12[:]) + defer delete(img.data, context.allocator) + testing.expectf(t, img.bits == 8, "decoded image claims %d bits", img.bits) + testing.expect(t, len(img.data) == 64) + for v, i in img.data { + testing.expectf(t, v == 133, "sample %d is %d, want 133", i, v) + } +} + +// --------------------------------------------------------------------------- +// Embedded files +// --------------------------------------------------------------------------- + +@(private) +JPEG_T_GREY8_PIXELS := [64]byte{ + 20, 45, 71, 94, 121, 145, 170, 195, 22, 48, 73, 97, 124, 147, 173, 198, + 26, 51, 77, 100, 127, 151, 176, 201, 29, 54, 80, 103, 130, 154, 179, 204, + 31, 57, 82, 106, 133, 156, 182, 207, 34, 60, 85, 109, 135, 159, 185, 210, + 38, 63, 89, 112, 139, 163, 188, 213, 40, 66, 91, 115, 141, 165, 191, 216, +} + +@(private) +JPEG_T_GREY8 := [341]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08, + 0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xC4, 0xFD, 0x86, 0xF4, 0x4F, 0xF9, 0x07, 0xFC, + 0xBF, 0xDD, 0xAF, 0xFF, 0xD9, +} + +@(private) +JPEG_T_GREY16 := [442]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, + 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x07, 0x0C, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0F, 0x0B, + 0x0B, 0x09, 0x0C, 0x11, 0x0F, 0x12, 0x12, 0x11, 0x0F, 0x11, 0x11, 0x13, 0x16, 0x1C, 0x17, 0x13, + 0x14, 0x1A, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18, 0x1A, 0x1D, 0x1D, 0x1F, 0x1F, 0x1F, 0x13, 0x17, + 0x22, 0x24, 0x22, 0x1E, 0x24, 0x1C, 0x1E, 0x1F, 0x1E, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x10, + 0x00, 0x10, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x4F, 0xF9, 0x3D, 0x1F, 0xFA, 0x91, 0x3F, 0xE1, + 0x0E, 0xFF, 0x00, 0xB8, 0x97, 0xDA, 0xFE, 0xD9, 0xFF, 0x00, 0x7E, 0x7C, 0xBD, 0x9F, 0x65, 0xFF, + 0x00, 0x6B, 0x3B, 0xFB, 0x63, 0x93, 0xFE, 0x4F, 0x47, 0xFE, 0xA4, 0x4F, 0xF8, 0x43, 0xBF, 0xEE, + 0x25, 0xF6, 0xBF, 0xB6, 0x7F, 0xDF, 0x9F, 0x2F, 0x67, 0xD9, 0x7F, 0xDA, 0xCE, 0xFE, 0xD8, 0xE4, + 0xFF, 0x00, 0x93, 0xD1, 0xFF, 0x00, 0xA9, 0x13, 0xFE, 0x10, 0xEF, 0xFB, 0x89, 0x7D, 0xAF, 0xED, + 0x9F, 0xF7, 0xE7, 0xCB, 0xD9, 0xF6, 0x5F, 0xF6, 0xB3, 0xBF, 0xB6, 0x39, 0x3F, 0xE4, 0xF4, 0x7F, + 0xEA, 0x44, 0xFF, 0x00, 0x84, 0x3B, 0xFE, 0xE2, 0x5F, 0x6B, 0xFB, 0x67, 0xFD, 0xF9, 0xF2, 0xF6, + 0x7D, 0x97, 0xFD, 0xAC, 0xEF, 0xED, 0x8E, 0x7F, 0xFF, 0xD9, +} + +@(private) +JPEG_T_GREYPROG := [352]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, + 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x07, 0x0C, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0F, 0x0B, + 0x0B, 0x09, 0x0C, 0x11, 0x0F, 0x12, 0x12, 0x11, 0x0F, 0x11, 0x11, 0x13, 0x16, 0x1C, 0x17, 0x13, + 0x14, 0x1A, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18, 0x1A, 0x1D, 0x1D, 0x1F, 0x1F, 0x1F, 0x13, 0x17, + 0x22, 0x24, 0x22, 0x1E, 0x24, 0x1C, 0x1E, 0x1F, 0x1E, 0xFF, 0xC2, 0x00, 0x0B, 0x08, 0x00, 0x10, + 0x00, 0x10, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xDA, 0x00, + 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x83, 0xFF, 0xC4, 0x00, 0x15, 0x10, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0xFF, + 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0xB4, 0x5A, 0x2D, 0x16, 0x8F, 0xFF, 0xC4, + 0x00, 0x1B, 0x10, 0x00, 0x01, 0x04, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x11, 0x14, 0x32, 0x54, 0x83, 0x91, 0xFF, 0xDA, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x06, 0x3F, 0x02, 0xC1, 0x87, 0xB0, 0x8F, 0x8D, 0x55, 0x83, 0x0F, 0x61, 0x1F, 0x1A, + 0xAB, 0x06, 0x1E, 0xC2, 0x3E, 0x35, 0x56, 0x0C, 0x3D, 0x84, 0x7C, 0x6A, 0xAF, 0xFF, 0xC4, 0x00, + 0x18, 0x10, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x31, 0x71, 0xF0, 0xF1, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, + 0x21, 0xAB, 0x35, 0x36, 0x55, 0x9A, 0x9B, 0x2A, 0xCD, 0x4D, 0x95, 0x66, 0xA6, 0xCF, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x10, 0xFF, 0x00, 0xFF, 0xC4, 0x00, 0x18, 0x10, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, + 0xF0, 0x21, 0x51, 0x71, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x10, 0x7A, 0x8E, + 0x95, 0x12, 0xF5, 0x1D, 0x2A, 0x25, 0xEA, 0x3A, 0x54, 0x4B, 0xD4, 0x74, 0xA8, 0x9F, 0xFF, 0xD9, +} + +@(private) +JPEG_T_RGB444 := [717]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x03, 0x04, + 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0D, 0x0B, 0x0D, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xFF, 0xC0, + 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, + 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, + 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, + 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, + 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, + 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, + 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, + 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, + 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, + 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, + 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, + 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, + 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, + 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, + 0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xF9, + 0x57, 0xC2, 0x9F, 0x0A, 0x7E, 0xE7, 0xEE, 0x7F, 0x4A, 0xFD, 0x83, 0x19, 0x9C, 0x6F, 0xA9, 0xE7, + 0xF0, 0xDE, 0x7F, 0xF0, 0xFB, 0xC7, 0xB3, 0x78, 0x4F, 0xE1, 0x4F, 0xDC, 0xFD, 0xCF, 0xE9, 0x5F, + 0x0D, 0x8C, 0xCE, 0x37, 0xD4, 0xFE, 0xAB, 0xE1, 0xBC, 0xFF, 0x00, 0xE1, 0xF7, 0x8F, 0x62, 0xF0, + 0x9F, 0xC2, 0x9F, 0xB9, 0xFB, 0x9F, 0xD2, 0xBE, 0x1F, 0x19, 0x9C, 0x6F, 0xA9, 0xFE, 0x63, 0x70, + 0xDE, 0x7D, 0xF0, 0xEA, 0x7B, 0x37, 0x85, 0x3E, 0x14, 0xFD, 0xCF, 0xDC, 0xFE, 0x95, 0xF0, 0xB8, + 0xCC, 0xE3, 0x7D, 0x4F, 0xEA, 0xAE, 0x1B, 0xCF, 0xBE, 0x1D, 0x4F, 0xFF, 0xD9, +} + +@(private) +JPEG_T_RGB420 := [678]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x03, 0x04, + 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0D, 0x0B, 0x0D, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xFF, 0xC0, + 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, + 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, + 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, + 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, + 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, + 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, + 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00, 0x03, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, + 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, + 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, + 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, + 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, + 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, + 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, + 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, + 0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xF9, + 0x57, 0xC2, 0x9F, 0x0A, 0x7E, 0xE7, 0xEE, 0x7F, 0x4A, 0xF6, 0x6F, 0x09, 0xFC, 0x29, 0xFB, 0x9F, + 0xB9, 0xFD, 0x2B, 0xD8, 0xBC, 0x27, 0xF0, 0xA7, 0xEE, 0x7E, 0xE7, 0xF4, 0xAF, 0x66, 0xF0, 0xA7, + 0xC2, 0x9F, 0xB9, 0xFB, 0x9F, 0xD2, 0xBA, 0xF3, 0x9E, 0x30, 0xDF, 0xDE, 0x27, 0xC3, 0x6E, 0x3D, + 0xF8, 0x3D, 0xFE, 0xC7, 0xFF, 0xD9, +} + +@(private) +JPEG_T_RGBPROG := [556]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x03, 0x04, + 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0D, 0x0B, 0x0D, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xFF, 0xC2, + 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, + 0x01, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0xFF, 0xC4, 0x00, 0x15, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0xFF, + 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01, 0x95, 0x33, 0x62, + 0xCE, 0xA7, 0xFF, 0xC4, 0x00, 0x16, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, + 0x00, 0x01, 0x05, 0x02, 0x56, 0x50, 0xA4, 0xA1, 0x49, 0x42, 0xB2, 0x8F, 0xFF, 0xC4, 0x00, 0x18, + 0x11, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x06, 0x21, 0x31, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x01, + 0x8D, 0xBE, 0xCB, 0x3F, 0xFF, 0xC4, 0x00, 0x17, 0x11, 0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x61, 0xFF, 0xDA, 0x00, + 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x01, 0x72, 0xC6, 0x9F, 0xFF, 0xC4, 0x00, 0x15, 0x10, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x31, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x88, 0x88, 0xFF, 0xC4, 0x00, + 0x15, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x31, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x21, 0x9B, 0x16, + 0x2C, 0xDF, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x10, + 0x0B, 0xFF, 0xC4, 0x00, 0x15, 0x11, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, + 0x3F, 0x10, 0x9E, 0xFF, 0xC4, 0x00, 0x15, 0x11, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, + 0x01, 0x01, 0x3F, 0x10, 0xB1, 0xFF, 0xC4, 0x00, 0x15, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xFF, 0xDA, 0x00, 0x08, + 0x01, 0x01, 0x00, 0x01, 0x3F, 0x10, 0x94, 0x84, 0x84, 0x94, 0xFF, 0xD9, +} + +@(private) +JPEG_T_CMYK := [431]byte{ + 0xFF, 0xD8, 0xFF, 0xEE, 0x00, 0x0E, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, + 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, 0x07, 0x06, + 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, 0x0E, 0x11, + 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, 0x17, 0x18, + 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC0, 0x00, 0x14, 0x08, 0x00, 0x10, 0x00, 0x10, + 0x04, 0x43, 0x11, 0x00, 0x4D, 0x11, 0x00, 0x59, 0x11, 0x00, 0x4B, 0x11, 0x00, 0xFF, 0xC4, 0x00, + 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, + 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, + 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, + 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, + 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, + 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, + 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, + 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, + 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x0E, 0x04, 0x43, 0x00, 0x4D, 0x00, 0x59, 0x00, + 0x4B, 0x00, 0x00, 0x3F, 0x00, 0xFA, 0xF7, 0xF6, 0x92, 0xFF, 0x00, 0x97, 0xAF, 0xC6, 0xBE, 0xBD, + 0xFF, 0x00, 0x86, 0x92, 0xFF, 0x00, 0xA7, 0xAF, 0xFC, 0x7A, 0xBE, 0x3C, 0xAF, 0xD0, 0x3A, 0xFC, + 0xAA, 0xFD, 0xA4, 0xBF, 0xE5, 0xEB, 0xF1, 0xA3, 0xFE, 0x1A, 0x4B, 0xFE, 0x9E, 0xBF, 0xF1, 0xEA, + 0x28, 0xAF, 0xD5, 0x5F, 0xDA, 0x4B, 0xFE, 0x5E, 0xBF, 0x1A, 0xFC, 0xAA, 0xFF, 0x00, 0x86, 0x92, + 0xFF, 0x00, 0xA7, 0xAF, 0xFC, 0x7A, 0x8A, 0x2B, 0xF2, 0xAB, 0xF6, 0x92, 0xFF, 0x00, 0x97, 0xAF, + 0xC6, 0x8F, 0xF8, 0x69, 0x2F, 0xFA, 0x7A, 0xFF, 0x00, 0xC7, 0xA8, 0xA2, 0xBF, 0xFF, 0xD9, +} + +@(private) +JPEG_T_RST := [410]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, + 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x07, 0x0C, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0F, 0x0B, + 0x0B, 0x09, 0x0C, 0x11, 0x0F, 0x12, 0x12, 0x11, 0x0F, 0x11, 0x11, 0x13, 0x16, 0x1C, 0x17, 0x13, + 0x14, 0x1A, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18, 0x1A, 0x1D, 0x1D, 0x1F, 0x1F, 0x1F, 0x13, 0x17, + 0x22, 0x24, 0x22, 0x1E, 0x24, 0x1C, 0x1E, 0x1F, 0x1E, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x10, + 0x00, 0x20, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDD, + 0x00, 0x04, 0x00, 0x01, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xF0, 0x5D, + 0x07, 0x4D, 0xFB, 0xBF, 0x2D, 0x7F, 0xFF, 0xD0, 0xE1, 0x74, 0x1D, 0x37, 0xEE, 0xFC, 0xB5, 0xFF, + 0xD1, 0x9B, 0x41, 0xD3, 0x7E, 0xEF, 0xCB, 0x5F, 0xFF, 0xD2, 0xF4, 0x6D, 0x07, 0x4D, 0xFB, 0xBF, + 0x2D, 0x7F, 0xFF, 0xD3, 0xF3, 0x8D, 0x07, 0x4D, 0xFB, 0xBF, 0x2D, 0x7F, 0xFF, 0xD4, 0x83, 0x41, + 0xD3, 0x7E, 0xEF, 0xCB, 0x5F, 0xFF, 0xD5, 0xEE, 0x74, 0x1D, 0x37, 0xEE, 0xFC, 0xB5, 0xFF, 0xD6, + 0xF7, 0x9D, 0x07, 0x4D, 0xFB, 0xBF, 0x2D, 0x7F, 0xFF, 0xD9, +} + +@(private) +JPEG_T_NORST := [388]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, + 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x07, 0x0C, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0F, 0x0B, + 0x0B, 0x09, 0x0C, 0x11, 0x0F, 0x12, 0x12, 0x11, 0x0F, 0x11, 0x11, 0x13, 0x16, 0x1C, 0x17, 0x13, + 0x14, 0x1A, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18, 0x1A, 0x1D, 0x1D, 0x1F, 0x1F, 0x1F, 0x13, 0x17, + 0x22, 0x24, 0x22, 0x1E, 0x24, 0x1C, 0x1E, 0x1F, 0x1E, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x10, + 0x00, 0x20, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xF0, 0x5D, 0x07, 0x4D, 0xFB, 0xBF, 0x2D, 0x7A, + 0x06, 0x83, 0xA6, 0xFD, 0xDF, 0x96, 0xBD, 0x03, 0x41, 0xD3, 0x7E, 0xEF, 0xCB, 0x5E, 0x81, 0xA0, + 0xE9, 0xBF, 0x77, 0xE5, 0xAF, 0x9C, 0xB4, 0x1D, 0x37, 0xEE, 0xFC, 0xB5, 0xE8, 0x1A, 0x0E, 0x9B, + 0xF7, 0x7E, 0x5A, 0xF4, 0x0D, 0x07, 0x4D, 0xFB, 0xBF, 0x2D, 0x7A, 0x06, 0x83, 0xA6, 0xFD, 0xDF, + 0x96, 0xBF, 0xFF, 0xD9, +} + +@(private) +JPEG_T_DCFLAT := [153]byte{ + 0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08, 0x00, 0x08, + 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x00, 0x3F, 0x00, 0xEA, 0x1F, 0xFF, 0xD9, +} + +@(private) +JPEG_T_ODDSAMP := [177]byte{ + 0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x20, + 0x03, 0x01, 0x41, 0x00, 0x02, 0x11, 0x00, 0x03, 0x21, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, + 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x10, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x3F, 0x00, 0xFE, + 0x37, 0xDF, 0xD4, 0x03, 0xF5, 0x01, 0xFD, 0x40, 0x3E, 0x5F, 0x7D, 0xE0, 0xFC, 0x1F, 0x7F, 0xFF, + 0xD9, +} + +@(private) +JPEG_T_PROGRST := [408]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x05, 0x03, 0x04, 0x04, 0x04, 0x03, 0x05, + 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x07, 0x0C, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0F, 0x0B, + 0x0B, 0x09, 0x0C, 0x11, 0x0F, 0x12, 0x12, 0x11, 0x0F, 0x11, 0x11, 0x13, 0x16, 0x1C, 0x17, 0x13, + 0x14, 0x1A, 0x15, 0x11, 0x11, 0x18, 0x21, 0x18, 0x1A, 0x1D, 0x1D, 0x1F, 0x1F, 0x1F, 0x13, 0x17, + 0x22, 0x24, 0x22, 0x1E, 0x24, 0x1C, 0x1E, 0x1F, 0x1E, 0xFF, 0xC2, 0x00, 0x0B, 0x08, 0x00, 0x10, + 0x00, 0x10, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xDD, 0x00, 0x04, + 0x00, 0x01, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x3F, 0xFF, 0xD0, 0x3F, + 0xFF, 0xD1, 0x3F, 0xFF, 0xD2, 0x3F, 0xFF, 0xC4, 0x00, 0x15, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0xFF, 0xDA, 0x00, + 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0xB4, 0x7F, 0xFF, 0xD0, 0xB4, 0x7F, 0xFF, 0xD1, 0xB4, + 0x7F, 0xFF, 0xD2, 0xB4, 0x7F, 0xFF, 0xC4, 0x00, 0x1B, 0x10, 0x00, 0x01, 0x04, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x11, 0x14, 0x32, 0x54, + 0x83, 0x91, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0xC1, 0x87, 0xB0, 0x8F, + 0x8D, 0x55, 0xFF, 0xD0, 0xC1, 0x87, 0xB0, 0x8F, 0x8D, 0x55, 0xFF, 0xD1, 0xC1, 0x87, 0xB0, 0x8F, + 0x8D, 0x55, 0xFF, 0xD2, 0xC1, 0x87, 0xB0, 0x8F, 0x8D, 0x55, 0xFF, 0xC4, 0x00, 0x18, 0x10, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x31, 0x71, 0xF0, 0xF1, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x21, 0xAB, 0x35, + 0x36, 0x7F, 0xFF, 0xD0, 0xAB, 0x35, 0x36, 0x7F, 0xFF, 0xD1, 0xAB, 0x35, 0x36, 0x7F, 0xFF, 0xD2, + 0xAB, 0x35, 0x36, 0x7F, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x10, 0xFF, 0x00, + 0xFF, 0xD0, 0xFF, 0x00, 0xFF, 0xD1, 0xFF, 0x00, 0xFF, 0xD2, 0xFF, 0x00, 0xFF, 0xC4, 0x00, 0x18, + 0x10, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x11, 0xF0, 0x21, 0x51, 0x71, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x10, + 0x7A, 0x8E, 0x95, 0x13, 0xFF, 0xD0, 0x7A, 0x8E, 0x95, 0x13, 0xFF, 0xD1, 0x7A, 0x8E, 0x95, 0x13, + 0xFF, 0xD2, 0x7A, 0x8E, 0x95, 0x13, 0xFF, 0xD9, +} + +@(private) +JPEG_T_BITS12 := [154]byte{ + 0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFF, 0xC0, 0x00, 0x0B, 0x0C, 0x00, 0x08, 0x00, 0x08, + 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFE, 0xA0, 0x1F, 0xFF, 0xD9, +} + +// The bars image above encoded four ways: baseline and progressive, each with +// and without restart markers. libjpeg-turbo at quality 90 over 64x64 pixels of +// eight-row bars, which is what a page of text costs an encoder -- a handful of +// bits per MCU. `-restart 3` and `-restart 2` are MCU rows, so the DRI segments +// read 24 and 16. + +@(private) +JPEG_T_LOWENT := [392]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x40, + 0x00, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFD, 0x10, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0x2B, + 0xF0, 0x12, 0x8A, 0x28, 0xA2, 0x8A, 0x28, 0xAF, 0xDF, 0xBA, 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0xBF, + 0x01, 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0xFD, 0xFB, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0x2B, 0xF0, + 0x12, 0x8A, 0x28, 0xA2, 0x8A, 0x28, 0xAF, 0xDF, 0xBA, 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0xBF, 0x01, + 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0xFF, 0xD9, +} + +@(private) +JPEG_T_LOWENT_RST := [403]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x40, + 0x00, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDD, + 0x00, 0x04, 0x00, 0x18, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFD, 0x10, + 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0x2B, 0xF0, 0x12, 0x8A, 0x28, 0xA2, 0x8A, 0x28, 0xAF, 0xDF, 0xBA, + 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0xBF, 0xFF, 0xD0, 0xF8, 0x52, 0x8A, 0x28, 0xA2, 0x8A, 0x28, 0xAF, + 0xDF, 0xBA, 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0xBF, 0x01, 0x28, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0xFF, + 0xD1, 0xFD, 0x10, 0xA2, 0x8A, 0x28, 0xA2, 0x8A, 0x2B, 0xF0, 0x12, 0x8A, 0x28, 0xA2, 0x8A, 0x28, + 0xAF, 0xFF, 0xD9, +} + +@(private) +JPEG_T_LOWENTP := [308]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC2, 0x00, 0x0B, 0x08, 0x00, 0x40, + 0x00, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xDA, 0x00, + 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0xA2, 0x00, 0x40, 0x20, 0x2F, 0xE0, 0x10, 0x08, 0x0B, + 0xF8, 0x04, 0x02, 0x02, 0xFE, 0x01, 0x00, 0x80, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x01, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, + 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x01, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x21, 0x01, 0xFF, 0xDA, 0x00, 0x08, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, + 0x10, 0x01, 0xFF, 0xD9, +} + +@(private) +JPEG_T_LOWENTP_RST := [365]byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0A, 0x07, + 0x07, 0x06, 0x08, 0x0C, 0x0A, 0x0C, 0x0C, 0x0B, 0x0A, 0x0B, 0x0B, 0x0D, 0x0E, 0x12, 0x10, 0x0D, + 0x0E, 0x11, 0x0E, 0x0B, 0x0B, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0C, 0x0F, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xFF, 0xC2, 0x00, 0x0B, 0x08, 0x00, 0x40, + 0x00, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xDD, 0x00, + 0x04, 0x00, 0x10, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0xA2, 0x00, 0x40, + 0x20, 0x3F, 0xFF, 0xD0, 0xA2, 0x00, 0x40, 0x20, 0x3F, 0xFF, 0xD1, 0xA2, 0x00, 0x40, 0x20, 0x3F, + 0xFF, 0xD2, 0xA2, 0x00, 0x40, 0x20, 0x3F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0xDA, 0x00, + 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x07, 0xFF, 0xD0, 0x07, 0xFF, 0xD1, 0x07, 0xFF, 0xD2, + 0x07, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, + 0x02, 0x07, 0xFF, 0xD0, 0x07, 0xFF, 0xD1, 0x07, 0xFF, 0xD2, 0x07, 0xFF, 0xC4, 0x00, 0x14, 0x10, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, 0x21, 0x07, 0xFF, 0xD0, 0x07, 0xFF, + 0xD1, 0x07, 0xFF, 0xD2, 0x07, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, + 0xFF, 0x00, 0xFF, 0xD0, 0x00, 0xFF, 0x00, 0xFF, 0xD1, 0x00, 0xFF, 0x00, 0xFF, 0xD2, 0x00, 0xFF, + 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3F, + 0x10, 0x07, 0xFF, 0xD0, 0x07, 0xFF, 0xD1, 0x07, 0xFF, 0xD2, 0x07, 0xFF, 0xD9, +} + diff --git a/filter_flate.odin b/filter_flate.odin new file mode 100644 index 0000000..0094e8d --- /dev/null +++ b/filter_flate.odin @@ -0,0 +1,679 @@ +package pdf + +// FlateDecode -- RFC 1950 (zlib wrapper) and RFC 1951 (DEFLATE), ISO 32000-2 +// 7.4.4. +// +// core:compress/zlib is a conforming decoder, which is exactly why it is not +// used here: it rejects a missing zlib header, a wrong Adler-32, a truncated +// stream and trailing garbage, and all four occur in PDFs that every shipping +// viewer opens. This decoder is deliberately permissive -- it returns whatever +// it managed to inflate and only reports failure when it got nothing at all. +// +// The Huffman decoder is table-driven: a flat 512-entry first-level table +// resolves every code of 9 bits or fewer in one load, and the rare longer +// codes fall back to a canonical bit-at-a-time walk. Every content stream in +// every document goes through here, so the fast path matters. + +// A first level of 9 bits covers the whole fixed-literal alphabet and, in +// practice, well over 95% of dynamic codes. +@(private) FLATE_FAST_BITS :: 9 +@(private) FLATE_FAST_SIZE :: 1 << FLATE_FAST_BITS +@(private) FLATE_FAST_MASK :: FLATE_FAST_SIZE - 1 +@(private) FLATE_MAX_BITS :: 15 +@(private) FLATE_MAX_SYMBOLS :: 288 + +// DEFLATE cannot expand its input by more than about 1032:1. The margin above +// that absorbs multi-block streams and the zlib wrapper; the floor keeps tiny +// streams (a two-token content stream, an empty stream) workable, and the +// ceiling is the backstop against a decompression bomb. +@(private) FLATE_MAX_RATIO :: 1200 +@(private) FLATE_MIN_OUTPUT :: 1 << 16 +@(private) FLATE_MAX_OUTPUT :: 1 << 30 + +// How far into the stream a zlib header is still worth looking for. Producers +// that leak an extra EOL or a stray token before the stream data are common; +// producers that leak a kilobyte are not. +@(private) FLATE_JUNK_SCAN :: 64 + +// flate_decode inflates a FlateDecode stream. +// +// The result is allocated with `allocator` and belongs to the caller -- and so +// do the attempts that lose. Damaged data is read several ways below and only +// the best reading survives; each rejected attempt is freed, which needs an +// `allocator` where freeing means something. Give it scratch, never a Document +// arena: an arena's `delete` is a silent no-op, so every attempt would sit +// there until `close`. `apply_filter` in filters.odin is where that is +// arranged, and the comment there is the long version. +// +// A truncated or otherwise damaged stream still returns .None together with +// everything that could be recovered, so callers should compare the length +// against what they expected rather than trusting the error alone; .None with +// a short result is the signal to record a .Bad_Filter_Data repair. +// .Bad_Flate_Data means not one byte was recoverable. +flate_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) { + if len(data) == 0 { + return nil, .Bad_Flate_Data + } + + // Leading EOL between "stream" and the data proper is required by the + // spec but frequently mis-counted by the /Length, so it can survive into + // the raw bytes. + start := 0 + for start < len(data) && flate_is_space(data[start]) { + start += 1 + } + if start >= len(data) { + return nil, .Bad_Flate_Data + } + + // A zlib header is strong evidence: CM, CINFO and the FCHECK modulus all + // have to agree. When one sits at the front of the data, the bytes behind + // it are the stream, and guessing at other offsets would only invent + // output for a file that is merely damaged. + limit := flate_output_limit(len(data)) + header := -1 + if hlen, ok := flate_header_len(data[start:]); ok { + header = start + hlen + got, done := flate_inflate(data[header:], limit, allocator) + if done || len(got) > 0 { + return flate_finish(&got, done) + } + delete(got) + } + + // Otherwise there is nothing to do but guess, in descending order of + // evidence: a header a little way in (a producer that leaked a keyword or + // an extra EOL before the stream data), raw DEFLATE with no wrapper at + // all, Acrobat's long-standing bug of dropping the first header byte, and + // a header that is present but corrupt. + scan := -1 + if header < 0 { + scan_end := min(start + FLATE_JUNK_SCAN, len(data) - 1) + for i in start + 1 ..< scan_end { + if n, found := flate_header_len(data[i:]); found { + scan = i + n + break + } + } + } + cands := [5]int{scan, start, start + 1, start + 2, 0} + // If a header did validate and still yielded nothing, only a guess that + // runs all the way to a final block is worth more than admitting defeat; + // a partial decode from a guessed offset is noise dressed up as data. + need_done := header >= 0 + + best: [dynamic]byte + best_done := false + best_rank := -1 + + for off, ci in cands { + if off < 0 || off >= len(data) || off == header { + continue + } + dup := false + for j in 0 ..< ci { + if cands[j] == off { + dup = true + } + } + if dup { + continue + } + got, done := flate_inflate(data[off:], limit, allocator) + rank := flate_rank(done, len(got)) + // Earlier candidates are the more likely reading of the bytes, so a + // later one has to be strictly better to displace them. + if (!need_done || done) && (rank > best_rank || (rank == best_rank && len(got) > len(best))) { + if best_rank >= 0 { + delete(best) + } + best = got + best_done = done + best_rank = rank + } else { + delete(got) + } + if best_rank == 3 { + break + } + } + + if best_rank < 0 { + return nil, .Bad_Flate_Data + } + return flate_finish(&best, best_done) +} + +// flate_finish hands a completed attempt to the caller, trimming the growth +// slack off the buffer. An empty result is only an error when the stream never +// reached its end -- an empty stream really does inflate to nothing. +@(private) +flate_finish :: proc(buf: ^[dynamic]byte, done: bool) -> ([]byte, Error) { + if len(buf) == 0 { + delete(buf^) + return nil, done ? .None : .Bad_Flate_Data + } + shrink(buf) + return buf[:], .None +} + +// flate_rank orders two attempts at the same bytes: a stream that finished and +// produced output beats one that only produced output, which beats one that +// finished empty, which beats nothing at all. +@(private) +flate_rank :: proc(done: bool, n: int) -> int { + switch { + case done && n > 0: return 3 + case n > 0: return 2 + case done: return 1 + } + return 0 +} + +@(private) +flate_is_space :: proc(c: byte) -> bool { + return c == 0x00 || c == 0x09 || c == 0x0a || c == 0x0c || c == 0x0d || c == 0x20 +} + +// flate_header_len recognises an RFC 1950 header and reports how many bytes it +// occupies (2, or 6 with a preset dictionary identifier). +@(private) +flate_header_len :: proc(d: []byte) -> (n: int, ok: bool) { + if len(d) < 2 { + return 0, false + } + cmf, flg := d[0], d[1] + if cmf & 0x0f != 8 { + return 0, false // CM must be DEFLATE + } + if cmf >> 4 > 7 { + return 0, false // CINFO above 7 means a window larger than 32K + } + if (u16(cmf) << 8 | u16(flg)) % 31 != 0 { + return 0, false // FCHECK + } + if flg & 0x20 != 0 { + if len(d) < 6 { + return 0, false + } + return 6, true + } + return 2, true +} + +@(private) +flate_output_limit :: proc(input_size: int) -> int { + if input_size >= FLATE_MAX_OUTPUT / FLATE_MAX_RATIO { + return FLATE_MAX_OUTPUT // guard the multiply, not just its result + } + return clamp(input_size * FLATE_MAX_RATIO, FLATE_MIN_OUTPUT, FLATE_MAX_OUTPUT) +} + +// --------------------------------------------------------------------------- +// bit reader +// --------------------------------------------------------------------------- + +// DEFLATE packs bits least-significant-first within each byte, so the buffer +// fills from the low end and is consumed by shifting right. +@(private) +Flate_Bits :: struct { + data: []byte, + pos: int, + buf: u64, + cnt: u32, +} + +@(private) +flate_refill :: proc(br: ^Flate_Bits) { + for br.cnt <= 56 { + if br.pos >= len(br.data) { + break + } + br.buf |= u64(br.data[br.pos]) << br.cnt + br.pos += 1 + br.cnt += 8 + } +} + +// flate_get returns n bits, or ok=false when the input ran out. n <= 32. +@(private) +flate_get :: proc(br: ^Flate_Bits, n: u32) -> (v: u32, ok: bool) { + if n == 0 { + return 0, true + } + if br.cnt < n { + flate_refill(br) + if br.cnt < n { + return 0, false + } + } + v = u32(br.buf & ((u64(1) << n) - 1)) + br.buf >>= n + br.cnt -= n + return v, true +} + +// flate_align drops the partial byte and pushes whole buffered bytes back, so +// a stored block can be copied straight out of the input. +@(private) +flate_align :: proc(br: ^Flate_Bits) { + whole := int(br.cnt / 8) + br.pos -= whole + br.buf = 0 + br.cnt = 0 +} + +// --------------------------------------------------------------------------- +// Huffman +// --------------------------------------------------------------------------- + +// `fast` holds sym<<4 | len for every code of FLATE_FAST_BITS or fewer, keyed +// by the next 9 stream bits; 0 means "not a short code" and sends the caller +// to the canonical walk over `counts`/`symbols`. Zero can never be a real +// entry because a code length of 0 is not assigned. +@(private) +Flate_Huffman :: struct { + fast: [FLATE_FAST_SIZE]u16, + counts: [FLATE_MAX_BITS + 1]u16, + symbols: [FLATE_MAX_SYMBOLS]u16, +} + +@(private) +flate_reverse :: proc(code: u32, nbits: u32) -> u32 { + v := code + r := u32(0) + for _ in 0 ..< nbits { + r = (r << 1) | (v & 1) + v >>= 1 + } + return r +} + +// flate_build fills a decoder from a code-length vector. An incomplete code is +// accepted -- a single-entry distance tree is legal and shows up in real +// streams -- but an over-subscribed one is not, because there is no sane +// interpretation of it. +@(private) +flate_build :: proc(h: ^Flate_Huffman, lengths: []u8) -> bool { + h.fast = {} + h.counts = {} + if len(lengths) > FLATE_MAX_SYMBOLS { + return false + } + + used := 0 + for l in lengths { + if l > FLATE_MAX_BITS { + return false + } + if l != 0 { + h.counts[l] += 1 + used += 1 + } + } + if used == 0 { + return true // no codes: any decode against it fails, which is correct + } + + left := 1 + for l in 1 ..= FLATE_MAX_BITS { + left <<= 1 + left -= int(h.counts[l]) + if left < 0 { + return false + } + } + + offsets: [FLATE_MAX_BITS + 1]u16 + next: [FLATE_MAX_BITS + 1]u32 + off := u16(0) + code := u32(0) + for l in 1 ..= FLATE_MAX_BITS { + offsets[l] = off + off += h.counts[l] + code = (code + u32(h.counts[l - 1])) << 1 + next[l] = code + } + + for sym in 0 ..< len(lengths) { + l := u32(lengths[sym]) + if l == 0 { + continue + } + h.symbols[offsets[l]] = u16(sym) + offsets[l] += 1 + cv := next[l] + next[l] += 1 + if l <= FLATE_FAST_BITS { + entry := u16(sym) << 4 | u16(l) + step := u32(1) << l + for i := flate_reverse(cv, l); i < FLATE_FAST_SIZE; i += step { + h.fast[i] = entry + } + } + } + return true +} + +// flate_slow walks the canonical code one bit at a time. Only codes longer +// than FLATE_FAST_BITS reach it. +@(private) +flate_slow :: proc(br: ^Flate_Bits, h: ^Flate_Huffman) -> (sym: u16, ok: bool) { + code := u32(0) + first := u32(0) + index := u32(0) + for l in 1 ..= FLATE_MAX_BITS { + b := flate_get(br, 1) or_return + code |= b + count := u32(h.counts[l]) + if code - first < count { + at := index + (code - first) + if at >= FLATE_MAX_SYMBOLS { + return 0, false + } + return h.symbols[at], true + } + index += count + first = (first + count) << 1 + code <<= 1 + } + return 0, false +} + +@(private) +flate_sym :: proc(br: ^Flate_Bits, h: ^Flate_Huffman) -> (sym: u16, ok: bool) { + if br.cnt < FLATE_FAST_BITS { + flate_refill(br) + } + // Near end of input the peek is zero-padded, but a hit is still sound: + // the code is a prefix code, so if the first `l` bits form a complete code + // that is the code, whatever follows. + e := h.fast[u32(br.buf) & FLATE_FAST_MASK] + if e != 0 { + l := u32(e & 15) + if l > br.cnt { + return 0, false + } + br.buf >>= l + br.cnt -= l + return e >> 4, true + } + return flate_slow(br, h) +} + +// --------------------------------------------------------------------------- +// inflate +// --------------------------------------------------------------------------- + +// RFC 1951 3.2.5. +@(private) FLATE_LEN_BASE := [29]u16{ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, + 67, 83, 99, 115, 131, 163, 195, 227, 258, +} +@(private) FLATE_LEN_EXTRA := [29]u8{ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 5, 5, 5, 5, 0, +} +@(private) FLATE_DIST_BASE := [30]u16{ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, +} +@(private) FLATE_DIST_EXTRA := [30]u8{ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, +} +// RFC 1951 3.2.7: code-length codes are transmitted in this order. +@(private) FLATE_CLEN_ORDER := [19]u8{ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15, +} + +@(private) +Flate_State :: struct { + br: Flate_Bits, + lit: Flate_Huffman, + dist: Flate_Huffman, + clen: Flate_Huffman, +} + +// flate_inflate decodes a raw DEFLATE stream. `done` is true only when a final +// block was reached and decoded; otherwise `out` holds everything recovered +// before the stream went bad, ran out, or hit `limit`. +@(private) +flate_inflate :: proc(src: []byte, limit: int, allocator := context.allocator) -> (out: [dynamic]byte, done: bool) { + // PDF streams typically expand three- or four-fold; guessing that up front + // saves most of the regrowth without betting a large allocation on an + // offset that may turn out to be nonsense. + initial := min(max(1024, len(src) * 4), 1 << 20) + initial = min(initial, limit) + out = make([dynamic]byte, 0, max(initial, 16), allocator) + if len(src) == 0 { + return out, false + } + + // Roughly 5 KiB of tables. On the stack rather than the caller's + // allocator, because the caller's allocator is usually a Document arena + // that is never freed until the document closes, and a long document + // decodes thousands of streams. + state: Flate_State + st := &state + st.br.data = src + + // Every block header consumes at least three bits, so this can only be + // reached by input that is already exhausted; it is a belt-and-braces + // bound, not the real terminator. + max_blocks := len(src) * 8 / 3 + 8 + for _ in 0 ..< max_blocks { + final := flate_get(&st.br, 1) or_break + btype := flate_get(&st.br, 2) or_break + switch btype { + case 0: + if !flate_stored(st, &out, limit) { + return out, false + } + case 1: + if !flate_fixed_tables(st) { + return out, false + } + if !flate_block(st, &out, limit) { + return out, false + } + case 2: + if !flate_dynamic_tables(st) { + return out, false + } + if !flate_block(st, &out, limit) { + return out, false + } + case: + return out, false // btype 3 is reserved + } + if final == 1 { + // Anything after the final block is not our business; PDF streams + // routinely carry padding, an EOL, or the next object's bytes. + return out, true + } + } + return out, false +} + +@(private) +flate_grow :: proc(out: ^[dynamic]byte, extra: int, limit: int) -> bool { + need := len(out) + extra + if need > limit { + return false + } + if need <= cap(out) { + return true + } + new_cap := cap(out) * 2 + if new_cap < need { + new_cap = need + } + if new_cap > limit { + new_cap = limit + } + if reserve(out, new_cap) != nil { + return false + } + return true +} + +@(private) +flate_stored :: proc(st: ^Flate_State, out: ^[dynamic]byte, limit: int) -> bool { + br := &st.br + flate_align(br) + if br.pos < 0 || br.pos + 4 > len(br.data) { + return false + } + blen := int(br.data[br.pos]) | int(br.data[br.pos + 1]) << 8 + bnlen := int(br.data[br.pos + 2]) | int(br.data[br.pos + 3]) << 8 + br.pos += 4 + // LEN/~LEN disagreeing means we are not looking at a block header at all, + // usually because a fallback offset guessed wrong. Trusting LEN anyway + // would copy megabytes of garbage. + if blen != (~bnlen & 0xffff) { + return false + } + avail := min(blen, len(br.data) - br.pos) + if avail > 0 { + if !flate_grow(out, avail, limit) { + avail = min(avail, limit - len(out)) + if avail > 0 && !flate_grow(out, avail, limit) { + avail = 0 + } + } + if avail > 0 { + n := len(out) + non_zero_resize(out, n + avail) + copy(out[n:], br.data[br.pos:br.pos + avail]) + br.pos += avail + } + } + return avail == blen +} + +@(private) +flate_fixed_tables :: proc(st: ^Flate_State) -> bool { + // RFC 1951 3.2.6. + lengths: [FLATE_MAX_SYMBOLS]u8 + for i in 0 ..< 144 { lengths[i] = 8 } + for i in 144 ..< 256 { lengths[i] = 9 } + for i in 256 ..< 280 { lengths[i] = 7 } + for i in 280 ..< 288 { lengths[i] = 8 } + if !flate_build(&st.lit, lengths[:]) { + return false + } + dlengths: [30]u8 + for i in 0 ..< 30 { dlengths[i] = 5 } + return flate_build(&st.dist, dlengths[:]) +} + +@(private) +flate_dynamic_tables :: proc(st: ^Flate_State) -> bool { + br := &st.br + hlit := int(flate_get(br, 5) or_return) + 257 + hdist := int(flate_get(br, 5) or_return) + 1 + hclen := int(flate_get(br, 4) or_return) + 4 + + clen: [19]u8 + for i in 0 ..< hclen { + clen[FLATE_CLEN_ORDER[i]] = u8(flate_get(br, 3) or_return) + } + if !flate_build(&st.clen, clen[:]) { + return false + } + + total := hlit + hdist // at most 288 + 32 + lengths: [FLATE_MAX_SYMBOLS + 32]u8 + i := 0 + for i < total { + sym := flate_sym(br, &st.clen) or_return + switch { + case sym < 16: + lengths[i] = u8(sym) + i += 1 + case sym == 16: + if i == 0 { + return false // nothing to repeat + } + prev := lengths[i - 1] + rep := 3 + int(flate_get(br, 2) or_return) + rep = min(rep, total - i) // a run past the end is corruption; clamp + for _ in 0 ..< rep { + lengths[i] = prev + i += 1 + } + case sym == 17: + rep := 3 + int(flate_get(br, 3) or_return) + i = min(i + rep, total) + case sym == 18: + rep := 11 + int(flate_get(br, 7) or_return) + i = min(i + rep, total) + case: + return false + } + } + + if !flate_build(&st.lit, lengths[:hlit]) { + return false + } + return flate_build(&st.dist, lengths[hlit:total]) +} + +@(private) +flate_block :: proc(st: ^Flate_State, out: ^[dynamic]byte, limit: int) -> bool { + br := &st.br + // Each iteration consumes at least one bit, so the input already bounds + // this loop; the counter only makes that guarantee local. + fuel := len(br.data) * 8 + 64 + for _ in 0 ..< fuel { + sym := flate_sym(br, &st.lit) or_return + switch { + case sym < 256: + if !flate_grow(out, 1, limit) { + return false + } + append(out, u8(sym)) + case sym == 256: + return true + case: + si := int(sym) - 257 + if si >= 29 { + return false // 286 and 287 are never assigned a meaning + } + length := int(FLATE_LEN_BASE[si]) + int(flate_get(br, u32(FLATE_LEN_EXTRA[si])) or_return) + dsym := flate_sym(br, &st.dist) or_return + if int(dsym) >= 30 { + return false + } + dist := int(FLATE_DIST_BASE[dsym]) + int(flate_get(br, u32(FLATE_DIST_EXTRA[dsym])) or_return) + if dist <= 0 || dist > len(out) { + return false // reference before the start of the output + } + if !flate_copy(out, dist, length, limit) { + return false + } + } + } + return false +} + +// flate_copy performs the LZ77 back-reference. The copy must be byte at a +// time: runs overlap by design (distance 1, length 258 is a 258-byte run). +@(private) +flate_copy :: proc(out: ^[dynamic]byte, dist: int, length: int, limit: int) -> bool { + n := len(out) + take := min(length, limit - n) + if take <= 0 { + return false + } + if !flate_grow(out, take, limit) { + return false + } + non_zero_resize(out, n + take) + src := n - dist + for i in 0 ..< take { + out[n + i] = out[src + i] + } + return take == length +} diff --git a/filter_flate_test.odin b/filter_flate_test.odin new file mode 100644 index 0000000..4a3f8e1 --- /dev/null +++ b/filter_flate_test.odin @@ -0,0 +1,688 @@ +package pdf + +import "core:bytes" +import "core:compress/zlib" +import "core:mem" +import "core:slice" +import "core:testing" + +// Test vectors are produced by the reference implementation (zlib 1.2.x via +// CPython) so the expected output is never this decoder's own opinion, and the +// well-formed ones are additionally checked against core:compress/zlib at run +// time. The malformed ones are hand-assembled bit by bit. + +@(private="file") +V_STORED_PLAIN :: "Hello, PDF world!" +@(private="file") +V_FIXED_PLAIN :: "abcabcabcabcabcabcabcabc" + +// zlib.compress("") -- a final fixed block whose only symbol is end-of-block. +@(private="file") +v_empty := [?]byte{ + 0x78, 0xda, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, +} + +// level 0: a single stored block carrying V_STORED_PLAIN verbatim. +@(private="file") +v_stored := [?]byte{ + 0x78, 0x01, 0x01, 0x11, 0x00, 0xee, 0xff, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x50, 0x44, + 0x46, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x31, 0xf6, 0x05, 0x84, +} + +// fixed-Huffman block with a length/distance pair ("abc" repeated 8 times). +@(private="file") +v_fixed := [?]byte{ + 0x78, 0xda, 0x4b, 0x4c, 0x4a, 0x4e, 0xc4, 0x86, 0x00, 0x72, 0xe0, 0x09, 0x31, +} + +// dynamic-Huffman block: 201 bytes of content-stream-like text. +@(private="file") +v_dynamic := [?]byte{ + 0x78, 0xda, 0x2d, 0x8e, 0xbb, 0x15, 0xc4, 0x30, 0x08, 0x04, 0xf3, 0xab, 0x62, 0x4b, 0x10, 0x7f, + 0x54, 0x87, 0xdb, 0x38, 0xf7, 0x9f, 0x7a, 0x85, 0x1d, 0x10, 0xcc, 0x3c, 0x09, 0x66, 0x61, 0xe1, + 0xe6, 0x2c, 0xfc, 0x71, 0xfd, 0x04, 0x46, 0x2a, 0x88, 0x0c, 0x2a, 0x92, 0x28, 0x0e, 0xd5, 0x61, + 0xc3, 0x26, 0x2b, 0x5f, 0xd9, 0xb0, 0x43, 0xf4, 0x88, 0x86, 0xfb, 0x88, 0x80, 0x04, 0x85, 0x05, + 0x22, 0x46, 0x24, 0xa4, 0x29, 0x9c, 0x9b, 0x72, 0x44, 0x9d, 0xef, 0x14, 0x1b, 0x55, 0x23, 0x1a, + 0xea, 0x14, 0x91, 0xe8, 0x1e, 0xb1, 0xa1, 0x45, 0x91, 0x3c, 0xb6, 0xdf, 0xa8, 0x05, 0x3b, 0x91, + 0xb5, 0xd8, 0xf5, 0x75, 0x4e, 0xc2, 0x7d, 0x76, 0x88, 0xbe, 0xad, 0x0f, 0xb2, 0x55, 0x27, 0xe5, +} + +// a dynamic block with power-of-two symbol frequencies, so its literal tree +// is a 10-deep caterpillar and decoding it has to leave the 9-bit table. +// 1024 bytes out. +@(private="file") +v_slow := [?]byte{ + 0x78, 0x01, 0x05, 0xc1, 0x81, 0x61, 0x04, 0x45, 0x10, 0x04, 0xb1, 0xd8, 0x00, 0xff, 0xed, 0x74, + 0x29, 0xff, 0x7c, 0x90, 0xfa, 0x5d, 0x0a, 0x0b, 0x78, 0x8b, 0xcd, 0x55, 0xb9, 0xca, 0x8b, 0xed, + 0x04, 0x78, 0x52, 0xa1, 0x6b, 0x9c, 0x19, 0xb3, 0x18, 0xe7, 0x86, 0x46, 0xde, 0xbd, 0x0d, 0x00, + 0x84, 0xfe, 0x70, 0xe4, 0xaa, 0x61, 0x79, 0xdb, 0xa9, 0x29, 0x87, 0xb0, 0xe1, 0x86, 0x2a, 0xa5, + 0xfe, 0xe0, 0x8f, 0x51, 0x76, 0x05, 0x95, 0xac, 0xf1, 0x79, 0xd1, 0x20, 0xc4, 0xac, 0x3a, 0x21, + 0x50, 0xfc, 0x3c, 0x0f, 0x77, 0xab, 0xfb, 0x6c, 0x0b, 0xfb, 0x48, 0x7a, 0x93, 0xdc, 0x7c, 0x4c, + 0x77, 0xa7, 0x98, 0x19, 0xe7, 0x77, 0xdf, 0x61, 0xb5, 0xf9, 0x8c, 0xc0, 0x0b, 0xe5, 0xba, 0x7b, + 0x56, 0x6c, 0x32, 0xee, 0x4e, 0xda, 0x91, 0x90, 0xb8, 0x6d, 0x81, 0x71, 0x27, 0xaf, 0xcb, 0x18, + 0x3f, 0xb1, 0x75, 0xea, 0xc1, 0x1b, 0x58, 0x9b, 0x08, 0x3b, 0x76, 0xca, 0xd0, 0x89, 0x16, 0x4d, + 0x7b, 0x9c, 0x46, 0x8c, 0x11, 0x1e, 0xb6, 0x1a, 0x77, 0xaf, 0xe8, 0x73, 0xed, 0xb4, 0x8a, 0x20, + 0x50, 0xe5, 0x37, 0xf4, 0x1f, 0xf4, 0x29, 0xf2, 0x51, 0x5a, 0x61, 0xa0, 0x3d, 0xba, 0xcf, 0x52, + 0x9f, 0x12, 0xab, 0x50, 0x01, 0x00, 0x08, 0x85, 0x16, 0xe1, 0x80, 0x2b, 0xfe, 0x09, 0x90, 0x9e, + 0xc5, 0xed, 0x59, 0x0f, 0x43, 0xec, 0x0b, 0x81, 0x85, 0x0a, 0xa3, 0xac, 0xa2, 0x50, 0x44, 0x3f, + 0x23, 0x2a, 0xe4, 0xa1, 0x83, 0xba, 0xc9, 0x64, 0xaa, 0x22, 0xa6, 0xd5, 0x53, 0xbb, 0xc0, 0x56, + 0x9e, 0x27, 0xc1, 0x5e, 0xdc, 0xb3, 0x62, 0xce, 0xa4, 0x23, 0x8b, 0x76, 0xbd, 0x82, 0x23, 0x94, + 0x39, 0xe6, 0xf5, 0xa3, 0xe2, 0x4b, 0xbe, 0xde, 0x64, 0x29, 0x75, 0xe1, 0x3f, 0xd4, 0x35, 0xf5, + 0x95, 0x31, 0x58, 0xcd, 0x66, 0x7f, 0xfe, 0xdd, 0x62, 0xc7, 0xc2, 0x6a, 0x7e, 0x9c, 0xa6, 0x6e, + 0x9e, 0x91, 0x0f, 0x20, 0xfe, 0x07, 0xbe, 0x54, 0x28, 0x11, +} + +// 256 KiB of zeroes -- a ~946:1 expansion, close to what DEFLATE can do at +// all, and therefore the shape a decompression bomb takes. +@(private="file") +v_ratio := [?]byte{ + 0x78, 0xda, 0xed, 0xc1, 0x31, 0x01, 0x00, 0x00, 0x00, 0xc2, 0xa0, 0xf5, 0x4f, 0xed, 0x6d, 0x07, + 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, + 0x00, 0x00, 0x3c, 0x00, 0x01, +} + +// a bare fixed-Huffman block, no zlib wrapper at all: "Hi PDF". +@(private="file") +v_raw_fixed := [?]byte{ + 0xf3, 0xc8, 0x54, 0x08, 0x70, 0x71, 0x03, 0x00, +} + +// one literal, then a back-reference 7 bytes before a 1-byte output. The +// literal survives; the match cannot. +@(private="file") +v_bad_distance := [?]byte{ + 0x73, 0x04, 0x52, 0x00, +} + +// a stored block whose LEN and ones-complement NLEN disagree. +@(private="file") +v_stored_bad_nlen := [?]byte{ + 0x78, 0x01, 0x01, 0x05, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, +} + +// a stored block that promises 32 bytes and supplies 5. +@(private="file") +v_stored_short := [?]byte{ + 0x78, 0x01, 0x01, 0x20, 0x00, 0xdf, 0xff, 0x68, 0x65, 0x6c, 0x6c, 0x6f, +} + +// BTYPE 3 is reserved; nothing here can be decoded. +@(private="file") +v_btype3 := [?]byte{ + 0x07, 0x07, 0x07, 0x07, +} + +// a dynamic block whose code-length alphabet assigns four 1-bit codes, which +// no prefix code can satisfy. +@(private="file") +v_oversubscribed := [?]byte{ + 0x05, 0x00, 0x92, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +// raw DEFLATE flushed but never finished, so no block ever sets BFINAL -- +// what a writer that crashed mid-stream leaves behind. 68 bytes out. +@(private="file") +v_no_final := [?]byte{ + 0xf2, 0x48, 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x08, 0x70, 0x71, 0x53, 0x28, 0xcf, 0x2f, 0xca, 0x49, + 0x51, 0xf4, 0x20, 0x5d, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// zlib_reference inflates with core:compress/zlib, which is strict and +// therefore only usable on the well-formed vectors. Caller frees. +@(private="file") +zlib_reference :: proc(t: ^testing.T, src: []byte) -> []byte { + buf: bytes.Buffer + err := zlib.inflate(src, &buf) + if err != nil { + testing.expectf(t, false, "core:compress/zlib rejected a vector it should accept: %v", err) + bytes.buffer_destroy(&buf) + return nil + } + return bytes.buffer_to_bytes(&buf) +} + +@(private="file") +expect_decodes_to :: proc(t: ^testing.T, src: []byte, want: string, label: string) { + got, err := flate_decode(src) + defer delete(got) + testing.expectf(t, err == .None, "%s: %v", label, error_string(err)) + testing.expectf(t, string(got) == want, "%s: got %q, want %q", label, string(got), want) +} + +// --------------------------------------------------------------------------- +// the four block shapes +// --------------------------------------------------------------------------- + +@(test) +test_flate_empty_stream :: proc(t: ^testing.T) { + // An empty stream is not damage: it inflates to nothing and reports .None. + out, err := flate_decode(v_empty[:]) + defer delete(out) + testing.expectf(t, err == .None, "empty stream: %v", error_string(err)) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_flate_stored_block :: proc(t: ^testing.T) { + expect_decodes_to(t, v_stored[:], V_STORED_PLAIN, "stored block") +} + +@(test) +test_flate_fixed_huffman :: proc(t: ^testing.T) { + expect_decodes_to(t, v_fixed[:], V_FIXED_PLAIN, "fixed Huffman") +} + +@(test) +test_flate_raw_fixed_huffman :: proc(t: ^testing.T) { + expect_decodes_to(t, v_raw_fixed[:], "Hi PDF", "bare fixed-Huffman block") +} + +@(test) +test_flate_dynamic_huffman :: proc(t: ^testing.T) { + want := zlib_reference(t, v_dynamic[:]) + defer delete(want) + got, err := flate_decode(v_dynamic[:]) + defer delete(got) + testing.expectf(t, err == .None, "dynamic Huffman: %v", error_string(err)) + testing.expect_value(t, len(got), 201) + testing.expect(t, slice.equal(got, want), "dynamic Huffman output disagrees with core:compress/zlib") +} + +@(test) +test_flate_long_codes_take_the_slow_path :: proc(t: ^testing.T) { + // v_slow's literal tree is 10 deep, so at least one symbol per repetition + // misses the 9-bit first-level table. + want := zlib_reference(t, v_slow[:]) + defer delete(want) + got, err := flate_decode(v_slow[:]) + defer delete(got) + testing.expectf(t, err == .None, "long codes: %v", error_string(err)) + testing.expect_value(t, len(got), 1024) + testing.expect(t, slice.equal(got, want), "long-code output disagrees with core:compress/zlib") +} + +@(test) +test_flate_agrees_with_zlib :: proc(t: ^testing.T) { + cases := [][]byte{v_empty[:], v_stored[:], v_fixed[:], v_dynamic[:], v_slow[:], v_ratio[:]} + for src, i in cases { + want := zlib_reference(t, src) + defer delete(want) + got, err := flate_decode(src) + defer delete(got) + testing.expectf(t, err == .None, "case %d: %v", i, error_string(err)) + testing.expectf(t, len(got) == len(want), "case %d: %d bytes, zlib says %d", i, len(got), len(want)) + testing.expectf(t, slice.equal(got, want), "case %d: bytes differ from core:compress/zlib", i) + } +} + +@(test) +test_flate_high_ratio_stream :: proc(t: ^testing.T) { + // ~946:1. A real stream may legitimately expand this much, so the bomb + // guard must not be tighter than what DEFLATE can actually produce. + out, err := flate_decode(v_ratio[:]) + defer delete(out) + testing.expectf(t, err == .None, "high ratio: %v", error_string(err)) + testing.expect_value(t, len(out), 256 * 1024) + all_zero := true + for b in out { + if b != 0 { + all_zero = false + break + } + } + testing.expect(t, all_zero, "high-ratio stream should decode to zeroes") +} + +// --------------------------------------------------------------------------- +// the wrapper is where producers go wrong +// --------------------------------------------------------------------------- + +@(test) +test_flate_raw_deflate_without_wrapper :: proc(t: ^testing.T) { + // Some producers write the deflate stream with no zlib header at all. + expect_decodes_to(t, v_fixed[2:], V_FIXED_PLAIN, "header stripped") +} + +@(test) +test_flate_corrupt_header :: proc(t: ^testing.T) { + src: [len(v_fixed)]byte + copy(src[:], v_fixed[:]) + src[0] = 0x00 // CM is no longer 8, so FCHECK cannot rescue it either + expect_decodes_to(t, src[:], V_FIXED_PLAIN, "corrupt CMF") + + copy(src[:], v_fixed[:]) + src[1] ~= 0x01 // FCHECK fails + expect_decodes_to(t, src[:], V_FIXED_PLAIN, "corrupt FLG") +} + +@(test) +test_flate_acrobat_off_by_one :: proc(t: ^testing.T) { + // A known Acrobat bug drops the first byte of the header. + expect_decodes_to(t, v_fixed[1:], V_FIXED_PLAIN, "one header byte lost") +} + +@(test) +test_flate_leading_whitespace :: proc(t: ^testing.T) { + src: [4 + len(v_dynamic)]byte + copy(src[:], []byte{0x0d, 0x0a, 0x20, 0x09}) + copy(src[4:], v_dynamic[:]) + want := zlib_reference(t, v_dynamic[:]) + defer delete(want) + got, err := flate_decode(src[:]) + defer delete(got) + testing.expectf(t, err == .None, "leading whitespace: %v", error_string(err)) + testing.expect(t, slice.equal(got, want), "leading whitespace changed the output") +} + +@(test) +test_flate_leading_junk :: proc(t: ^testing.T) { + // Not whitespace, so the header has to be found by scanning. + src: [7 + len(v_dynamic)]byte + copy(src[:], transmute([]byte)string("stream\n")) + copy(src[7:], v_dynamic[:]) + want := zlib_reference(t, v_dynamic[:]) + defer delete(want) + got, err := flate_decode(src[:]) + defer delete(got) + testing.expectf(t, err == .None, "leading junk: %v", error_string(err)) + testing.expect(t, slice.equal(got, want), "leading junk changed the output") +} + +@(test) +test_flate_trailing_garbage :: proc(t: ^testing.T) { + // Everything after the final block is somebody else's problem. + src: [len(v_fixed) + 24]byte + copy(src[:], v_fixed[:]) + copy(src[len(v_fixed):], transmute([]byte)string("\nendstream\nendobj\n%%EOF\n")) + expect_decodes_to(t, src[:], V_FIXED_PLAIN, "trailing garbage") +} + +// --------------------------------------------------------------------------- +// damage +// --------------------------------------------------------------------------- + +@(test) +test_flate_missing_final_block :: proc(t: ^testing.T) { + // No block ever sets BFINAL, so the decoder simply runs out of input; the + // bytes it already produced are still good. + out, err := flate_decode(v_no_final[:]) + defer delete(out) + testing.expectf(t, err == .None, "no final block: %v", error_string(err)) + want := "Hello, PDF world!Hello, PDF world!Hello, PDF world!Hello, PDF world!" + testing.expectf(t, string(out) == want, "no final block: got %q", string(out)) +} + +@(test) +test_flate_stored_block_truncated :: proc(t: ^testing.T) { + // LEN says 32, five bytes are present. Keep the five. + out, err := flate_decode(v_stored_short[:]) + defer delete(out) + testing.expectf(t, err == .None, "short stored block: %v", error_string(err)) + testing.expectf(t, string(out) == "hello", "short stored block: got %q", string(out)) +} + +@(test) +test_flate_stored_block_bad_nlen :: proc(t: ^testing.T) { + // LEN and ~LEN disagreeing means these are not really block-header bytes, + // so nothing is recoverable and nothing is invented. + out, err := flate_decode(v_stored_bad_nlen[:]) + defer delete(out) + testing.expectf(t, err == .Bad_Flate_Data, "bad NLEN: %v", error_string(err)) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_flate_distance_before_start :: proc(t: ^testing.T) { + // A match reaching behind the first output byte stops the block; the + // literal decoded before it is still returned. + out, err := flate_decode(v_bad_distance[:]) + defer delete(out) + testing.expectf(t, err == .None, "bad distance: %v", error_string(err)) + testing.expectf(t, string(out) == "A", "bad distance: got %q", string(out)) +} + +@(test) +test_flate_nothing_recoverable :: proc(t: ^testing.T) { + empty: []byte + cases := [][]byte{ + empty, + transmute([]byte)string(" \r\n\t"), + v_btype3[:], + } + for src, i in cases { + out, err := flate_decode(src) + defer delete(out) + testing.expectf(t, err == .Bad_Flate_Data, "case %d: %v", i, error_string(err)) + testing.expect_value(t, len(out), 0) + } +} + +@(test) +test_flate_every_truncation :: proc(t: ^testing.T) { + // Cutting a stream short must never crash, must never produce bytes the + // whole stream would not have produced, and must never lose ground as more + // input arrives. + streams := [][]byte{v_dynamic[:], v_slow[:], v_fixed[:], v_stored[:], v_ratio[:]} + for src in streams { + full, ferr := flate_decode(src) + testing.expect(t, ferr == .None, "full stream should decode") + prev := 0 + for n in 1 ..= len(src) { + got, _ := flate_decode(src[:n]) + testing.expectf(t, len(got) <= len(full), + "truncating to %d/%d produced %d bytes, more than the whole stream's %d", + n, len(src), len(got), len(full)) + if len(got) <= len(full) { + testing.expectf(t, slice.equal(got, full[:len(got)]), + "truncating to %d/%d produced bytes the whole stream does not", n, len(src)) + } + testing.expectf(t, len(got) >= prev, + "truncating to %d/%d produced %d bytes, fewer than %d bytes of input gave (%d)", + n, len(src), len(got), n - 1, prev) + prev = len(got) + delete(got) + } + testing.expect_value(t, prev, len(full)) + delete(full) + } +} + +@(test) +test_flate_bit_flips :: proc(t: ^testing.T) { + // Every single-bit corruption of a dynamic stream: no crash, no hang, no + // allocation the caller cannot free. + src: [len(v_dynamic)]byte + limit := flate_output_limit(len(v_dynamic)) + for i in 0 ..< len(v_dynamic) { + for bit in u32(0) ..< 8 { + copy(src[:], v_dynamic[:]) + src[i] ~= byte(1 << bit) + out, err := flate_decode(src[:]) + testing.expectf(t, len(out) <= limit, "bit flip %d/%d exceeded the output cap", i, bit) + testing.expect(t, err == .None || err == .Bad_Flate_Data, "unexpected error kind") + testing.expect(t, err != .Bad_Flate_Data || len(out) == 0, "an error must come with no output") + delete(out) + } + } +} + +@(test) +test_flate_malformed_never_crashes :: proc(t: ^testing.T) { + cases := [][]byte{ + v_oversubscribed[:], + v_btype3[:], + v_stored_bad_nlen[:], + v_stored_short[:], + v_bad_distance[:], + {0x78}, + {0x78, 0x9c}, + {0xff}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + } + for src, i in cases { + out, err := flate_decode(src) + testing.expectf(t, err == .None || err == .Bad_Flate_Data, "case %d: %v", i, error_string(err)) + delete(out) + } +} + +// --------------------------------------------------------------------------- +// the pieces, on their own +// --------------------------------------------------------------------------- + +@(test) +test_flate_output_limit :: proc(t: ^testing.T) { + // Small streams get a floor so that a legitimate tiny stream is never + // clipped; large ones are bounded by what DEFLATE can actually expand to; + // everything is bounded absolutely. + testing.expect_value(t, flate_output_limit(0), FLATE_MIN_OUTPUT) + testing.expect_value(t, flate_output_limit(1), FLATE_MIN_OUTPUT) + testing.expect_value(t, flate_output_limit(1024), 1024 * FLATE_MAX_RATIO) + testing.expect_value(t, flate_output_limit(1 << 30), FLATE_MAX_OUTPUT) + testing.expect_value(t, flate_output_limit(max(int) / 2), FLATE_MAX_OUTPUT) + testing.expect(t, FLATE_MAX_RATIO > 1032, + "DEFLATE can expand about 1032:1, so a tighter cap would reject valid streams") +} + +@(test) +test_flate_inflate_stops_at_the_limit :: proc(t: ^testing.T) { + // The bomb guard, exercised directly: 256 KiB of output, 100 bytes allowed. + out, done := flate_inflate(v_ratio[2:], 100) + defer delete(out) + testing.expect_value(t, len(out), 100) + testing.expect(t, !done, "a stream cut off by the output cap has not finished") +} + +@(test) +test_flate_header_len :: proc(t: ^testing.T) { + Case :: struct { + input: []byte, + n: int, + ok: bool, + why: string, + } + cases := []Case{ + {{0x78, 0x01}, 2, true, "level 0/1"}, + {{0x78, 0x9c}, 2, true, "default level"}, + {{0x78, 0xda}, 2, true, "level 9"}, + {{0x68, 0xde}, 2, true, "8K window"}, + {{0x78, 0x9d}, 0, false, "FCHECK is wrong"}, + {{0x77, 0x9c}, 0, false, "CM is not DEFLATE"}, + {{0x88, 0x9c}, 0, false, "CINFO asks for a 64K window"}, + {{0x78}, 0, false, "one byte is not a header"}, + {{}, 0, false, "no bytes at all"}, + {{0x78, 0xbb, 0x01, 0x02, 0x03, 0x04}, 6, true, "FDICT adds a 4-byte identifier"}, + {{0x78, 0xbb, 0x01}, 0, false, "FDICT set but the identifier is cut off"}, + } + for c in cases { + n, ok := flate_header_len(c.input) + testing.expectf(t, ok == c.ok && n == c.n, "%s: got (%d, %v), want (%d, %v)", c.why, n, ok, c.n, c.ok) + } +} + +@(test) +test_flate_build_rejects_oversubscribed :: proc(t: ^testing.T) { + h := new(Flate_Huffman) + defer free(h) + + // Four one-bit codes: no prefix code can hold them. + testing.expect(t, !flate_build(h, []u8{1, 1, 1, 1}), "over-subscribed code accepted") + // A length past 15 bits cannot be expressed. + testing.expect(t, !flate_build(h, []u8{16, 1}), "a 16-bit code length was accepted") + // Incomplete codes are legal and do occur: a distance alphabet with a + // single symbol is the usual example. + testing.expect(t, flate_build(h, []u8{1}), "single-symbol code rejected") + // An alphabet with nothing in it builds; decoding against it just fails. + testing.expect(t, flate_build(h, []u8{0, 0, 0}), "empty alphabet rejected") + testing.expect(t, flate_build(h, []u8{1, 1}), "complete two-symbol code rejected") +} + +@(test) +test_flate_build_fixed_tables :: proc(t: ^testing.T) { + st := new(Flate_State) + defer free(st) + testing.expect(t, flate_fixed_tables(st), "the fixed tables must always build") + + // RFC 1951 3.2.6: literal 0 is 00110000, so the stream bits are 0,0,0,0,1,1,0,0 + // and the 9-bit peek that selects it is 0b000110000 reversed = 0x00C. + st.br = {data = []byte{0x0c, 0x00}} + sym, ok := flate_sym(&st.br, &st.lit) + testing.expect(t, ok, "fixed literal 0 should decode") + testing.expect_value(t, sym, u16(0)) + + // End-of-block is seven zero bits. + st.br = {data = []byte{0x00, 0x00}} + sym2, ok2 := flate_sym(&st.br, &st.lit) + testing.expect(t, ok2, "end-of-block should decode") + testing.expect_value(t, sym2, u16(256)) +} + +@(test) +test_flate_reverse :: proc(t: ^testing.T) { + testing.expect_value(t, flate_reverse(0b1, 1), u32(0b1)) + testing.expect_value(t, flate_reverse(0b10, 2), u32(0b01)) + testing.expect_value(t, flate_reverse(0b00110000, 8), u32(0b00001100)) + testing.expect_value(t, flate_reverse(0, 9), u32(0)) + testing.expect_value(t, flate_reverse(0b111111111, 9), u32(0b111111111)) +} + +@(test) +test_flate_bit_reader_runs_dry :: proc(t: ^testing.T) { + br := Flate_Bits{data = []byte{0xff}} + v, ok := flate_get(&br, 8) + testing.expect(t, ok, "eight bits are available") + testing.expect_value(t, v, u32(0xff)) + _, ok2 := flate_get(&br, 1) + testing.expect(t, !ok2, "the ninth bit is not") + v3, ok3 := flate_get(&br, 0) + testing.expect(t, ok3 && v3 == 0, "asking for no bits always succeeds") +} +// three blocks in one stream: content, a sync-flush boundary, then the final +// block. "first half, second half." +@(private="file") +v_multi_block := [?]byte{ + 0x78, 0xda, 0x4a, 0xcb, 0x2c, 0x2a, 0x2e, 0x51, 0xc8, 0x48, 0xcc, 0x49, 0xd3, 0x51, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x2b, 0x4e, 0x4d, 0xce, 0xcf, 0x4b, 0x01, 0x73, 0xf4, 0x00, 0x6e, 0x7b, + 0x08, 0x95, +} + +// a fixed block using distance symbol 30, which RFC 1951 never assigns. +@(private="file") +v_bad_dist_code := [?]byte{ + 0xab, 0xa8, 0x04, 0xbe, 0x2a, 0x00, +} + +// a fixed block using literal/length symbol 286, likewise unassigned. +@(private="file") +v_bad_len_code := [?]byte{ + 0xab, 0xa8, 0x1c, 0x03, 0x00, +} + +// FDICT is set, so a 4-byte dictionary identifier follows the header and the +// data cannot be decoded without a dictionary we do not have. +@(private="file") +v_fdict := [?]byte{ + 0x78, 0x20, 0x11, 0x22, 0x33, 0x44, 0xcb, 0x4b, 0x4d, 0x4d, 0x29, 0x56, 0x48, 0x54, 0x48, 0xc9, + 0x4c, 0x2e, 0xc9, 0xcc, 0xcf, 0x4b, 0x2c, 0xaa, 0x04, 0x00, 0x3f, 0x3b, 0x06, 0xe7, +} + +@(test) +test_flate_multiple_blocks :: proc(t: ^testing.T) { + expect_decodes_to(t, v_multi_block[:], "first half, second half.", "three blocks") +} + +@(test) +test_flate_preset_dictionary_header :: proc(t: ^testing.T) { + // FDICT pushes the data four bytes further along. Nothing else changes. + expect_decodes_to(t, v_fdict[:], "needs a dictionary", "FDICT header") +} + +@(test) +test_flate_unassigned_symbols :: proc(t: ^testing.T) { + // Symbols 286/287 and distance symbols 30/31 exist in the alphabets but + // have no meaning; hitting one ends the block, keeping what came before. + out1, err1 := flate_decode(v_bad_dist_code[:]) + defer delete(out1) + testing.expectf(t, err1 == .None, "distance symbol 30: %v", error_string(err1)) + testing.expectf(t, string(out1) == "xy", "distance symbol 30: got %q", string(out1)) + + out2, err2 := flate_decode(v_bad_len_code[:]) + defer delete(out2) + testing.expectf(t, err2 == .None, "length symbol 286: %v", error_string(err2)) + testing.expectf(t, string(out2) == "xy", "length symbol 286: got %q", string(out2)) +} + +@(test) +test_flate_random_bytes :: proc(t: ^testing.T) { + // Nothing here is a valid stream; the point is that a few thousand + // arbitrary byte sequences neither crash, hang, nor leak. + seed := u64(0x9e3779b97f4a7c15) + src: [64]byte + for round in 0 ..< 2000 { + n := 1 + round % len(src) + for i in 0 ..< n { + seed = seed * 6364136223846793005 + 1442695040888963407 + src[i] = byte(seed >> 33) + } + out, err := flate_decode(src[:n]) + testing.expect(t, err == .None || err == .Bad_Flate_Data, "unexpected error kind") + testing.expect(t, len(out) <= flate_output_limit(n), "output cap ignored") + delete(out) + } +} + +@(test) +test_flate_honours_the_allocator :: proc(t: ^testing.T) { + // An arena that cannot free: every attempt, and the result, have to come + // out of the buffer the caller supplied and nowhere else. + backing := make([]byte, 256 * 1024) + defer delete(backing) + arena: mem.Arena + mem.arena_init(&arena, backing) + + want := zlib_reference(t, v_dynamic[:]) + defer delete(want) + + out, err := flate_decode(v_dynamic[:], mem.arena_allocator(&arena)) + testing.expectf(t, err == .None, "arena decode: %v", error_string(err)) + testing.expect(t, slice.equal(out, want), "arena decode produced different bytes") + testing.expect(t, arena.offset > 0, "nothing was taken from the arena") +} + +// Rejected attempts are the hazard peculiar to this codec. flate_decode reads +// the same bytes several ways -- a header at the front, a header a little way +// in, raw DEFLATE, Acrobat's dropped first byte -- and each attempt allocates +// an output buffer before it can know whether the guess will yield anything. +// Dropping the losers with `delete` is right against the heap and does nothing +// whatever against a Document arena, which answers .Free with +// .Mode_Not_Implemented; the buffers would simply sit there until `close`. +// apply_filter is what keeps that from happening, by decoding into scratch and +// copying only the winner across, so this test is on the boundary rather than +// on flate_decode itself -- flate_decode is allowed its guesses, it just may +// not make them with the Document's memory. +@(test) +test_flate_speculation_stays_out_of_the_document_arena :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // A well-formed zlib header over bytes that are not DEFLATE at all, so + // every candidate offset is tried and every one of them comes back empty. + broken := [?]byte{0x78, 0x9c, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07} + before := document_memory_used(doc) + out, err := apply_filter(doc, "FlateDecode", broken[:], nil, doc.allocator) + testing.expect_value(t, err, Error.Bad_Flate_Data) + testing.expect_value(t, len(out), 0) + testing.expectf(t, document_memory_used(doc) == before, + "%d bytes stranded in the arena by eight bytes that decoded to nothing", + document_memory_used(doc) - before) + + // And a stream that does decode, but only after the wrong guesses have + // been made and thrown away: no zlib wrapper, so the offset is a guess. + before = document_memory_used(doc) + out2, err2 := apply_filter(doc, "FlateDecode", v_raw_fixed[:], nil, doc.allocator) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, string(out2), "Hi PDF") + growth := document_memory_used(doc) - before + testing.expectf(t, growth <= len(out2) + FT_ARENA_SLACK, + "the arena grew by %d bytes for %d bytes of output", growth, len(out2)) +} diff --git a/filter_lzw.odin b/filter_lzw.odin new file mode 100644 index 0000000..3e0029d --- /dev/null +++ b/filter_lzw.odin @@ -0,0 +1,243 @@ +package pdf + +import "core:slice" + +// LZWDecode -- the TIFF flavour of LZW that PDF adopted (ISO 32000-2, 7.4.4). +// +// Codes are packed most-significant-bit first, start at 9 bits wide and grow +// to 12. 256 clears the table, 257 is end-of-data, and the string table is +// filled from 258 upwards. The only wrinkle is /EarlyChange: a TIFF encoder +// adds its table entry immediately after writing a code, so its "next free +// code" runs exactly one ahead of the decoder's. Encoders that were written +// against the TIFF text therefore switch to a wider code one code sooner than +// the textbook algorithm says, and a decoder has to follow suit. /EarlyChange +// 1 is the default and by far the common case, but 0 occurs in real files, so +// both are supported. + +@(private) LZW_CLEAR :: 256 +@(private) LZW_EOD :: 257 +@(private) LZW_FIRST_CODE :: 258 +@(private) LZW_MAX_CODE :: 4095 // 12 bits is the widest code +@(private) LZW_TABLE_SIZE :: 4096 +@(private) LZW_NO_PREFIX :: 0xffff // marks a single-byte root entry + +// Bytes a single LZW stream is allowed to expand to. LZW's worst case is a +// factor of a few thousand -- a megabyte of hostile codes can name gigabytes +// of output -- so the ceiling has to be absolute rather than a multiple of the +// input. Matches MAX_OBJSTM_SIZE in document.odin; no honest PDF comes close, +// since LZW is a legacy filter used for small images and old content streams. +@(private) LZW_MAX_OUTPUT :: 1 << 30 + +// One string in the table, stored as "the string at `prefix`, plus `suffix`". +// `first` is cached because the decoder needs the first byte of a string on +// every single code and walking the prefix chain for it would be quadratic. +@(private) +Lzw_Entry :: struct { + prefix: u16, + length: u16, + suffix: u8, + first: u8, +} + +@(private) +Lzw_Bits :: struct { + data: []byte, + pos: u64, // index of the next bit to read +} + +// lzw_code_width returns the width the *next* code is read at, given how many +// table entries exist. With early_change the switch happens at 511/1023/2047 +// instead of 512/1024/2048. +@(private) +lzw_code_width :: proc(next_code: int, early_change: bool) -> uint { + n := next_code + (1 if early_change else 0) + switch { + case n < 512: return 9 + case n < 1024: return 10 + case n < 2048: return 11 + } + return 12 +} + +// lzw_read_code pulls one big-endian code off the bit stream. It fails rather +// than reading past the end, which is how a stream truncated mid-code stops. +@(private) +lzw_read_code :: proc(b: ^Lzw_Bits, width: uint) -> (code: u16, ok: bool) { + if width < 1 || width > 12 { + return 0, false + } + total := u64(len(b.data)) * 8 + if b.pos + u64(width) > total { + return 0, false + } + + // A code spans at most 3 bytes (7 bits of offset + 12 of width = 19). + i := int(b.pos >> 3) + off := uint(b.pos & 7) + acc := u32(b.data[i]) << 16 + if i + 1 < len(b.data) { + acc |= u32(b.data[i + 1]) << 8 + } + if i + 2 < len(b.data) { + acc |= u32(b.data[i + 2]) + } + shift := 24 - off - width + b.pos += u64(width) + return u16((acc >> shift) & ((u32(1) << width) - 1)), true +} + +// lzw_emit appends the string named by `code`. The chain is walked backwards +// and the result reversed in place, which avoids a scratch buffer. It either +// appends the whole string or nothing at all, so a refusal never leaves half a +// string in the output. +@(private) +lzw_emit :: proc(buf: ^[dynamic]byte, table: ^[LZW_TABLE_SIZE]Lzw_Entry, code: u16) -> bool { + if int(code) >= LZW_TABLE_SIZE { + return false + } + n := int(table[code].length) + if n <= 0 || n > LZW_TABLE_SIZE { + return false + } + if len(buf^) > LZW_MAX_OUTPUT - n { + return false + } + + start := len(buf^) + c := code + // Prefixes are strictly decreasing by construction, so this terminates + // after at most LZW_TABLE_SIZE steps even if the table were corrupt. + for _ in 0 ..< LZW_TABLE_SIZE { + append(buf, table[c].suffix) + p := table[c].prefix + if p == LZW_NO_PREFIX || int(p) >= LZW_TABLE_SIZE { + break + } + c = p + } + if len(buf^) - start != n { + resize(buf, start) + return false + } + slice.reverse(buf^[start:]) + return true +} + +// lzw_decode decodes an LZWDecode stream. `early_change` mirrors the +// /EarlyChange decode parameter (default 1, i.e. true). +// +// The returned slice is allocated with `allocator` and belongs to the caller, +// and so does the buffer it grew out of: LZW's expansion is not knowable up +// front, so the buffer is sized by guess and regrown, and a stream that +// decodes to nothing frees it again. Give it scratch, not a Document arena, +// whose `delete` is a silent no-op that would keep the guess and every +// regrowth until `close`; `apply_filter` in filters.odin is where that is +// arranged. +// +// Damaged input degrades instead of failing: a stream with no EOD marker, a +// stream truncated in the middle of a code, a table that overflows without an +// intervening clear, and a code that was never defined all stop the decode and +// hand back everything produced up to that point. `err` is therefore non-.None +// only when nothing at all could be decoded from a non-empty stream -- callers +// that get bytes back should use them. +lzw_decode :: proc(data: []byte, early_change := true, allocator := context.allocator) -> (out: []byte, err: Error) { + if len(data) == 0 { + return nil, .None + } + + table: [LZW_TABLE_SIZE]Lzw_Entry + for i in 0 ..< 256 { + table[i] = {prefix = LZW_NO_PREFIX, length = 1, suffix = u8(i), first = u8(i)} + } + + // A guess, not a bound: LZW on real data lands near 3x. The buffer grows + // as needed and LZW_MAX_OUTPUT is what actually limits it. + guess := clamp(len(data) * 3, 64, 1 << 20) + buf, alloc_err := make([dynamic]byte, 0, guess, allocator) + if alloc_err != nil { + return nil, .Out_Of_Memory + } + + bits := Lzw_Bits{data = data} + next_code := LZW_FIRST_CODE + prev := -1 + saw_eod := false + + // Every pass either consumes at least 9 bits or leaves the loop, and the + // bit position is bounded by len(data)*8, so this cannot spin. + decode: for { + width := lzw_code_width(next_code, early_change) + raw, ok := lzw_read_code(&bits, width) + if !ok { + break decode + } + code := int(raw) + + switch { + case code == LZW_CLEAR: + next_code = LZW_FIRST_CODE + prev = -1 + continue decode + case code == LZW_EOD: + saw_eod = true + break decode + } + + start := len(buf) + switch { + case code < 256: + if len(buf) >= LZW_MAX_OUTPUT { + break decode + } + append(&buf, u8(code)) + + case code >= LZW_FIRST_CODE && code < next_code: + if !lzw_emit(&buf, &table, u16(code)) { + break decode + } + + case code == next_code && prev >= 0 && prev < LZW_TABLE_SIZE && next_code <= LZW_MAX_CODE: + // KwKwK: the encoder used the entry it is defining with this very + // code. The string is the previous one with its own first byte + // appended. ISO 32000-2, 7.4.4.2. + if !lzw_emit(&buf, &table, u16(prev)) { + break decode + } + if len(buf) >= LZW_MAX_OUTPUT { + break decode + } + append(&buf, table[prev].first) + + case: + // A code that was never defined and is not the legal KwKwK case. + // There is no way to resynchronise, so keep what we have. + break decode + } + if len(buf) == start { + break decode + } + + // The new entry is the previous string extended by the first byte of + // the one just emitted. Once the table is full we stop growing it and + // carry on decoding with what is there: a stream that overflows + // without sending a clear is broken, but usually only mildly. + if prev >= 0 && prev < LZW_TABLE_SIZE && next_code <= LZW_MAX_CODE { + table[next_code] = { + prefix = u16(prev), + length = u16(min(int(table[prev].length) + 1, LZW_TABLE_SIZE)), + suffix = buf[start], + first = table[prev].first, + } + next_code += 1 + } + prev = code + } + + if len(buf) == 0 { + delete(buf) + // An empty stream that reached its EOD really did encode nothing. + return nil, saw_eod ? .None : .Bad_Lzw_Data + } + _, _ = shrink(&buf) + return buf[:], .None +} diff --git a/filter_lzw_test.odin b/filter_lzw_test.odin new file mode 100644 index 0000000..372360b --- /dev/null +++ b/filter_lzw_test.odin @@ -0,0 +1,505 @@ +package pdf + +import "core:slice" +import "core:testing" + +// --------------------------------------------------------------------------- +// Test-side encoder +// +// Two pieces, deliberately kept apart. lzw_test_codes runs the textbook LZW +// encoder and produces a list of codes, which depends on nothing but the input +// bytes. lzw_test_pack turns that list into a bit stream, and *that* is where +// the code-width schedule lives. Splitting them means the round-trip tests +// exercise the table logic without the packer's width rule being taken on +// faith: the hand-computed vectors below pin the widths independently. +// --------------------------------------------------------------------------- + +@(private = "file") +Bit_Writer :: struct { + bytes: [dynamic]byte, + acc: u32, + nbits: uint, +} + +@(private = "file") +bw_put :: proc(w: ^Bit_Writer, code: u16, width: uint) { + w.acc = (w.acc << width) | u32(code) + w.nbits += width + for w.nbits >= 8 { + w.nbits -= 8 + append(&w.bytes, u8((w.acc >> w.nbits) & 0xff)) + } +} + +@(private = "file") +bw_flush :: proc(w: ^Bit_Writer) { + if w.nbits > 0 { + append(&w.bytes, u8((w.acc << (8 - w.nbits)) & 0xff)) + w.nbits = 0 + } +} + +// lzw_test_pack packs codes at the widths a decoder will read them at: the +// table size is advanced exactly the way the decoder advances it (one entry +// per code after the first following a clear). +@(private = "file") +lzw_test_pack :: proc(codes: []u16, early_change: bool, allocator := context.allocator) -> [dynamic]byte { + w := Bit_Writer{bytes = make([dynamic]byte, allocator)} + next := LZW_FIRST_CODE + have_prev := false + for c in codes { + bw_put(&w, c, lzw_code_width(next, early_change)) + switch c { + case LZW_CLEAR: + next = LZW_FIRST_CODE + have_prev = false + case LZW_EOD: + // no table change + case: + if have_prev && next <= LZW_MAX_CODE { + next += 1 + } + have_prev = true + } + } + bw_flush(&w) + return w.bytes +} + +// lzw_test_codes is the plain TIFF encoder: it adds a table entry immediately +// after emitting a code, which is why its table runs one ahead of a decoder's. +@(private = "file") +lzw_test_codes :: proc(input: []byte, allocator := context.allocator) -> [dynamic]u16 { + codes := make([dynamic]u16, allocator) + append(&codes, u16(LZW_CLEAR)) + if len(input) == 0 { + append(&codes, u16(LZW_EOD)) + return codes + } + + dict := make(map[u32]u16, 1024, context.allocator) + defer delete(dict) + + next := u16(LZW_FIRST_CODE) + omega := u16(input[0]) + for i in 1 ..< len(input) { + k := input[i] + key := (u32(omega) << 8) | u32(k) + if c, found := dict[key]; found { + omega = c + } else { + append(&codes, omega) + if int(next) <= LZW_MAX_CODE { + dict[key] = next + next += 1 + } + omega = u16(k) + } + } + append(&codes, omega) + append(&codes, u16(LZW_EOD)) + return codes +} + +@(private = "file") +lzw_test_encode :: proc(input: []byte, early_change: bool, allocator := context.allocator) -> [dynamic]byte { + codes := lzw_test_codes(input, context.allocator) + defer delete(codes) + return lzw_test_pack(codes[:], early_change, allocator) +} + +// --------------------------------------------------------------------------- +// Published vector: TIFF 6.0 section 13 encodes "-----A---B" as the codes +// 256 45 258 258 65 259 66 257, packed at 9 bits into these nine bytes. +// --------------------------------------------------------------------------- + +@(private = "file") +TIFF_CODES := [?]u16{256, 45, 258, 258, 65, 259, 66, 257} +@(private = "file") +TIFF_PACKED := [?]byte{0x80, 0x0b, 0x60, 0x50, 0x22, 0x0c, 0x0c, 0x85, 0x01} +@(private = "file") +TIFF_PLAIN :: "-----A---B" + +@(test) +test_lzw_tiff_vector :: proc(t: ^testing.T) { + for ec in ([?]bool{true, false}) { + out, err := lzw_decode(TIFF_PACKED[:], ec) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), TIFF_PLAIN) + } +} + +// The packer has to reproduce the spec's bytes exactly, or every round-trip +// test below would only be proving the decoder consistent with itself. +@(test) +test_lzw_packer_matches_tiff_vector :: proc(t: ^testing.T) { + packed := lzw_test_pack(TIFF_CODES[:], true) + defer delete(packed) + testing.expect(t, slice.equal(packed[:], TIFF_PACKED[:]), "packer disagrees with TIFF 6.0 vector") + + codes := lzw_test_codes(transmute([]byte)string(TIFF_PLAIN)) + defer delete(codes) + testing.expect(t, slice.equal(codes[:], TIFF_CODES[:]), "encoder disagrees with TIFF 6.0 vector") +} + +// --------------------------------------------------------------------------- +// KwKwK: "AAAAAA" encodes to 256 65 258 259 257, where 258 and 259 are both +// used by the code that defines them. Packed at 9 bits: 80 10 60 50 38 08. +// --------------------------------------------------------------------------- + +@(test) +test_lzw_kwkwk :: proc(t: ^testing.T) { + packed := [?]byte{0x80, 0x10, 0x60, 0x50, 0x38, 0x08} + out, err := lzw_decode(packed[:]) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), "AAAAAA") +} + +@(test) +test_lzw_kwkwk_without_predecessor :: proc(t: ^testing.T) { + // 258 immediately after a clear names a string that cannot exist yet. + codes := [?]u16{256, 258, 257} + packed := lzw_test_pack(codes[:], true) + defer delete(packed) + out, err := lzw_decode(packed[:]) + defer delete(out) + testing.expect_value(t, err, Error.Bad_Lzw_Data) + testing.expect_value(t, len(out), 0) +} + +// --------------------------------------------------------------------------- +// Degenerate and empty streams +// --------------------------------------------------------------------------- + +@(test) +test_lzw_empty_input :: proc(t: ^testing.T) { + out, err := lzw_decode(nil) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_lzw_eod_only :: proc(t: ^testing.T) { + for ec in ([?]bool{true, false}) { + codes := [?]u16{256, 257} + packed := lzw_test_pack(codes[:], ec) + defer delete(packed) + out, err := lzw_decode(packed[:], ec) + defer delete(out) + // An empty stream is legal; it must not be reported as damaged. + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(out), 0) + } +} + +@(test) +test_lzw_too_short_for_a_code :: proc(t: ^testing.T) { + // One byte cannot hold a 9-bit code, so nothing at all comes out. + short := [?]byte{0x80} + out, err := lzw_decode(short[:]) + defer delete(out) + testing.expect_value(t, err, Error.Bad_Lzw_Data) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_lzw_undefined_code :: proc(t: ^testing.T) { + // 300 is far beyond the two entries that exist at that point. + codes := [?]u16{256, u16('X'), 300, 257} + packed := lzw_test_pack(codes[:], true) + defer delete(packed) + out, err := lzw_decode(packed[:]) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), "X") +} + +@(test) +test_lzw_missing_eod :: proc(t: ^testing.T) { + // Everything but the trailing EOD; the decoder must stop at end of input + // rather than demanding the marker. + codes := TIFF_CODES[:len(TIFF_CODES) - 1] + packed := lzw_test_pack(codes, true) + defer delete(packed) + out, err := lzw_decode(packed[:]) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), TIFF_PLAIN) +} + +@(test) +test_lzw_truncated_at_every_offset :: proc(t: ^testing.T) { + // Truncation drops whole codes off the end, so whatever survives has to be + // a prefix of the full output -- and nothing may crash or hang. + whole := TIFF_PLAIN + for n in 0 ..= len(TIFF_PACKED) { + out, err := lzw_decode(TIFF_PACKED[:n]) + defer delete(out) + got := string(out) + testing.expect(t, len(got) <= len(whole), "truncated input produced too much output") + testing.expect(t, got == whole[:len(got)], "truncated output is not a prefix of the whole") + if len(got) > 0 { + testing.expect_value(t, err, Error.None) + } + } +} + +@(test) +test_lzw_clear_mid_stream :: proc(t: ^testing.T) { + // A clear resets the table, so the codes after it mean what they would at + // the start of a stream. + codes := [?]u16{256, u16('a'), u16('b'), 258, 256, u16('a'), u16('b'), 258, 257} + packed := lzw_test_pack(codes[:], true) + defer delete(packed) + out, err := lzw_decode(packed[:]) + defer delete(out) + testing.expect_value(t, err, Error.None) + // 258 is "ab" both times; without the reset the second 258 would still be + // "ab" but the entry numbering afterwards would differ. + testing.expect_value(t, string(out), "abababab") +} + +// --------------------------------------------------------------------------- +// Code width and /EarlyChange +// --------------------------------------------------------------------------- + +@(test) +test_lzw_code_width_boundaries :: proc(t: ^testing.T) { + Case :: struct { + next: int, + early: bool, + width: uint, + } + cases := [?]Case{ + {258, true, 9}, {510, true, 9}, {511, true, 10}, {512, true, 10}, + {1022, true, 10}, {1023, true, 11}, {2046, true, 11}, {2047, true, 12}, + {4095, true, 12}, {4096, true, 12}, + {258, false, 9}, {511, false, 9}, {512, false, 10}, {1023, false, 10}, + {1024, false, 11}, {2047, false, 11}, {2048, false, 12}, {4096, false, 12}, + } + for c in cases { + testing.expect_value(t, lzw_code_width(c.next, c.early), c.width) + } +} + +// 254 literal codes leave the table holding exactly 511 entries, which is the +// one place /EarlyChange 0 and 1 disagree about how wide the next code is. +@(private = "file") +lzw_width_boundary_codes :: proc(allocator := context.allocator) -> [dynamic]u16 { + codes := make([dynamic]u16, allocator) + append(&codes, u16(LZW_CLEAR)) + for i in 0 ..< 255 { + append(&codes, u16(i)) + } + append(&codes, u16(LZW_EOD)) + return codes +} + +@(test) +test_lzw_early_change_boundary :: proc(t: ^testing.T) { + codes := lzw_width_boundary_codes() + defer delete(codes) + + expect := make([]byte, 255) + defer delete(expect) + for i in 0 ..< 255 { + expect[i] = u8(i) + } + + for ec in ([?]bool{true, false}) { + packed := lzw_test_pack(codes[:], ec) + defer delete(packed) + out, err := lzw_decode(packed[:], ec) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect(t, slice.equal(out, expect), "width boundary round-trip failed") + } + + // The two settings really do produce different bytes, and reading one with + // the other's rule desynchronises rather than silently agreeing. + early := lzw_test_pack(codes[:], true) + defer delete(early) + late := lzw_test_pack(codes[:], false) + defer delete(late) + testing.expect(t, !slice.equal(early[:], late[:]), "EarlyChange made no difference to the bit stream") + + wrong, wrong_err := lzw_decode(early[:], false) + defer delete(wrong) + testing.expect_value(t, wrong_err, Error.None) + testing.expect(t, !slice.equal(wrong, expect), "decoding with the wrong EarlyChange should not match") +} + +// --------------------------------------------------------------------------- +// Round trips, including a table that fills up +// --------------------------------------------------------------------------- + +@(private = "file") +lzw_round_trip :: proc(t: ^testing.T, input: []byte, label: string, loc := #caller_location) { + for ec in ([?]bool{true, false}) { + packed := lzw_test_encode(input, ec) + defer delete(packed) + out, err := lzw_decode(packed[:], ec) + defer delete(out) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expectf(t, slice.equal(out, input), "%s round trip failed (early_change=%v)", label, ec, loc = loc) + } +} + +@(test) +test_lzw_round_trip_small :: proc(t: ^testing.T) { + inputs := [?]string{ + "-----A---B", + "A", + "AA", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "ABABABABABABABABABABABABA", + "the quick brown fox jumps over the lazy dog", + "\x00\x00\x00\xff\xff\xff\x00\x00\x00\xff\xff\xff", + } + for s in inputs { + lzw_round_trip(t, transmute([]byte)s, s) + } +} + +@(test) +test_lzw_round_trip_all_byte_values :: proc(t: ^testing.T) { + input := make([]byte, 256 * 8) + defer delete(input) + for i in 0 ..< len(input) { + input[i] = u8(i & 0xff) + } + lzw_round_trip(t, input, "all byte values") +} + +// A stream long enough to exhaust all 3838 table entries without ever sending +// a clear: the decoder has to stop growing the table and keep going. +@(test) +test_lzw_table_overflow_without_clear :: proc(t: ^testing.T) { + input := make([]byte, 40000) + defer delete(input) + state: u32 = 0x1234_5678 + for i in 0 ..< len(input) { + state = state * 1664525 + 1013904223 + input[i] = u8((state >> 19) & 0xff) + } + + codes := lzw_test_codes(input) + defer delete(codes) + // Confirms the premise: many more codes than the table can hold entries. + testing.expect(t, len(codes) > LZW_MAX_CODE + 500, "test input did not overflow the table") + + lzw_round_trip(t, input, "table overflow") +} + +@(test) +test_lzw_long_run_expands :: proc(t: ^testing.T) { + // Highly compressible input exercises the growth path of the output + // buffer and the long prefix chains that go with it. + input := make([]byte, 100_000) + defer delete(input) + slice.fill(input, u8('Z')) + lzw_round_trip(t, input, "long run") +} + +// --------------------------------------------------------------------------- +// Nothing may crash, hang or leak on arbitrary bytes +// --------------------------------------------------------------------------- + +@(test) +test_lzw_arbitrary_bytes :: proc(t: ^testing.T) { + buf := make([]byte, 512) + defer delete(buf) + state: u32 = 0xdead_beef + for _ in 0 ..< 200 { + n := (int(state >> 7) % len(buf)) + 1 + for i in 0 ..< n { + state = state * 1103515245 + 12345 + buf[i] = u8(state >> 16) + } + for ec in ([?]bool{true, false}) { + out, err := lzw_decode(buf[:n], ec) + defer delete(out) + if err != .None { + testing.expect_value(t, len(out), 0) + } + } + } +} + +@(test) +test_lzw_all_zero_bytes :: proc(t: ^testing.T) { + // Nine zero bits is code 0, so this is a run of NUL bytes plus whatever the + // growing table makes of it -- a cheap check that a pathological but legal + // stream terminates. + zeros := make([]byte, 4096) + defer delete(zeros) + out, err := lzw_decode(zeros) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect(t, len(out) > 0, "a stream of zero bits should decode to something") + for b in out { + testing.expect_value(t, b, u8(0)) + } +} + +@(test) +test_lzw_all_ff_bytes :: proc(t: ^testing.T) { + ones := make([]byte, 4096) + defer delete(ones) + slice.fill(ones, u8(0xff)) + out, err := lzw_decode(ones) + defer delete(out) + // 0x1ff is 511, an undefined code straight after the start, so nothing is + // decodable -- but the call must still return cleanly. + testing.expect_value(t, err, Error.Bad_Lzw_Data) + testing.expect_value(t, len(out), 0) +} + +// LZW's expansion is not knowable in advance, so lzw_decode sizes its buffer +// from the input length and grows it as it goes. Against a Document arena the +// first guess and every superseded growth buffer would be permanent -- an +// arena's `delete` is a silent no-op -- and a stream that decodes to nothing +// would leave the whole guess behind. apply_filter is where that is prevented, +// by decoding into scratch and copying the result across, so what the arena is +// charged is the output and nothing else. +@(test) +test_lzw_growth_stays_out_of_the_document_arena :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // 0x82 0x00 is the 9-bit code 260, which no table defines at the start of + // a stream, so not one byte is decoded -- after 300 KiB have been reserved + // for output that will never be written. + junk := make([]byte, 100 * 1024) + defer delete(junk) + junk[0] = 0x82 + + before := document_memory_used(doc) + out, err := apply_filter(doc, "LZWDecode", junk, nil, doc.allocator) + testing.expect_value(t, err, Error.Bad_Lzw_Data) + testing.expect_value(t, len(out), 0) + testing.expectf(t, document_memory_used(doc) == before, + "%d bytes stranded in the arena by a stream that decoded to nothing", + document_memory_used(doc) - before) + + // 16 KiB out of a buffer that starts at a few kilobytes: a handful of + // regrowths, every one of which would be permanent if it came from here. + plain := make([]byte, 16 * 1024) + defer delete(plain) + for i in 0 ..< len(plain) { + plain[i] = byte((i / 64) % 251) + } + packed := lzw_test_encode(plain, true) + defer delete(packed) + + before = document_memory_used(doc) + out2, err2 := apply_filter(doc, "LZWDecode", packed[:], nil, doc.allocator) + testing.expect_value(t, err2, Error.None) + testing.expect(t, slice.equal(out2, plain), "the stream did not round trip") + growth := document_memory_used(doc) - before + testing.expectf(t, growth <= len(out2) + FT_ARENA_SLACK, + "the arena grew by %d bytes for %d bytes of output", growth, len(out2)) +} diff --git a/filter_runlength.odin b/filter_runlength.odin new file mode 100644 index 0000000..907201c --- /dev/null +++ b/filter_runlength.odin @@ -0,0 +1,93 @@ +package pdf + +// RunLengthDecode: byte-oriented RLE, ISO 32000-2, 7.4.5. +// +// The stream is a sequence of length bytes: +// +// 0..127 the next length+1 bytes are literal +// 128 end of data +// 129..255 the next byte repeats 257-length times (2..128) +// +// Only ever seen on scanned images from older producers, and on those the data +// is as likely to be truncated as not, so running out of input mid-run ends +// the stream with what was decoded so far instead of failing. + +// The filter expands by at most 128x, so a hostile stream can ask for a +// gigabyte from eight megabytes of input. Decoding stops at this point; a real +// image that needs more than this is not one we could raster anyway. +@(private = "file") +MAX_RUN_LENGTH_OUTPUT :: 1 << 30 + +// run_length_decode decodes RunLengthDecode data. +// +// A missing EOD marker, or input that ends inside a run, ends the stream +// rather than failing. +// +// The returned slice is owned by the caller and allocated with `allocator`. +run_length_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) { + n := run_length_scan(data, nil) + if n == 0 { + return nil, .None + } + buf, alloc_err := make([]byte, n, allocator) + if alloc_err != nil { + return nil, .Out_Of_Memory + } + run_length_scan(data, buf) + return buf, .None +} + +// run_length_scan measures the output when `out` is nil and fills it +// otherwise; see the note in filter_ascii.odin on why the work is done twice. +@(private) +run_length_scan :: proc(data: []byte, out: []byte) -> int { + n := 0 + i := 0 + // `i` strictly increases on every branch, so the loop is bounded by the + // input length without needing separate fuel. + for i < len(data) { + length := int(data[i]) + i += 1 + if length == 128 { + break + } + if length < 128 { + count := length + 1 + // A literal run reaching past the end takes what is there; the + // stream is truncated, and the bytes before the cut are still good. + if count > len(data) - i { + count = len(data) - i + } + for k in 0 ..< count { + if n >= MAX_RUN_LENGTH_OUTPUT { + return n + } + if n < len(out) { + out[n] = data[i + k] + } + n += 1 + } + i += count + if count < length + 1 { + break + } + continue + } + if i >= len(data) { + break + } + b := data[i] + i += 1 + count := 257 - length + for _ in 0 ..< count { + if n >= MAX_RUN_LENGTH_OUTPUT { + return n + } + if n < len(out) { + out[n] = b + } + n += 1 + } + } + return n +} diff --git a/filter_runlength_test.odin b/filter_runlength_test.odin new file mode 100644 index 0000000..e916ea9 --- /dev/null +++ b/filter_runlength_test.odin @@ -0,0 +1,200 @@ +package pdf + +import "core:mem/virtual" +import "core:slice" +import "core:testing" + +@(private = "file") +Rl_Case :: struct { + name: string, + in_: []byte, + want: []byte, +} + +@(private = "file") +check_rl :: proc(t: ^testing.T, c: Rl_Case) { + out, err := run_length_decode(c.in_) + defer delete(out) + testing.expectf(t, err == .None, "%s: err = %v", c.name, err) + testing.expectf(t, slice.equal(out, c.want), "%s: got %v, want %v", c.name, out, c.want) +} + +@(test) +test_run_length_literals :: proc(t: ^testing.T) { + cases := []Rl_Case{ + {"one byte", {0, 'A', 128}, {'A'}}, + {"three bytes", {2, 'a', 'b', 'c', 128}, {'a', 'b', 'c'}}, + {"two runs", {1, 'a', 'b', 0, 'c', 128}, {'a', 'b', 'c'}}, + {"nul literal", {1, 0x00, 0x80, 128}, {0x00, 0x80}}, + // 128 only means EOD in a length position; as data it is just a byte. + {"128 as data", {0, 128, 128}, {128}}, + } + for c in cases { + check_rl(t, c) + } +} + +@(test) +test_run_length_repeats :: proc(t: ^testing.T) { + cases := []Rl_Case{ + // 257-255 = 2 is the shortest repeat the encoding can express. + {"two", {255, 'x', 128}, {'x', 'x'}}, + {"three", {254, 'x', 128}, {'x', 'x', 'x'}}, + {"four", {253, 0xFF, 128}, {0xFF, 0xFF, 0xFF, 0xFF}}, + {"mixed", {1, 'a', 'b', 254, 'c', 0, 'd', 128}, + {'a', 'b', 'c', 'c', 'c', 'd'}}, + } + for c in cases { + check_rl(t, c) + } +} + +@(test) +test_run_length_boundaries :: proc(t: ^testing.T) { + // The longest literal run (127 -> 128 bytes) and the longest repeat + // (129 -> 128 copies), the two lengths an off-by-one lands on. + enc := make([dynamic]byte) + defer delete(enc) + want := make([dynamic]byte) + defer delete(want) + + append(&enc, 127) + for i in 0 ..< 128 { + append(&enc, byte(i)) + append(&want, byte(i)) + } + append(&enc, 129, 'Z') + for _ in 0 ..< 128 { + append(&want, 'Z') + } + append(&enc, 128) + + check_rl(t, {"max runs", enc[:], want[:]}) +} + +@(test) +test_run_length_degenerate :: proc(t: ^testing.T) { + cases := []Rl_Case{ + {"empty", {}, {}}, + {"eod immediately", {128}, {}}, + {"eod then junk", {128, 5, 'a', 'b'}, {}}, + // No EOD marker: the data simply ends. + {"missing eod", {1, 'a', 'b'}, {'a', 'b'}}, + {"repeat, no eod", {254, 'q'}, {'q', 'q', 'q'}}, + } + for c in cases { + check_rl(t, c) + } +} + +@(test) +test_run_length_truncated :: proc(t: ^testing.T) { + cases := []Rl_Case{ + // A literal run promising more bytes than remain keeps what is there. + {"short literal", {5, 'a', 'b'}, {'a', 'b'}}, + {"literal, none", {5}, {}}, + // A repeat with no byte to repeat produces nothing and ends. + {"repeat, no byte", {250}, {}}, + {"tail after good", {1, 'a', 'b', 9, 'c'}, {'a', 'b', 'c'}}, + } + for c in cases { + check_rl(t, c) + } +} + +@(test) +test_run_length_truncated_everywhere :: proc(t: ^testing.T) { + // Every prefix of a well-formed stream must decode a prefix of the + // plaintext without crashing. + enc := []byte{2, 'a', 'b', 'c', 254, 'z', 0, 'Q', 129, '!', 128} + full := "abczzzQ" + for cut in 0 ..= len(enc) { + out, err := run_length_decode(enc[:cut]) + testing.expectf(t, err == .None, "cut %d: err = %v", cut, err) + n := min(len(out), len(full)) + testing.expectf(t, slice.equal(out[:n], transmute([]byte)full[:n]), + "cut %d: got %v", cut, out) + // Nothing past the '!' run can appear, so the length is bounded. + testing.expectf(t, len(out) <= len(full) + 128, "cut %d: %d bytes", cut, len(out)) + delete(out) + } +} + +@(test) +test_run_length_all_lengths :: proc(t: ^testing.T) { + // Sweep every length byte as the first byte of a stream, with one byte of + // payload behind it, and assert the decoded length is exactly what the + // encoding promises. This is the table the filter is. + for length in 0 ..= 255 { + buf := [3]byte{byte(length), 'p', 128} + out, err := run_length_decode(buf[:]) + want := 0 + switch { + case length == 128: + want = 0 + case length < 128: + // "length+1 literal bytes", of which only 2 are present here + // ('p' and the 128 that would otherwise have been EOD). + want = min(length + 1, 2) + case: + want = 257 - length + } + testing.expectf(t, err == .None, "length %d: err = %v", length, err) + testing.expectf(t, len(out) == want, "length %d: got %d bytes, want %d", + length, len(out), want) + delete(out) + } +} + +@(test) +test_run_length_cross_check :: proc(t: ^testing.T) { + // Vectors from an independent RunLength encoder over random bytes drawn + // from a small alphabet, so literal and repeat runs interleave. + cases := []struct{name, in_, want: string}{ + {"n=1", "\x00\xFF\x80", "\xFF"}, + {"n=3", "\xFF\x00\x00a\x80", "\x00\x00a"}, + {"n=9", "\x05\x00\xFFb\x00\xFF\x00\xFFc\x00a\x80", "\x00\xFFb\x00\xFF\x00cca"}, + {"n=40", + "\x00b\xFF\x00\x02a\xFFa\xFF\x00\x09cbcab\xFFcbc\x00\xFFa\x07\x00bacab\xFFa\xFFc\x09\x00a\xFFca\x00ba\x00b\x80", + "b\x00\x00a\xFFa\x00\x00cbcab\xFFcbc\x00aa\x00bacab\xFFacc\x00a\xFFca\x00ba\x00b"}, + } + for c in cases { + check_rl(t, {c.name, transmute([]byte)c.in_, transmute([]byte)c.want}) + } +} + +@(private = "file") +rl_fuzz_next :: proc(state: ^u64) -> u32 { + state^ = state^ * 6364136223846793005 + 1442695040888963407 + return u32(state^ >> 33) +} + +@(test) +test_run_length_fuzz :: proc(t: ^testing.T) { + // Random bytes decode to at most 128 bytes per input byte and must never + // crash or hang, whatever the length bytes happen to say. + state: u64 = 0xBADF00D + buf: [256]byte + for round in 0 ..< 400 { + n := int(rl_fuzz_next(&state) % u32(len(buf) + 1)) + for i in 0 ..< n { + buf[i] = byte(rl_fuzz_next(&state)) + } + out, err := run_length_decode(buf[:n]) + testing.expectf(t, err == .None, "round %d: err %v", round, err) + testing.expectf(t, len(out) <= 128 * n, "round %d: grew to %d from %d", + round, len(out), n) + delete(out) + } +} + +@(test) +test_run_length_honours_allocator :: proc(t: ^testing.T) { + arena: virtual.Arena + testing.expect(t, virtual.arena_init_growing(&arena) == nil) + defer virtual.arena_destroy(&arena) + + out, err := run_length_decode({2, 'a', 'b', 'c', 128}, virtual.arena_allocator(&arena)) + testing.expect(t, err == .None) + testing.expect(t, slice.equal(out, transmute([]byte)string("abc"))) +} diff --git a/filters.odin b/filters.odin new file mode 100644 index 0000000..dcd2fdc --- /dev/null +++ b/filters.odin @@ -0,0 +1,803 @@ +package pdf + +import "core:mem" + +// Stream decoding: the /Filter chain, the /DecodeParms that go with it, and +// the two predictors (ISO 32000-2, 7.4). +// +// The codecs themselves live in filter_*.odin. This file is only the plumbing: +// which filters a stream asks for, in what order, with which parameters, and +// what to do when one of them cannot deliver. +// +// The governing rule is that a stream which will not decode is an *empty* +// stream, not a failed document. A page whose one broken image will not +// inflate should still draw its text, so `stream_data` records a repair and +// hands back nothing rather than returning an error every caller would have to +// ignore. `Stream.state` is where the difference between "decoded to nothing" +// and "would not decode" is kept. + +// A chain longer than this is not a document, it is an attempt to make a +// reader do work. Real files use one filter; two (an ASCII armour over a +// compressor) is the most that occurs in practice. +@(private) +MAX_FILTER_CHAIN :: 16 + +// --------------------------------------------------------------------------- +// Filter names +// --------------------------------------------------------------------------- + +@(private) +Filter_Kind :: enum u8 { + Unknown = 0, + Ascii_Hex, + Ascii85, + Lzw, + Flate, + Run_Length, + Ccitt, + Dct, + Jpx, + Jbig2, + // /Crypt is a marker, not a transformation: decryption has already + // happened by the time a stream reaches this file. + Crypt, +} + +// filter_kind classifies a filter name. +// +// The abbreviations are the ones ISO 32000-2 table 93 defines for inline +// images. They are accepted on ordinary streams too: a producer that emits +// /Fl outside an inline image is wrong, but the intent is not in doubt and no +// other filter name could be meant. +@(private) +filter_kind :: proc(name: string) -> Filter_Kind { + switch name { + case "ASCIIHexDecode", "AHx": return .Ascii_Hex + case "ASCII85Decode", "A85": return .Ascii85 + case "LZWDecode", "LZW": return .Lzw + case "FlateDecode", "Fl": return .Flate + case "RunLengthDecode", "RL": return .Run_Length + case "CCITTFaxDecode", "CCF": return .Ccitt + case "DCTDecode", "DCT": return .Dct + case "JPXDecode": return .Jpx + case "JBIG2Decode": return .Jbig2 + case "Crypt": return .Crypt + } + return .Unknown +} + +// filter_name returns the unabbreviated spelling, so callers downstream of +// `stream_image_data` can compare against one name instead of two. +@(private) +filter_name :: proc(kind: Filter_Kind, fallback: string) -> string { + switch kind { + case .Ascii_Hex: return "ASCIIHexDecode" + case .Ascii85: return "ASCII85Decode" + case .Lzw: return "LZWDecode" + case .Flate: return "FlateDecode" + case .Run_Length: return "RunLengthDecode" + case .Ccitt: return "CCITTFaxDecode" + case .Dct: return "DCTDecode" + case .Jpx: return "JPXDecode" + case .Jbig2: return "JBIG2Decode" + case .Crypt: return "Crypt" + case .Unknown: return fallback + } + return fallback +} + +// filter_is_image reports whether a filter produces an image rather than a +// byte stream. These four are the ones `stream_image_data` stops at: decoding +// them needs the image dictionary (dimensions, colour space, /Decode), and +// their output is samples, not bytes a content parser could read. +@(private) +filter_is_image :: proc(kind: Filter_Kind) -> bool { + #partial switch kind { + case .Dct, .Jpx, .Jbig2, .Ccitt: return true + } + return false +} + +// filter_takes_parms reports whether a filter reads /DecodeParms at all. Used +// to place a parameter dictionary that a producer wrote where an array was +// required; see `filter_attach_parms`. +@(private) +filter_takes_parms :: proc(kind: Filter_Kind) -> bool { + #partial switch kind { + case .Lzw, .Flate, .Ccitt, .Jbig2, .Crypt: return true + } + return false +} + +// --------------------------------------------------------------------------- +// The chain +// --------------------------------------------------------------------------- + +@(private) +Filter_Step :: struct { + name: string, + kind: Filter_Kind, + parms: ^Dict, + // Position in the /Filter array, which is what /DecodeParms is indexed by. + // Not the same as the step's own index once a malformed entry is dropped. + slot: int, +} + +// filter_chain fills `steps` with the stream's filters in application order. +// +// `ok` is false only when the file asks for more filters than +// MAX_FILTER_CHAIN; every other malformation is absorbed. A missing /Filter +// leaves n == 0 and ok == true. +@(private) +filter_chain :: proc(doc: ^Document, d: ^Dict, steps: []Filter_Step) -> (n: int, ok: bool) { + if d == nil || len(steps) == 0 { + return 0, true + } + + // Inline images abbreviate both keys (ISO 32000-2, table 91). On an + // ordinary stream dictionary /F is an external file specification instead, + // but that is a string or a dictionary and neither survives the name test + // below, so looking is free. + filters := dict_get_alt(doc, d, "Filter", "F") + parms := dict_get_alt(doc, d, "DecodeParms", "DP") + + #partial switch f in filters { + case Name: + steps[0] = Filter_Step{name = string(f), kind = filter_kind(string(f)), slot = 0} + n = 1 + case ^Array: + count := array_len(f) + if count > len(steps) { + return 0, false + } + for i in 0 ..< count { + name, is_name := to_name(array_get(doc, f, i)) + if !is_name { + // A null or a stray number inside /Filter. Dropping the entry + // keeps the rest of the chain usable; treating it as a filter + // would lose the stream over one bad element. + continue + } + steps[n] = Filter_Step{name = name, kind = filter_kind(name), slot = i} + n += 1 + } + } + if n == 0 { + return 0, true + } + + #partial switch p in parms { + case ^Array: + // Positional, and a short or over-long array is not fatal: a missing + // or null entry simply means "no parameters for that filter". + for i in 0 ..< n { + steps[i].parms = array_dict(doc, p, steps[i].slot) + } + case ^Dict: + filter_attach_parms(steps[:n], p) + case ^Stream: + // Never legal, but `to_dict` accepts a stream where a dictionary is + // wanted and a file that points /DecodeParms at a stream object still + // means its dictionary. + filter_attach_parms(steps[:n], p.dict) + } + return n, true +} + +@(private) +filter_attach_parms :: proc(steps: []Filter_Step, p: ^Dict) { + if len(steps) == 1 { + steps[0].parms = p + return + } + // ISO 32000-2, 7.4.1 requires an array of dictionaries when /Filter is an + // array, but producers write a bare dictionary for chains such as + // [/ASCII85Decode /FlateDecode]. It can only have been meant for whichever + // filter reads parameters at all, so give it to the first one that does. + for i in 0 ..< len(steps) { + if filter_takes_parms(steps[i].kind) { + steps[i].parms = p + return + } + } + steps[0].parms = p +} + +// --------------------------------------------------------------------------- +// Decode parameters +// --------------------------------------------------------------------------- + +@(private) +Predictor_Parms :: struct { + predictor: int, + colors: int, + bpc: int, + columns: int, +} + +// filter_predictor_parms reads the four predictor entries of ISO 32000-2 +// table 8. Their defaults are what an absent /DecodeParms means, so a nil +// dictionary is the ordinary case rather than an error. +@(private) +filter_predictor_parms :: proc(doc: ^Document, parms: ^Dict) -> Predictor_Parms { + return Predictor_Parms{ + predictor = int(dict_int(doc, parms, "Predictor", 1)), + colors = int(dict_int(doc, parms, "Colors", 1)), + bpc = int(dict_int(doc, parms, "BitsPerComponent", 8)), + columns = int(dict_int(doc, parms, "Columns", 1)), + } +} + +// filter_ccitt_params reads the CCITTFaxDecode parameters of ISO 32000-2 +// table 12. Exposed to the image layer, which gets the raw parameter +// dictionary back from `stream_image_data` and has to build the same struct. +@(private) +filter_ccitt_params :: proc(doc: ^Document, parms: ^Dict) -> Ccitt_Params { + p := ccitt_default_params() + p.k = int(dict_int(doc, parms, "K", 0)) + p.columns = int(dict_int(doc, parms, "Columns", 1728)) + p.rows = int(dict_int(doc, parms, "Rows", 0)) + p.black_is_1 = dict_bool(doc, parms, "BlackIs1", false) + p.byte_align = dict_bool(doc, parms, "EncodedByteAlign", false) + p.end_of_line = dict_bool(doc, parms, "EndOfLine", false) + p.end_of_block = dict_bool(doc, parms, "EndOfBlock", true) + return p +} + +// --------------------------------------------------------------------------- +// One filter +// --------------------------------------------------------------------------- + +// apply_filter runs a single filter over `data`. +// +// `name` may be the full name or the inline-image abbreviation. `parms` is the +// matching /DecodeParms entry and may be nil, which means "all defaults". +// +// Ownership: when `err` is .None the result is allocated with `allocator` and +// belongs to the caller. When it is not, the result is either nil or `data` +// itself and must not be freed -- the pass-through errors (.Unknown_Filter, +// .Unsupported_Image, .Unsupported_Jpx) hand the bytes back untouched so a +// caller that knows better can still do something with them. +// +// A codec that fails part-way through returns .None with a short result, which +// is deliberate: half a content stream draws half a page, and that beats a +// blank one. Compare the length against what you expected if you need to know. +// +// This is also the allocator boundary, and that is the part worth reading +// twice. Every codec here grows a buffer it cannot size in advance, and the +// tolerant ones -- flate above all -- decode the same bytes several ways and +// keep the best attempt, dropping the losers with `delete`. Both habits are +// harmless against the heap and ruinous against `doc.allocator`: a +// `core:mem/virtual` arena answers .Free with .Mode_Not_Implemented, so +// `delete` on arena memory is a silent no-op rather than an error, and every +// rejected attempt and every superseded growth buffer stays until `close`. +// A damaged 256 KiB flate stream can strand megabytes that way, and the frees +// that would appear to clean it up are the ones ARCHITECTURE.md forbids. +// +// So a codec never sees the caller's allocator. It speculates in scratch, +// where its own `delete` calls do what they say, and exactly one buffer -- the +// winner, at its final length -- is copied into `allocator` afterwards. The +// caller's arena grows by the size of the output, not by the number of +// attempts it took to find it, and a codec added later inherits that without +// having to know about it. +apply_filter :: proc(doc: ^Document, name: string, data: []byte, parms: ^Dict, + allocator := context.allocator) -> (out: []byte, err: Error) { + scratch := context.allocator + if allocator == scratch { + // The caller already asked for the allocator the codec would have + // speculated in, so copying the result to itself would only double the + // peak for nothing. + return apply_filter_decode(doc, name, data, parms, scratch) + } + + tmp, terr := apply_filter_decode(doc, name, data, parms, scratch) + if terr != .None { + // Per the ownership rule above, a failed filter hands back nil or + // `data` itself. Neither is ours to copy or to free. + return tmp, terr + } + defer delete(tmp, scratch) + return filter_copy(tmp, allocator) +} + +// apply_filter_decode is the dispatch proper. `scratch` is throw-away memory +// that `apply_filter` frees on the way out, so a codec reached from here may +// allocate and free as freely as it likes; nothing it leaves behind outlives +// the call. Do not call this with a Document arena -- that is the whole point +// of it being separate. +@(private) +apply_filter_decode :: proc(doc: ^Document, name: string, data: []byte, parms: ^Dict, + scratch: mem.Allocator) -> (out: []byte, err: Error) { + switch filter_kind(name) { + case .Ascii_Hex: + return ascii_hex_decode(data, scratch) + + case .Ascii85: + return ascii85_decode(data, scratch) + + case .Run_Length: + return run_length_decode(data, scratch) + + case .Flate: + pp := filter_predictor_parms(doc, parms) + if pp.predictor <= 1 { + return flate_decode(data, scratch) + } + // The un-predicted bytes are an intermediate: freed here rather than + // carried out, so a predicted stream costs one buffer and not two. + raw, ferr := flate_decode(data, scratch) + defer delete(raw, scratch) + if ferr != .None { + return nil, ferr + } + return apply_predictor(raw, pp.predictor, pp.colors, pp.bpc, pp.columns, scratch) + + case .Lzw: + // /EarlyChange defaults to 1 (ISO 32000-2, table 8). + early := dict_int(doc, parms, "EarlyChange", 1) != 0 + pp := filter_predictor_parms(doc, parms) + if pp.predictor <= 1 { + return lzw_decode(data, early, scratch) + } + raw, ferr := lzw_decode(data, early, scratch) + defer delete(raw, scratch) + if ferr != .None { + return nil, ferr + } + return apply_predictor(raw, pp.predictor, pp.colors, pp.bpc, pp.columns, scratch) + + case .Ccitt: + return ccitt_decode(data, filter_ccitt_params(doc, parms), scratch) + + case .Crypt: + // Decryption is done at parse time, by the time any stream reaches + // here, so /Crypt is the identity. ISO 32000-2, 7.4.10. + return filter_copy(data, scratch) + + case .Dct, .Jbig2: + // Decoded by the image layer, which needs the image dictionary to make + // sense of the samples. `stream_image_data` is the way in. + return data, .Unsupported_Image + + case .Jpx: + return data, .Unsupported_Jpx + + case .Unknown: + return data, .Unknown_Filter + } + return data, .Unknown_Filter +} + +@(private) +filter_copy :: proc(data: []byte, allocator: mem.Allocator) -> (out: []byte, err: Error) { + if len(data) == 0 { + return nil, .None + } + buf, aerr := make([]byte, len(data), allocator) + if aerr != nil { + return nil, .Out_Of_Memory + } + copy(buf, data) + return buf, .None +} + +// --------------------------------------------------------------------------- +// Whole streams +// --------------------------------------------------------------------------- + +// stream_data decodes a stream's whole filter chain and caches the result. +// +// The bytes belong to the Document (they are allocated in its arena, or are a +// sub-slice of the file when the stream has no filters) and must not be freed. +// Calling this twice returns the same slice without decoding again. +// +// A stream that will not decode is reported as empty, with a .Bad_Filter_Data +// repair on the Document and `Stream.state` left at .Failed; the error return +// is reserved for a caller mistake. This is the difference between a reader +// and a validator: one broken image must not cost a page. +// +// With a nil Document there is no arena to allocate from, so the cached bytes +// come from the context allocator and belong to the caller -- except for a +// stream with no filters, which still borrows the file. `Stream.raw` says +// which of the two you have. +stream_data :: proc(doc: ^Document, s: ^Stream) -> (data: []byte, err: Error) { + if s == nil { + return nil, .Invalid_Argument + } + switch s.state { + case .Decoded: + return s.decoded, .None + case .Failed: + return nil, .None + case .Pending: + } + + allocator := context.allocator + if doc != nil { + allocator = doc.allocator + } + + // An empty payload decodes to nothing whatever the chain says. Running + // flate over it would only manufacture a repair for a stream that is + // legitimately empty -- an empty content stream is written this way. + if len(s.raw) == 0 { + s.decoded = nil + s.state = .Decoded + return nil, .None + } + + steps: [MAX_FILTER_CHAIN]Filter_Step + n, chain_ok := filter_chain(doc, s.dict, steps[:]) + if !chain_ok { + return stream_failed(doc, s) + } + + // Only the last stage's output is worth keeping, so the ones before it are + // scratch: each is freed as soon as the next stage has read it, and the + // survivor is copied into `allocator` at the end. Giving `allocator` every + // stage instead would strand the intermediates of an /ASCII85Decode over a + // /FlateDecode in the Document arena for the life of the document -- the + // arena cannot free them and ARCHITECTURE.md forbids asking it to -- and + // with no Document at all it would leak them outright. + scratch := context.allocator + cur := s.raw + tmp: []byte + filtered := false + defer delete(tmp, scratch) + + for i in 0 ..< n { + if steps[i].kind == .Crypt { + // Identity here, and skipping it avoids copying the payload for + // nothing. + continue + } + next, ferr := apply_filter(doc, steps[i].name, cur, steps[i].parms, scratch) + if ferr != .None { + return stream_failed(doc, s) + } + delete(tmp, scratch) + tmp = next + cur = next + filtered = true + } + + if !filtered { + // No /Filter at all, or a chain of nothing but /Crypt, which was + // already applied at parse time. The payload is still a sub-slice of + // the file, so there is nothing to copy and nothing to own -- and a + // caller with a nil Document must not free what it gets back here. + s.decoded = s.raw + s.state = .Decoded + return s.decoded, .None + } + + out := cur + if allocator == scratch { + // Where the two are the same allocator -- a nil Document -- the last + // stage already produced the buffer the caller keeps. Hand ownership + // over instead of copying it and freeing the original. + tmp = nil + } else { + cerr: Error + out, cerr = filter_copy(cur, allocator) + if cerr != .None { + return stream_failed(doc, s) + } + } + + s.decoded = out + s.state = .Decoded + return out, .None +} + +@(private) +stream_failed :: proc(doc: ^Document, s: ^Stream) -> (data: []byte, err: Error) { + add_repair(doc, .Bad_Filter_Data, 0, s.ref) + s.decoded = nil + s.state = .Failed + return nil, .None +} + +// stream_data_raw returns the stream's bytes exactly as stored -- still +// filtered, but already decrypted. Borrowed from the Document; do not free. +stream_data_raw :: proc(doc: ^Document, s: ^Stream) -> []byte { + if s == nil { + return nil + } + return s.raw +} + +// stream_image_data decodes everything up to the stream's image filter. +// +// An image whose final filter is DCTDecode, JPXDecode, JBIG2Decode or +// CCITTFaxDecode is decoded straight to pixels by the raster path, which needs +// the image dictionary to do it. So this walks the chain applying only the +// byte filters, stops at the first image filter, and returns the bytes that +// filter would have been given, its unabbreviated name and its parameters. +// `image_filter` is "" when the chain held no image filter, in which case +// `data` is the finished sample bytes. +// +// The result is allocated with `allocator` and belongs to the caller. A JPX or +// JBIG2 codestream comes back intact and without an error: deciding what to do +// with a format this layer does not decode is the image layer's business. +stream_image_data :: proc(doc: ^Document, s: ^Stream, allocator := context.allocator) -> + (data: []byte, image_filter: string, parms: ^Dict, err: Error) { + if s == nil { + return nil, "", nil, .Invalid_Argument + } + + steps: [MAX_FILTER_CHAIN]Filter_Step + n, chain_ok := filter_chain(doc, s.dict, steps[:]) + if !chain_ok { + add_repair(doc, .Bad_Filter_Data, 0, s.ref) + return nil, "", nil, .Unknown_Filter + } + + // Intermediates are scratch and are freed as the chain advances; only the + // last result is copied into `allocator`. Keeping them out of `allocator` + // means this is leak-free even when the caller hands us an arena it never + // frees individually. + cur := s.raw + tmp: []byte + defer delete(tmp, context.allocator) + + for i in 0 ..< n { + if filter_is_image(steps[i].kind) { + out, cerr := filter_copy(cur, allocator) + if cerr != .None { + return nil, "", nil, cerr + } + return out, filter_name(steps[i].kind, steps[i].name), steps[i].parms, .None + } + if steps[i].kind == .Crypt { + continue + } + next, ferr := apply_filter(doc, steps[i].name, cur, steps[i].parms, context.allocator) + if ferr != .None { + add_repair(doc, .Bad_Filter_Data, 0, s.ref) + return nil, "", nil, ferr + } + delete(tmp, context.allocator) + tmp = next + cur = next + } + + out, cerr := filter_copy(cur, allocator) + if cerr != .None { + return nil, "", nil, cerr + } + return out, "", nil, .None +} + +// --------------------------------------------------------------------------- +// Predictors +// +// A predictor is not a filter: it is a reversible transform the encoder ran +// over the samples first, so that the compressor behind it sees mostly zeroes. +// ISO 32000-2, 7.4.4.4 defines two, both borrowed wholesale -- predictor 2 is +// TIFF 6.0's horizontal differencing, and 10..15 are the five PNG line filters +// (RFC 2083, 6). The PNG value is the *maximum* filter the encoder used; +// the actual filter is the first byte of each row, so all six PNG values +// behave identically on decode. +// --------------------------------------------------------------------------- + +// DeviceN tops out at 32 colorants, and no predictor has ever been applied to +// anything wider. +@(private) +MAX_PREDICTOR_COLORS :: 32 +@(private) +MAX_PREDICTOR_COLUMNS :: 1 << 20 +// 16 MiB for a single row. An xref stream's rows are a handful of bytes and an +// image's are tens of kilobytes; this only exists so a /Columns out of a fuzzer +// cannot ask for an allocation the machine will not survive. +@(private) +MAX_PREDICTOR_ROW_BYTES :: 1 << 24 +@(private) +MAX_PREDICTOR_OUTPUT :: 1 << 30 + +// apply_predictor undoes a /Predictor transform. +// +// `predictor` is the /Predictor value: 0 or 1 for none, 2 for TIFF horizontal +// differencing, 10..15 for the PNG line filters. `colors`, `bpc` and `columns` +// are /Colors, /BitsPerComponent and /Columns. +// +// Parameters that no image could have are rejected with .Bad_Predictor rather +// than clamped: unlike a damaged payload, a nonsensical /Columns means the +// output would be misaligned garbage, and garbage that looks like data is +// worse than nothing. +// +// A final row cut short by a truncated payload is zero-padded and kept. +// Damaged xref streams rely on this -- the last entry of a truncated xref +// stream is still worth having. +// +// The result is allocated with `allocator` and belongs to the caller. +apply_predictor :: proc(data: []byte, predictor, colors, bpc, columns: int, + allocator := context.allocator) -> (out: []byte, err: Error) { + png := false + switch { + case predictor == 0, predictor == 1: + // /Predictor 0 is not defined; files that write it mean "none". + return filter_copy(data, allocator) + case predictor == 2: + case predictor >= 10 && predictor <= 15: + png = true + case: + return nil, .Bad_Predictor + } + + if colors < 1 || colors > MAX_PREDICTOR_COLORS { + return nil, .Bad_Predictor + } + switch bpc { + case 1, 2, 4, 8, 16: + case: + return nil, .Bad_Predictor + } + if columns < 1 || columns > MAX_PREDICTOR_COLUMNS { + return nil, .Bad_Predictor + } + + row_bits := i64(columns) * i64(colors) * i64(bpc) + if row_bits > i64(MAX_PREDICTOR_ROW_BYTES) * 8 { + return nil, .Bad_Predictor + } + row_bytes := int((row_bits + 7) / 8) + if row_bytes < 1 { + return nil, .Bad_Predictor + } + if len(data) == 0 { + return nil, .None + } + + // A PNG row carries a leading filter tag; a TIFF row does not. + stride := row_bytes + 1 if png else row_bytes + rows := (len(data) + stride - 1) / stride + total := rows * row_bytes + if total < 1 || total > MAX_PREDICTOR_OUTPUT { + return nil, .Bad_Predictor + } + + buf, aerr := make([]byte, total, allocator) + if aerr != nil { + return nil, .Out_Of_Memory + } + + for r in 0 ..< rows { + row := buf[r * row_bytes:][:row_bytes] + src := r * stride + (1 if png else 0) + // `buf` came from make and is zeroed, so a short final row is padded + // by simply not copying over it. + if src < len(data) { + copy(row, data[src:min(src + row_bytes, len(data))]) + } + if png { + tag := 0 + if r * stride < len(data) { + tag = int(data[r * stride]) + } + prev: []byte + if r > 0 { + prev = buf[(r - 1) * row_bytes:][:row_bytes] + } + predictor_png_row(row, prev, tag, max(1, (colors * bpc + 7) / 8)) + } else { + predictor_tiff_row(row, colors, bpc, columns) + } + } + return buf, .None +} + +// predictor_png_row reverses one PNG line filter in place. `prev` is the +// already reconstructed line above, or nil on the first line, where the +// filters are defined to read zeroes. `bpp` is the byte distance to the pixel +// on the left, at least 1 (RFC 2083, 6.1). +@(private) +predictor_png_row :: proc(row, prev: []byte, tag, bpp: int) { + n := len(row) + switch tag { + case 0: // None + case 1: // Sub + for i in bpp ..< n { + row[i] += row[i - bpp] + } + case 2: // Up + if prev == nil { + return + } + for i in 0 ..< n { + row[i] += prev[i] + } + case 3: // Average + for i in 0 ..< n { + left := i >= bpp ? int(row[i - bpp]) : 0 + up := prev != nil ? int(prev[i]) : 0 + row[i] += byte((left + up) / 2) + } + case 4: // Paeth + for i in 0 ..< n { + a := i >= bpp ? int(row[i - bpp]) : 0 + b := prev != nil ? int(prev[i]) : 0 + c := prev != nil && i >= bpp ? int(prev[i - bpp]) : 0 + row[i] += byte(predictor_paeth(a, b, c)) + } + case: + // An undefined tag. Encoders do not emit one, but a byte lost from the + // front of the payload shifts every tag, and reading the row as + // unfiltered keeps the rest of the stream in step instead of failing + // the whole stream on one byte. + } +} + +// predictor_paeth is the predictor of RFC 2083, 6.6, verbatim: of the three +// neighbours it picks whichever the linear estimate a+b-c is nearest to, +// resolving ties towards a then b. +@(private) +predictor_paeth :: proc(a, b, c: int) -> int { + p := a + b - c + pa := abs(p - a) + pb := abs(p - b) + pc := abs(p - c) + if pa <= pb && pa <= pc { + return a + } + if pb <= pc { + return b + } + return c +} + +// predictor_tiff_row reverses TIFF horizontal differencing in place: every +// component is stored as its difference from the component `colors` positions +// earlier in the same row, modulo the component's own width. +@(private) +predictor_tiff_row :: proc(row: []byte, colors, bpc, columns: int) { + n := columns * colors + switch bpc { + case 8: + limit := min(n, len(row)) + for i in colors ..< limit { + row[i] += row[i - colors] + } + case 16: + limit := min(n, len(row) / 2) + for i in colors ..< limit { + prev := u16(row[(i - colors) * 2]) << 8 | u16(row[(i - colors) * 2 + 1]) + cur := u16(row[i * 2]) << 8 | u16(row[i * 2 + 1]) + v := cur + prev + row[i * 2] = byte(v >> 8) + row[i * 2 + 1] = byte(v) + } + case 1, 2, 4: + // Sub-byte components are packed high bit first and never straddle a + // byte, since 1, 2 and 4 all divide 8. + mask := (1 << uint(bpc)) - 1 + limit := min(n, len(row) * 8 / bpc) + for i in colors ..< limit { + v := predictor_bits_get(row, i - colors, bpc) + predictor_bits_get(row, i, bpc) + predictor_bits_set(row, i, bpc, v & mask) + } + } +} + +@(private) +predictor_bits_get :: proc(row: []byte, index, bpc: int) -> int { + bit := index * bpc + at := bit >> 3 + if at < 0 || at >= len(row) { + return 0 + } + shift := uint(8 - bpc - (bit & 7)) + return int(row[at] >> shift) & ((1 << uint(bpc)) - 1) +} + +@(private) +predictor_bits_set :: proc(row: []byte, index, bpc, value: int) { + bit := index * bpc + at := bit >> 3 + if at < 0 || at >= len(row) { + return + } + shift := uint(8 - bpc - (bit & 7)) + mask := byte(((1 << uint(bpc)) - 1) << shift) + row[at] = (row[at] & ~mask) | (byte(value) << shift) & mask +} diff --git a/filters_test.odin b/filters_test.odin new file mode 100644 index 0000000..4660d9b --- /dev/null +++ b/filters_test.odin @@ -0,0 +1,1352 @@ +package pdf + +import "core:mem/virtual" +import "core:slice" +import "core:testing" + +// The predictor vectors below are hand-computed from RFC 2083 section 6 and +// TIFF 6.0 section 14 and are written out byte by byte in the test that uses +// them, so a change of behaviour has to be argued with rather than re-blessed. +// The compressed vectors come from zlib via CPython, and the LZW one is the +// published TIFF 6.0 example. + +// --------------------------------------------------------------------------- +// Vectors +// --------------------------------------------------------------------------- + +@(private = "file") +V_CHAIN_PLAIN :: "Hello, filter chain!" + +// zlib.compress(V_CHAIN_PLAIN) +@(private = "file") +v_chain_flate := [?]byte{ + 0x78, 0xda, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x48, 0xcb, 0xcc, 0x29, 0x49, 0x2d, 0x52, + 0x48, 0xce, 0x48, 0xcc, 0xcc, 0x53, 0x04, 0x00, 0x4a, 0x94, 0x07, 0x0b, +} + +// the same bytes in ASCII85: "GhVa[c,n)Z;+SmS.7l*h8BscMbYnWE8sBPg~>" +@(private = "file") +v_chain_a85 := [?]byte{ + 0x47, 0x68, 0x56, 0x61, 0x5b, 0x63, 0x2c, 0x6e, 0x29, 0x5a, 0x3b, 0x2b, 0x53, 0x6d, 0x53, 0x2e, + 0x37, 0x6c, 0x2a, 0x68, 0x38, 0x42, 0x73, 0x63, 0x4d, 0x62, 0x59, 0x6e, 0x57, 0x45, 0x38, 0x73, + 0x42, 0x50, 0x67, 0x7e, 0x3e, +} + +// The shape an xref stream has: three four-byte rows, PNG Up-filtered, then +// deflated. The encoded rows are {2,1,0,0,0}, {2,0,0,0,1}, {2,0,0,0,1} and +// they reconstruct to V_PRED_PAYLOAD. +@(private = "file") +v_pred_flate := [?]byte{ + 0x78, 0xda, 0x63, 0x62, 0x64, 0x60, 0x60, 0x60, 0x02, 0x62, 0x46, 0x30, 0x01, 0x00, 0x00, 0x60, + 0x00, 0x0a, +} + +@(private = "file") +v_pred_payload := [?]byte{1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 2} + +// TIFF 6.0 section 13: "-----A---B" packed at 9 bits. +@(private = "file") +v_lzw_tiff := [?]byte{0x80, 0x0b, 0x60, 0x50, 0x22, 0x0c, 0x0c, 0x85, 0x01} + +@(private = "file") +V_LZW_PLAIN :: "-----A---B" + +// --------------------------------------------------------------------------- +// Fixtures +// +// A Document built by hand: filters need nothing but the arena and the repair +// list, and going through open_bytes would drag in the whole file structure. +// --------------------------------------------------------------------------- + +// Package-private rather than file-private: the codec test files measure how +// much of this arena a decode costs, and that rule belongs to apply_filter +// rather than to any one codec, so they should not each grow a Document of +// their own. +@(private) +filter_test_doc :: proc() -> ^Document { + doc := new(Document) + doc.backing = context.allocator + if err := virtual.arena_init_growing(&doc.arena); err != nil { + free(doc) + return nil + } + doc.allocator = virtual.arena_allocator(&doc.arena) + doc.repairs = make([dynamic]Repair, doc.allocator) + return doc +} + +@(private) +filter_test_doc_free :: proc(doc: ^Document) { + if doc == nil { + return + } + virtual.arena_destroy(&doc.arena) + free(doc, doc.backing) +} + +@(private = "file") +ft_names :: proc(doc: ^Document, names: []string) -> ^Array { + a := new_array(doc.allocator) + for n in names { + array_push(a, Name(n)) + } + return a +} + +// ft_parms builds a parameter dictionary from alternating key/value pairs, +// where every value is an integer -- which is all the predictor and +// /EarlyChange need. +@(private = "file") +ft_parms :: proc(doc: ^Document, keys: []string, values: []i64) -> ^Dict { + d := new_dict(doc.allocator) + for k, i in keys { + if i < len(values) { + dict_set(d, k, values[i]) + } + } + return d +} + +@(private = "file") +ft_stream :: proc(doc: ^Document, raw: []byte, filter: Object, parms: Object, + filter_key := "Filter", parms_key := "DecodeParms") -> ^Stream { + d := new_dict(doc.allocator) + if filter != nil { + dict_set(d, filter_key, filter) + } + if parms != nil { + dict_set(d, parms_key, parms) + } + return new_stream(d, raw, doc.allocator) +} + +@(private = "file") +ft_repairs :: proc(doc: ^Document, kind: Repair_Kind) -> int { + n := 0 + for r in doc.repairs { + if r.kind == kind { + n += 1 + } + } + return n +} + +// --------------------------------------------------------------------------- +// Filter names +// --------------------------------------------------------------------------- + +@(test) +test_filter_kind_accepts_full_names_and_abbreviations :: proc(t: ^testing.T) { + Case :: struct { + name: string, + kind: Filter_Kind, + } + cases := [?]Case{ + {"ASCIIHexDecode", .Ascii_Hex}, + {"AHx", .Ascii_Hex}, + {"ASCII85Decode", .Ascii85}, + {"A85", .Ascii85}, + {"LZWDecode", .Lzw}, + {"LZW", .Lzw}, + {"FlateDecode", .Flate}, + {"Fl", .Flate}, + {"RunLengthDecode", .Run_Length}, + {"RL", .Run_Length}, + {"CCITTFaxDecode", .Ccitt}, + {"CCF", .Ccitt}, + {"DCTDecode", .Dct}, + {"DCT", .Dct}, + {"JPXDecode", .Jpx}, + {"JBIG2Decode", .Jbig2}, + {"Crypt", .Crypt}, + {"", .Unknown}, + {"Flate", .Unknown}, + {"flatedecode", .Unknown}, + {"NotAFilter", .Unknown}, + } + for c in cases { + testing.expectf(t, filter_kind(c.name) == c.kind, + "filter_kind(%q) = %v, want %v", c.name, filter_kind(c.name), c.kind) + } + // The abbreviation is always reported back in full, so the image layer has + // one spelling to compare against. + testing.expect_value(t, filter_name(filter_kind("DCT"), "DCT"), "DCTDecode") + testing.expect_value(t, filter_name(.Unknown, "Whatever"), "Whatever") +} + +// --------------------------------------------------------------------------- +// PNG predictors -- RFC 2083, 6.2 to 6.6 +// --------------------------------------------------------------------------- + +// One row per filter type, each hand-reconstructed above: +// +// None 10 20 30 +// Sub 1 1+2=3 3+3=6 +// Up 5+1 5+3 5+6 +// Average 1+(0+6)/2 1+(4+8)/2 1+(7+11)/2 +// Paeth 1+b(4) 2+b(7) 3+b(10) +@(test) +test_predictor_png_every_filter :: proc(t: ^testing.T) { + data := [?]byte{ + 0, 10, 20, 30, + 1, 1, 2, 3, + 2, 5, 5, 5, + 3, 1, 1, 1, + 4, 1, 2, 3, + } + want := [?]byte{ + 10, 20, 30, + 1, 3, 6, + 6, 8, 11, + 4, 7, 10, + 5, 9, 13, + } + out, err := apply_predictor(data[:], 12, 1, 8, 3) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// Every /Predictor from 10 to 15 decodes identically: the value records the +// widest filter the encoder was allowed to pick, and the row tag is what +// actually selects one. +@(test) +test_predictor_png_values_10_to_15_agree :: proc(t: ^testing.T) { + data := [?]byte{1, 1, 2, 3, 2, 5, 5, 5} + want := [?]byte{1, 3, 6, 6, 8, 11} + for p in 10 ..= 15 { + out, err := apply_predictor(data[:], p, 1, 8, 3) + defer delete(out) + testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err)) + testing.expectf(t, slice.equal(out, want[:]), "predictor %d: got %v", p, out) + } +} + +// On the first row the filters read zeroes for the line above, so Up is a +// no-op, Average halves the left neighbour and Paeth degenerates to Sub. +@(test) +test_predictor_png_first_row_reads_zeroes :: proc(t: ^testing.T) { + Case :: struct { + tag: byte, + want: [2]byte, + } + cases := [?]Case{ + {0, {5, 3}}, + {1, {5, 8}}, + {2, {5, 3}}, + {3, {5, 5}}, + {4, {5, 8}}, + } + for c in cases { + data := [?]byte{c.tag, 5, 3} + want := c.want + out, err := apply_predictor(data[:], 15, 1, 8, 2) + defer delete(out) + testing.expectf(t, err == .None, "tag %d: %s", c.tag, error_string(err)) + testing.expectf(t, slice.equal(out, want[:]), "tag %d: got %v, want %v", + c.tag, out, want) + } +} + +// bpp is the byte distance to the pixel on the left, so Sub over three +// 8-bit colours adds the value three bytes back, not one. +@(test) +test_predictor_png_multibyte_pixel :: proc(t: ^testing.T) { + data := [?]byte{1, 10, 20, 30, 1, 2, 3} + want := [?]byte{10, 20, 30, 11, 22, 33} + out, err := apply_predictor(data[:], 12, 3, 8, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// bpp has a floor of 1: four 1-bit colours are half a byte per pixel, and Sub +// still reaches one byte back. +@(test) +test_predictor_png_sub_byte_pixel :: proc(t: ^testing.T) { + data := [?]byte{1, 0x11, 0x22} + want := [?]byte{0x11, 0x33} + out, err := apply_predictor(data[:], 12, 4, 1, 4) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// Arithmetic is modulo 256 in both directions (RFC 2083, 6.2). +@(test) +test_predictor_png_wraps_at_256 :: proc(t: ^testing.T) { + data := [?]byte{1, 200, 100, 2, 200} + // 200; 200+100 = 300 mod 256 = 44; then Up: 200+200 = 400 mod 256 = 144. + want := [?]byte{200, 44, 144, 44} + out, err := apply_predictor(data[:], 12, 1, 8, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// A tag no encoder emits: one byte lost from the front of the payload shifts +// every tag, and reading the row unfiltered keeps the rest of the stream in +// step instead of losing all of it. The row after it still decodes against +// the row the unknown tag produced. +@(test) +test_predictor_png_unknown_row_tag_is_none :: proc(t: ^testing.T) { + data := [?]byte{9, 7, 7, 2, 1, 2} + want := [?]byte{7, 7, 8, 9} + out, err := apply_predictor(data[:], 12, 1, 8, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +@(test) +test_predictor_paeth_matches_the_reference :: proc(t: ^testing.T) { + reference :: proc(a, b, c: int) -> int { + p := a + b - c + pa, pb, pc := abs(p - a), abs(p - b), abs(p - c) + if pa <= pb && pa <= pc { + return a + } else if pb <= pc { + return b + } + return c + } + for a := 0; a < 256; a += 7 { + for b := 0; b < 256; b += 5 { + for c := 0; c < 256; c += 3 { + testing.expectf(t, predictor_paeth(a, b, c) == reference(a, b, c), + "paeth(%d,%d,%d) = %d, want %d", a, b, c, + predictor_paeth(a, b, c), reference(a, b, c)) + } + } + } +} + +// --------------------------------------------------------------------------- +// TIFF predictor -- TIFF 6.0, section 14 +// --------------------------------------------------------------------------- + +@(test) +test_predictor_tiff_8_bit :: proc(t: ^testing.T) { + data := [?]byte{10, 5, 5, 1, 1, 1} + want := [?]byte{10, 15, 20, 1, 2, 3} + out, err := apply_predictor(data[:], 2, 1, 8, 3) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// Differencing is per colour, so with three colours each component is the +// difference from the same component of the pixel to its left. +@(test) +test_predictor_tiff_8_bit_three_colours :: proc(t: ^testing.T) { + data := [?]byte{10, 20, 30, 1, 2, 3} + want := [?]byte{10, 20, 30, 11, 22, 33} + out, err := apply_predictor(data[:], 2, 3, 8, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +// Four 4-bit components in two bytes: 1 2 3 4 accumulates to 1 3 6 10. +@(test) +test_predictor_tiff_4_bit :: proc(t: ^testing.T) { + data := [?]byte{0x12, 0x34} + want := [?]byte{0x13, 0x6a} + out, err := apply_predictor(data[:], 2, 1, 4, 4) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want) +} + +// The same two bytes read as two pixels of two 4-bit colours: component 2 adds +// component 0 and component 3 adds component 1. +@(test) +test_predictor_tiff_4_bit_two_colours :: proc(t: ^testing.T) { + data := [?]byte{0x12, 0x34} + want := [?]byte{0x12, 0x46} + out, err := apply_predictor(data[:], 2, 2, 4, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want) +} + +// Four 2-bit components in one byte: 1 2 3 1 accumulates to 1 3 2 3, each +// sum taken modulo 4. +@(test) +test_predictor_tiff_2_bit :: proc(t: ^testing.T) { + data := [?]byte{0b01_10_11_01} + want := [?]byte{0b01_11_10_11} + out, err := apply_predictor(data[:], 2, 1, 2, 4) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %b, want %b", out, want) +} + +// 1-bit components add modulo 2, which is an exclusive or along the row. +@(test) +test_predictor_tiff_1_bit :: proc(t: ^testing.T) { + data := [?]byte{0xaa} + want := [?]byte{0xcc} + out, err := apply_predictor(data[:], 2, 1, 1, 8) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want) +} + +// 16-bit components are big-endian and wrap at 65536, not at 256. +@(test) +test_predictor_tiff_16_bit :: proc(t: ^testing.T) { + Case :: struct { + data: [4]byte, + want: [4]byte, + } + cases := [?]Case{ + {{0x00, 0x10, 0x00, 0x20}, {0x00, 0x10, 0x00, 0x30}}, + {{0xff, 0xff, 0x00, 0x02}, {0xff, 0xff, 0x00, 0x01}}, + } + for c in cases { + input := c.data + want := c.want + out, err := apply_predictor(input[:], 2, 1, 16, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want) + } +} + +// The padding bits at the end of a row are not components and must be left +// alone: 3 columns of 4 bits occupy a byte and a half. +@(test) +test_predictor_tiff_ignores_row_padding :: proc(t: ^testing.T) { + data := [?]byte{0x12, 0x3f} + // components 1, 2, 3 -> 1, 3, 6; the low nibble of byte 1 is padding. + want := [?]byte{0x13, 0x6f} + out, err := apply_predictor(data[:], 2, 1, 4, 3) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want) +} + +// --------------------------------------------------------------------------- +// Predictor edge cases +// --------------------------------------------------------------------------- + +// A payload that stops mid-row is padded, not dropped. A truncated xref stream +// still describes the objects it got to. +@(test) +test_predictor_truncated_final_row_is_padded :: proc(t: ^testing.T) { + png := [?]byte{0, 10, 20, 30, 2, 1} + png_want := [?]byte{10, 20, 30, 11, 20, 30} + out, err := apply_predictor(png[:], 12, 1, 8, 3) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, png_want[:]), "png: got %v, want %v", out, png_want) + + tiff := [?]byte{10, 5, 5, 1, 1} + tiff_want := [?]byte{10, 15, 20, 1, 2, 2} + out2, err2 := apply_predictor(tiff[:], 2, 1, 8, 3) + defer delete(out2) + testing.expect_value(t, err2, Error.None) + testing.expectf(t, slice.equal(out2, tiff_want[:]), "tiff: got %v, want %v", out2, tiff_want) +} + +// A row consisting of nothing but its filter tag still produces a row. +@(test) +test_predictor_final_row_is_only_a_tag :: proc(t: ^testing.T) { + data := [?]byte{0, 10, 20, 1} + want := [?]byte{10, 20, 0, 0} + out, err := apply_predictor(data[:], 12, 1, 8, 2) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want) +} + +@(test) +test_predictor_none_copies :: proc(t: ^testing.T) { + data := [?]byte{1, 2, 3, 4} + for p in 0 ..= 1 { + out, err := apply_predictor(data[:], p, 1, 8, 4) + defer delete(out) + testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err)) + testing.expectf(t, slice.equal(out, data[:]), "predictor %d changed the data", p) + testing.expectf(t, raw_data(out) != raw_data(data[:]), + "predictor %d aliased its input", p) + } +} + +@(test) +test_predictor_empty_input :: proc(t: ^testing.T) { + for p in ([?]int{1, 2, 12}) { + out, err := apply_predictor(nil, p, 1, 8, 4) + defer delete(out) + testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err)) + testing.expectf(t, len(out) == 0, "predictor %d invented %d bytes", p, len(out)) + } +} + +@(test) +test_predictor_rejects_absurd_parameters :: proc(t: ^testing.T) { + Case :: struct { + label: string, + predictor, colors, bpc, columns: int, + } + cases := [?]Case{ + {"predictor 3 is not defined", 3, 1, 8, 4}, + {"predictor 9 is not defined", 9, 1, 8, 4}, + {"predictor 16 is past PNG", 16, 1, 8, 4}, + {"negative predictor", -1, 1, 8, 4}, + {"zero colours", 12, 0, 8, 4}, + {"negative colours", 12, -3, 8, 4}, + {"more colours than DeviceN", 12, 33, 8, 4}, + {"3 bits per component", 12, 1, 3, 4}, + {"zero bits per component", 12, 1, 0, 4}, + {"32 bits per component", 12, 1, 32, 4}, + {"negative bits per component", 12, 1, -8, 4}, + {"zero columns", 12, 1, 8, 0}, + {"negative columns", 12, 1, 8, -4}, + {"absurd columns", 12, 1, 8, 1 << 30}, + {"absurd row", 2, 32, 16, 1 << 20}, + } + data := [?]byte{0, 1, 2, 3, 4, 5, 6, 7} + for c in cases { + out, err := apply_predictor(data[:], c.predictor, c.colors, c.bpc, c.columns) + defer delete(out) + testing.expectf(t, err == .Bad_Predictor, "%s: got %s", c.label, error_string(err)) + testing.expectf(t, out == nil, "%s: returned %d bytes as well", c.label, len(out)) + } +} + +// Padding the final row is the only way the output can exceed the input, so +// the ceiling on an allocation the file controls is one row -- and a row is +// itself capped, which is what MAX_PREDICTOR_ROW_BYTES is for. +@(test) +test_predictor_output_is_bounded_by_one_row :: proc(t: ^testing.T) { + Case :: struct { + predictor, colors, bpc, columns: int, + row_bytes: int, + } + cases := [?]Case{ + {15, 32, 16, 1 << 15, 32 * 16 * (1 << 15) / 8}, + {12, 1, 8, 1 << 20, 1 << 20}, + {2, 4, 8, 1000, 4000}, + {12, 1, 1, 7, 1}, + } + data := make([]byte, 4096) + defer delete(data) + for c in cases { + out, err := apply_predictor(data, c.predictor, c.colors, c.bpc, c.columns) + defer delete(out) + testing.expectf(t, err == .None, "%v: %s", c, error_string(err)) + testing.expectf(t, len(out) <= len(data) + c.row_bytes, + "%v: %d bytes out of %d in, more than one row of slack", + c, len(out), len(data)) + } +} + +// --------------------------------------------------------------------------- +// apply_filter +// --------------------------------------------------------------------------- + +@(test) +test_apply_filter_byte_filters :: proc(t: ^testing.T) { + Case :: struct { + name: string, + raw: string, + want: string, + } + cases := [?]Case{ + {"ASCIIHexDecode", "48656C6C6F>", "Hello"}, + {"AHx", "48656C6C6F>", "Hello"}, + {"ASCII85Decode", "87cURD]j7BEbo7~>", "Hello world"}, + {"A85", "87cURD]j7BEbo7~>", "Hello world"}, + {"RunLengthDecode", "\x02abc\xfeZ\x80", "abcZZZ"}, + {"RL", "\x02abc\xfeZ\x80", "abcZZZ"}, + } + for c in cases { + out, err := apply_filter(nil, c.name, transmute([]byte)c.raw, nil) + defer delete(out) + testing.expectf(t, err == .None, "%s: %s", c.name, error_string(err)) + testing.expectf(t, string(out) == c.want, "%s: got %q, want %q", + c.name, string(out), c.want) + } +} + +@(test) +test_apply_filter_flate :: proc(t: ^testing.T) { + out, err := apply_filter(nil, "Fl", v_chain_flate[:], nil) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), V_CHAIN_PLAIN) +} + +@(test) +test_apply_filter_lzw_honours_early_change :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + for value in ([?]i64{0, 1}) { + parms := ft_parms(doc, {"EarlyChange"}, {value}) + out, err := apply_filter(doc, "LZW", v_lzw_tiff[:], parms) + defer delete(out) + testing.expectf(t, err == .None, "EarlyChange %d: %s", value, error_string(err)) + testing.expectf(t, string(out) == V_LZW_PLAIN, + "EarlyChange %d: got %q", value, string(out)) + } + // No parameters at all means /EarlyChange 1. + out, err := apply_filter(nil, "LZWDecode", v_lzw_tiff[:], nil) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), V_LZW_PLAIN) +} + +@(test) +test_apply_filter_runs_the_predictor :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4}) + out, err := apply_filter(doc, "FlateDecode", v_pred_flate[:], parms) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, v_pred_payload[:]), "got %v, want %v", out, v_pred_payload) +} + +// LZW with the TIFF predictor, end to end: "-----A---B" read as two rows of +// five 8-bit components, each accumulating along its row modulo 256. +@(test) +test_stream_data_lzw_with_the_tiff_predictor :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"Predictor", "Colors", "BitsPerComponent", "Columns"}, {2, 1, 8, 5}) + s := ft_stream(doc, v_lzw_tiff[:], Name("LZWDecode"), parms) + got, err := stream_data(doc, s) + want := [?]byte{45, 90, 135, 180, 225, 65, 110, 155, 200, 10} + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(got, want[:]), "got %v, want %v", got, want) +} + +// A predictor whose parameters cannot describe any image fails the filter +// rather than handing back bytes that only look like samples. +@(test) +test_apply_filter_bad_predictor_fails :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"Predictor", "Columns"}, {7, 4}) + out, err := apply_filter(doc, "FlateDecode", v_pred_flate[:], parms) + defer delete(out) + testing.expect_value(t, err, Error.Bad_Predictor) + testing.expect(t, out == nil, "bytes returned alongside a predictor failure") +} + +// The pass-through errors hand the input back untouched, which is what lets +// stream_image_data give a JPX codestream to the image layer. +@(test) +test_apply_filter_pass_through_errors :: proc(t: ^testing.T) { + Case :: struct { + name: string, + err: Error, + } + cases := [?]Case{ + {"DCTDecode", .Unsupported_Image}, + {"DCT", .Unsupported_Image}, + {"JBIG2Decode", .Unsupported_Image}, + {"JPXDecode", .Unsupported_Jpx}, + {"NoSuchThing", .Unknown_Filter}, + {"", .Unknown_Filter}, + } + data := [?]byte{1, 2, 3, 4} + for c in cases { + out, err := apply_filter(nil, c.name, data[:], nil) + testing.expectf(t, err == c.err, "%s: got %s", c.name, error_string(err)) + testing.expectf(t, raw_data(out) == raw_data(data[:]), + "%s: did not hand back the input", c.name) + } +} + +// /Crypt is a marker: the bytes were decrypted at parse time. +@(test) +test_apply_filter_crypt_is_identity :: proc(t: ^testing.T) { + data := [?]byte{9, 8, 7} + out, err := apply_filter(nil, "Crypt", data[:], nil) + defer delete(out) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(out, data[:]), "got %v", out) + testing.expect(t, raw_data(out) != raw_data(data[:]), "Crypt aliased its input") +} + +@(test) +test_apply_filter_ccitt_decodes_rows :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // Group 4, one row of 8 columns, all white: horizontal mode (001), a white + // run of 8 (10011) and a black run of 0 (0000110111), zero padded. + encoded := [?]byte{0b0011_0011, 0b0000_1101, 0b1100_0000} + + // With /BlackIs1 false -- the default -- a white pixel is a 1 bit. + parms := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 8, 1}) + out, err := apply_filter(doc, "CCF", encoded[:], parms) + defer delete(out) + testing.expectf(t, err == .None, "%s", error_string(err)) + testing.expectf(t, len(out) == 1 && out[0] == 0xff, "got %x", out) + + // /BlackIs1 exchanges them. + inverted := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 8, 1}) + dict_set(inverted, "BlackIs1", true) + out2, err2 := apply_filter(doc, "CCITTFaxDecode", encoded[:], inverted) + defer delete(out2) + testing.expectf(t, err2 == .None, "%s", error_string(err2)) + testing.expectf(t, len(out2) == 1 && out2[0] == 0x00, "got %x", out2) +} + +// --------------------------------------------------------------------------- +// stream_data: the chain +// --------------------------------------------------------------------------- + +@(test) +test_stream_data_two_filter_chain :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "FlateDecode"}), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) + testing.expect_value(t, s.state, Stream_Decode_State.Decoded) + testing.expect_value(t, len(doc.repairs), 0) +} + +@(test) +test_stream_data_abbreviated_names :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) +} + +// Inline images abbreviate the keys as well as the filter names +// (ISO 32000-2, table 91). +@(test) +test_stream_data_inline_image_keys :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4}) + s := ft_stream(doc, v_pred_flate[:], Name("Fl"), parms, "F", "DP") + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v", got) +} + +// A single name is as legal as a one-element array. +@(test) +test_stream_data_single_name_filter :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:], Name("FlateDecode"), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) +} + +// /DecodeParms is positional, and a null stands for "this filter has none". +@(test) +test_stream_data_decode_parms_array_with_a_null :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := new_array(doc.allocator) + array_push(parms, nil) + array_push(parms, ft_parms(doc, {"Predictor", "Columns"}, {12, 4})) + + // ASCII85 armour over the flate stream that needs the predictor. + armoured := [?]byte{ + 0x47, 0x68, 0x51, 0x59, 0x3a, 0x41, 0x37, 0x2f, 0x65, 0x51, 0x21, 0x62, 0x2b, 0x56, 0x51, + 0x21, 0x3c, 0x3c, 0x2b, 0x2d, 0x21, 0x22, 0x26, 0x7e, 0x3e, + } + s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "Fl"}), parms) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v, want %v", got, v_pred_payload) + testing.expect_value(t, len(doc.repairs), 0) +} + +// A short /DecodeParms array is padded with nulls rather than treated as a +// disagreement worth failing over. +@(test) +test_stream_data_short_decode_parms_array :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := new_array(doc.allocator) + array_push(parms, nil) + + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), parms) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) +} + +// A bare dictionary against a chain is malformed, but it can only have been +// meant for the filter that reads parameters -- here the flate, not the +// ASCII85 armour in front of it. +@(test) +test_stream_data_single_parms_dict_for_a_chain :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4}) + armoured := [?]byte{ + 0x47, 0x68, 0x51, 0x59, 0x3a, 0x41, 0x37, 0x2f, 0x65, 0x51, 0x21, 0x62, 0x2b, 0x56, 0x51, + 0x21, 0x3c, 0x3c, 0x2b, 0x2d, 0x21, 0x22, 0x26, 0x7e, 0x3e, + } + s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "Fl"}), parms) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v, want %v", got, v_pred_payload) +} + +// A stray null inside /Filter loses that entry, not the stream. +@(test) +test_stream_data_null_inside_the_filter_array :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + filters := new_array(doc.allocator) + array_push(filters, nil) + array_push(filters, Name("FlateDecode")) + + s := ft_stream(doc, v_chain_flate[:], filters, nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) +} + +// --------------------------------------------------------------------------- +// stream_data: degrading +// --------------------------------------------------------------------------- + +@(test) +test_stream_data_unknown_filter_degrades :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:], Name("SuperZipDecode"), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, got == nil, "an unknown filter produced bytes") + testing.expect_value(t, s.state, Stream_Decode_State.Failed) + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) + testing.expect(t, doc.is_damaged, "an unknown filter did not mark the document damaged") + + // And the failure is cached too: asking again is free and silent. + again, err2 := stream_data(doc, s) + testing.expect_value(t, err2, Error.None) + testing.expect(t, again == nil, "second call produced bytes") + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) +} + +// An unknown filter anywhere in the chain empties the stream: the bytes that +// did decode are still encoded as far as the next stage is concerned, and +// handing a content parser half-encoded data is worse than handing it nothing. +@(test) +test_stream_data_unknown_filter_late_in_the_chain :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "SuperZipDecode"}), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, got == nil, "produced bytes past an unknown filter") + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) +} + +// JPX and JBIG2 belong to the image layer. Reaching them through stream_data +// is a caller mistake, not a document defect worth an error. +@(test) +test_stream_data_image_filters_degrade :: proc(t: ^testing.T) { + for name in ([?]string{"DCTDecode", "JPXDecode", "JBIG2Decode"}) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:], Name(name), nil) + got, err := stream_data(doc, s) + testing.expectf(t, err == .None, "%s: %s", name, error_string(err)) + testing.expectf(t, got == nil, "%s: produced bytes", name) + testing.expectf(t, ft_repairs(doc, .Bad_Filter_Data) == 1, "%s: no repair recorded", name) + } +} + +@(test) +test_stream_data_broken_flate_degrades :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + junk := [?]byte{0xff, 0xff, 0xff, 0xff} + s := ft_stream(doc, junk[:], Name("FlateDecode"), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, got == nil, "produced bytes from junk") + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) +} + +// A stream truncated part-way keeps what inflated. Half a content stream draws +// half a page, which beats a blank one. +@(test) +test_stream_data_truncated_flate_keeps_what_it_got :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:len(v_chain_flate) - 6], Name("FlateDecode"), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, len(got) > 0, "a truncated stream decoded to nothing") + plain := V_CHAIN_PLAIN + testing.expectf(t, len(got) <= len(plain) && plain[:len(got)] == string(got), + "recovered prefix disagrees: %q", string(got)) +} + +// A chain this long is not a document. +@(test) +test_stream_data_absurdly_long_chain :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + filters := new_array(doc.allocator) + for _ in 0 ..< MAX_FILTER_CHAIN + 1 { + array_push(filters, Name("FlateDecode")) + } + s := ft_stream(doc, v_chain_flate[:], filters, nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, got == nil, "a 17-filter chain produced bytes") + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) +} + +// --------------------------------------------------------------------------- +// stream_data: caching and borrowing +// --------------------------------------------------------------------------- + +@(test) +test_stream_data_caches_the_decode :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), nil) + first, err1 := stream_data(doc, s) + second, err2 := stream_data(doc, s) + testing.expect_value(t, err1, Error.None) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, string(first), V_CHAIN_PLAIN) + // Same pointer, not merely the same bytes: a second decode would land + // somewhere else in the arena. + testing.expect(t, raw_data(first) == raw_data(second), "the decode was not cached") + testing.expect(t, raw_data(s.decoded) == raw_data(first), "the cache holds something else") +} + +// A stream with no filters is its own decoded form; copying every content +// stream into the arena would double the memory a document costs for nothing. +@(test) +test_stream_data_without_filters_borrows_the_raw_bytes :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + raw := [?]byte{'B', 'T', ' ', 'E', 'T'} + s := ft_stream(doc, raw[:], nil, nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect(t, raw_data(got) == raw_data(raw[:]), "the raw bytes were copied") + testing.expect_value(t, s.state, Stream_Decode_State.Decoded) +} + +// An empty payload is empty whatever the chain claims. Running flate over it +// would manufacture a repair for a stream that is legitimately empty. +@(test) +test_stream_data_empty_payload_is_not_a_repair :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, nil, Name("FlateDecode"), nil) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 0) + testing.expect_value(t, s.state, Stream_Decode_State.Decoded) + testing.expect_value(t, len(doc.repairs), 0) +} + +@(test) +test_stream_data_nil_arguments :: proc(t: ^testing.T) { + got, err := stream_data(nil, nil) + testing.expect_value(t, err, Error.Invalid_Argument) + testing.expect(t, got == nil, "bytes from a nil stream") + + // A nil Document is legal: the standalone object parser has none. The + // decoded bytes then come from the context allocator and are the caller's. + d := new_dict(context.allocator) + dict_set(d, "Filter", Name("FlateDecode")) + defer { + delete(d^) + free(d) + } + s := new_stream(d, v_chain_flate[:], context.allocator) + defer free(s) + got2, err2 := stream_data(nil, s) + defer delete(got2) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, string(got2), V_CHAIN_PLAIN) + + testing.expect(t, stream_data_raw(nil, nil) == nil, "raw bytes from a nil stream") + testing.expect(t, raw_data(stream_data_raw(nil, s)) == raw_data(v_chain_flate[:]), + "stream_data_raw did not hand back the stored bytes") +} + +// --------------------------------------------------------------------------- +// stream_image_data +// --------------------------------------------------------------------------- + +@(test) +test_stream_image_data_stops_at_the_image_filter :: proc(t: ^testing.T) { + names := [?]string{"DCTDecode", "DCT", "JPXDecode", "JBIG2Decode", "CCITTFaxDecode", "CCF"} + for name in names { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + payload := [?]byte{0xff, 0xd8, 0xff, 0xe0} + s := ft_stream(doc, payload[:], Name(name), nil) + data, filter, parms, err := stream_image_data(doc, s) + defer delete(data) + testing.expectf(t, err == .None, "%s: %s", name, error_string(err)) + testing.expectf(t, slice.equal(data, payload[:]), "%s: payload changed", name) + testing.expectf(t, filter == filter_name(filter_kind(name), name), + "%s: reported %q", name, filter) + testing.expectf(t, parms == nil, "%s: invented parameters", name) + // Caller-owned, so never the stream's own bytes. + testing.expectf(t, raw_data(data) != raw_data(payload[:]), + "%s: aliased the raw bytes", name) + } +} + +// The armour comes off; the image codec's own bytes are handed over intact. +@(test) +test_stream_image_data_applies_the_byte_filters_first :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // ASCII85 for the four bytes ff d8 ff e0. + armoured := [?]byte{'s', '4', 'I', 'A', '0', '~', '>'} + s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "DCTDecode"}), nil) + data, filter, _, err := stream_image_data(doc, s) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, filter, "DCTDecode") + want := [?]byte{0xff, 0xd8, 0xff, 0xe0} + testing.expectf(t, slice.equal(data, want[:]), "got %x, want %x", data, want) +} + +// The parameters that go with the image filter are handed over, since the +// image layer is the one that has to act on them. +@(test) +test_stream_image_data_returns_the_image_parameters :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + parms := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 16, 4}) + payload := [?]byte{0x00, 0x01} + s := ft_stream(doc, payload[:], Name("CCF"), parms) + data, filter, got_parms, err := stream_image_data(doc, s) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, filter, "CCITTFaxDecode") + testing.expect(t, got_parms == parms, "the parameter dictionary did not come back") + + p := filter_ccitt_params(doc, got_parms) + testing.expect_value(t, p.k, -1) + testing.expect_value(t, p.columns, 16) + testing.expect_value(t, p.rows, 4) + testing.expect_value(t, p.end_of_block, true) +} + +// No image filter at all: the samples are already bytes. +@(test) +test_stream_image_data_without_an_image_filter :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:], Name("FlateDecode"), nil) + data, filter, parms, err := stream_image_data(doc, s) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, filter, "") + testing.expect(t, parms == nil, "invented parameters") + testing.expect_value(t, string(data), V_CHAIN_PLAIN) +} + +// An unfiltered image: still caller-owned memory, so the caller can free it +// without knowing how it got there. +@(test) +test_stream_image_data_unfiltered :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + payload := [?]byte{1, 2, 3, 4, 5} + s := ft_stream(doc, payload[:], nil, nil) + data, filter, _, err := stream_image_data(doc, s) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, filter, "") + testing.expectf(t, slice.equal(data, payload[:]), "got %v", data) + testing.expect(t, raw_data(data) != raw_data(payload[:]), "aliased the raw bytes") +} + +@(test) +test_stream_image_data_broken_chain :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + s := ft_stream(doc, v_chain_flate[:], ft_names(doc, {"SuperZipDecode", "DCTDecode"}), nil) + data, filter, _, err := stream_image_data(doc, s) + defer delete(data) + testing.expect_value(t, err, Error.Unknown_Filter) + testing.expect(t, data == nil, "bytes past an unknown filter") + testing.expect_value(t, filter, "") + testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1) +} + +@(test) +test_stream_image_data_nil_stream :: proc(t: ^testing.T) { + data, filter, parms, err := stream_image_data(nil, nil) + testing.expect_value(t, err, Error.Invalid_Argument) + testing.expect(t, data == nil && parms == nil && filter == "", "something came back") +} + +// --------------------------------------------------------------------------- +// Malformed input +// --------------------------------------------------------------------------- + +// Every filter, over payloads of every shape, with parameters that make no +// sense: nothing here may panic, hang or leak. +@(test) +test_filters_never_crash_on_nonsense :: proc(t: ^testing.T) { + names := [?]string{ + "FlateDecode", "LZWDecode", "ASCII85Decode", "ASCIIHexDecode", "RunLengthDecode", + "CCITTFaxDecode", "DCTDecode", "JPXDecode", "JBIG2Decode", "Crypt", "Nonsense", "", + } + payloads := [?][]byte{ + nil, + {0x00}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {'~', '>'}, + {'>', '>', '>'}, + {0x78, 0xda}, + v_chain_flate[:], + v_chain_a85[:], + v_lzw_tiff[:], + } + parm_sets := [?][2]i64{{1, 1}, {2, 0}, {12, 4}, {15, 1 << 40}, {-1, -1}, {99, 7}} + + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + for name in names { + for payload in payloads { + for set in parm_sets { + parms := ft_parms(doc, + {"Predictor", "Columns", "Colors", "BitsPerComponent", "K", "Rows"}, + {set[0], set[1], set[0], set[1], set[0], set[1]}) + s := ft_stream(doc, payload, Name(name), parms) + data, derr := stream_data(doc, s) + testing.expectf(t, derr == .None || derr == .Invalid_Argument, + "%s: stream_data returned %s", name, error_string(derr)) + _ = data + + s2 := ft_stream(doc, payload, Name(name), parms) + img, _, _, _ := stream_image_data(doc, s2) + delete(img) + } + } + } +} + +// A /Filter that is not a name or an array of names is not a filter. +@(test) +test_stream_data_nonsense_filter_entry :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + raw := [?]byte{'x', 'y', 'z'} + entries := [?]Object{i64(42), f64(1.5), true, String(transmute([]byte)string("Fl"))} + for entry in entries { + s := ft_stream(doc, raw[:], entry, nil) + got, err := stream_data(doc, s) + testing.expectf(t, err == .None, "%v: %s", entry, error_string(err)) + testing.expectf(t, slice.equal(got, raw[:]), "%v: data changed", entry) + } +} + +// --------------------------------------------------------------------------- +// Allocator discipline +// +// Two rules, both invisible from the outside and both easy to break again. +// A filter chain must free its intermediates -- with no Document there is +// nobody else to. And a codec's speculation must not reach the Document arena, +// where `delete` is a silent no-op and nothing is reclaimed before `close`. +// The tests below are the only thing standing between either rule and a +// regression, so they measure rather than inspect: the tracking allocator the +// test runner installs catches the first, `document_memory_used` the second. +// --------------------------------------------------------------------------- + +// "Hi" run through ASCIIHexDecode twice and three times over. Decoding applies +// the chain in the order /Filter lists it (ISO 32000-2, 7.4.1), so each pass +// peels one layer of hex off and every pass but the last leaves a buffer +// behind that only `stream_data` can free. +@(private = "file") +V_HEX_TWICE :: "34383639" +@(private = "file") +V_HEX_THRICE :: "3334333833363339" + +@(test) +test_stream_data_nil_document_frees_the_intermediates :: proc(t: ^testing.T) { + cases := [?]string{V_HEX_TWICE, V_HEX_THRICE} + for raw, i in cases { + depth := i + 2 + + // A nil Document means no arena, so the chain runs on the context + // allocator and every buffer it produces is somebody's to free. + d := new_dict(context.allocator) + a := new_array(context.allocator) + for _ in 0 ..< depth { + array_push(a, Name("ASCIIHexDecode")) + } + dict_set(d, "Filter", a) + s := new_stream(d, transmute([]byte)raw, context.allocator) + + got, err := stream_data(nil, s) + testing.expectf(t, err == .None, "%d filters: %s", depth, error_string(err)) + testing.expectf(t, string(got) == "Hi", "%d filters: got %q", depth, string(got)) + testing.expectf(t, s.state == .Decoded, "%d filters: state %v", depth, s.state) + + // Only the last stage is reachable here; if the earlier ones were not + // freed as the chain advanced, nothing can free them now and the + // tracking allocator reports the leak. + delete(got) + delete(a.items) + free(a) + delete(d^) + free(d) + free(s) + } +} + +// The same chain over a real Document: the intermediates must not merely be +// freed, they must never have been taken from the arena in the first place, +// because an arena cannot give them back. +@(test) +test_stream_data_leaves_only_the_last_stage_in_the_arena :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // /ASCII85Decode over /FlateDecode: the armour comes off into a buffer + // that is 28 bytes of deflate, which inflates into the 20 bytes that are + // kept. Only the 20 may be charged to the arena. + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "FlateDecode"}), nil) + before := document_memory_used(doc) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(got), V_CHAIN_PLAIN) + + growth := document_memory_used(doc) - before + testing.expectf(t, growth <= len(got) + FT_ARENA_SLACK, + "the arena grew by %d bytes for a %d byte stream", growth, len(got)) + + // Decoding twice must not decode twice, let alone allocate twice. + settled := document_memory_used(doc) + again, err2 := stream_data(doc, s) + testing.expect_value(t, err2, Error.None) + testing.expect(t, raw_data(again) == raw_data(got), "the cache was not used") + testing.expect_value(t, document_memory_used(doc), settled) +} + +// An arena rounds each allocation up to its alignment, so an exact byte count +// would be a test of core:mem/virtual rather than of this file. The point is +// that growth tracks the output and not the number of buffers it took to +// produce it, and one alignment quantum of slack does not blunt that. +@(private) +FT_ARENA_SLACK :: 64 + +// apply_filter is where a codec's guesswork stops. Every rejected attempt and +// every superseded growth buffer belongs to the scratch allocator; exactly one +// buffer, at its final length, is charged to the caller's. +@(test) +test_apply_filter_charges_the_arena_for_one_buffer :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + before := document_memory_used(doc) + out, err := apply_filter(doc, "FlateDecode", v_chain_flate[:], nil, doc.allocator) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(out), V_CHAIN_PLAIN) + + growth := document_memory_used(doc) - before + testing.expectf(t, growth <= len(out) + FT_ARENA_SLACK, + "the arena grew by %d bytes for %d bytes of output", growth, len(out)) +} + +// A filter that fails hands back nil or the input untouched, and either way +// must not have spent a byte of the arena on the attempt. +@(test) +test_apply_filter_failure_costs_the_arena_nothing :: proc(t: ^testing.T) { + broken := [?]byte{0x78, 0x9c, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07} + cases := [?]struct{name: string, data: []byte, want: Error}{ + {"FlateDecode", broken[:], .Bad_Flate_Data}, + {"DCTDecode", broken[:], .Unsupported_Image}, + {"JPXDecode", broken[:], .Unsupported_Jpx}, + {"NoSuchThing", broken[:], .Unknown_Filter}, + } + for c in cases { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + before := document_memory_used(doc) + out, err := apply_filter(doc, c.name, c.data, nil, doc.allocator) + testing.expectf(t, err == c.want, "%s: %s", c.name, error_string(err)) + testing.expectf(t, document_memory_used(doc) == before, + "%s: %d bytes stranded in the arena by a filter that returned an error", + c.name, document_memory_used(doc) - before) + // The pass-through errors hand back the caller's own bytes, which + // means the result is never something to free. + if err != .Bad_Flate_Data { + testing.expectf(t, raw_data(out) == raw_data(c.data), + "%s: the input was copied on an error path", c.name) + } + } +} + +// The chain runs on scratch even when it is one filter long, so a filter that +// fails half way through a chain leaves nothing behind either. +@(test) +test_stream_data_failed_chain_costs_the_arena_nothing :: proc(t: ^testing.T) { + doc := filter_test_doc() + defer filter_test_doc_free(doc) + + // A85 decodes to eight bytes that JPXDecode will refuse, and refusing is + // what ends the chain. + s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "JPXDecode"}), nil) + before := document_memory_used(doc) + got, err := stream_data(doc, s) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(got), 0) + testing.expect_value(t, s.state, Stream_Decode_State.Failed) + testing.expectf(t, ft_repairs(doc, .Bad_Filter_Data) == 1, "no repair was recorded") + + growth := document_memory_used(doc) - before + // The repair itself is charged to the arena, which is the only thing that + // should be. + testing.expectf(t, growth <= size_of(Repair) * 8 + FT_ARENA_SLACK, + "the arena grew by %d bytes for a chain that decoded to nothing", growth) +} diff --git a/font.odin b/font.odin new file mode 100644 index 0000000..4fcfe10 --- /dev/null +++ b/font.odin @@ -0,0 +1,1463 @@ +package pdf + +import "core:mem" +import "core:slice" +import "core:unicode/utf8" + +// Fonts (ISO 32000-2, 9.5 - 9.10). +// +// A Font ties a PDF font dictionary to a glyph source. The three font-program +// readers (font_cff.odin, font_truetype.odin, font_type1.odin) know nothing +// about PDF: they turn bytes into outlines and are separately testable. This +// file is where the format's encoding, width and CID machinery lives. +// +// A font that fails to load still produces a Font with widths, so that text +// lays out and extracts correctly even when no glyph can be drawn. Missing +// glyphs draw nothing; they never abort a page. +// +// THE THREE SHAPES A PDF FONT COMES IN +// +// simple one byte per code, 256 codes, an encoding of glyph NAMES, and +// /Widths indexed from /FirstChar. Type1, MMType1 and TrueType. +// composite a CMap turns a variable-width code into a CID, the descendant +// font turns the CID into a glyph, and /W gives the widths. +// Type3 glyphs are content streams, so this file only records where +// they are; the interpreter draws them. Its /FontMatrix is the +// font's own and is not 1/1000. +// +// Everything a Font points at is allocated from the Document's arena and dies +// with it. Fonts are cached on the Document keyed by the address of their +// dictionary, because a page that sets the same font two hundred times must +// build it once. +// +// WHAT IS PRECOMPUTED AND WHY +// +// A simple font's code -> glyph and code -> width tables are built once, at +// load: 256 entries each, resolved through the whole encoding cascade. Text +// showing then costs one array index per character rather than a walk through +// the encoding, the charset and two cmap subtables. A composite font cannot do +// the same -- its CID space is 65536 wide -- so its lookups stay on the hot +// path and are binary searches. + +// Codes a simple font has, and the width of the two precomputed tables. +@(private) +FONT_CODES :: 256 +// Entries kept from a /W or /W2 array. The arrays are attacker-controlled and +// the `c [w ...]` form costs one entry per width, so both are capped well +// above any real CJK font and far below anything that hurts. +@(private) +MAX_CID_WIDTHS :: 1 << 16 +// Items read from one /W or /W2 array before the walk gives up. Two per +// entry in the range form, plus the array elements themselves. +@(private) +MAX_W_ITEMS :: 1 << 17 +// u16 entries kept from a /CIDToGIDMap stream. CIDs are 16-bit, so this is +// the format's own ceiling rather than an invented one. +@(private) +MAX_CID_TO_GID :: 65536 +// Entries read from a /Differences array. Each is one code, so more than a +// few hundred is either damage or an attack. +@(private) +MAX_DIFFERENCES :: 4096 + +// /MissingWidth and every width from a file are clamped to this in absolute +// value. A width of 1e18 turns a text matrix into an infinity three +// multiplications later, and the geometry below the device line may not see +// one. 1000 is an em; ten thousand ems is already absurd. +@(private) +MAX_GLYPH_WIDTH :: 10_000_000.0 + +Font_Kind :: enum u8 { + Type1, + TrueType, + Type3, + Type0, // composite; the descendant carries the program +} + +// Cid_Width is one run of the /W array: CIDs `lo` through `hi` are `width` +// wide, in glyph space. +Cid_Width :: struct { + lo, hi: u32, + width: f64, +} + +// Cid_Vmetric is one run of the /W2 array: CIDs `lo` through `hi` advance by +// `w1y` vertically and have their origin displaced by (`v1x`, `v1y`) from the +// horizontal origin. ISO 32000-2, 9.7.4.3. +Cid_Vmetric :: struct { + lo, hi: u32, + w1y: f64, + v1x, v1y: f64, +} + +// Font_Code is one decoded character code, ready to draw and to extract. +Font_Code :: struct { + code: u32, // the raw code taken from the string + cid: u32, // equal to `code` for a simple font + gid: u32, // glyph index in the program; 0 is .notdef + width: f64, // glyph space, so 1/1000 text space for everything but Type3 + unicode: rune, // -1 when the font offers none + nbytes: int, // bytes of the string this code consumed + is_space: bool, // a single-byte code 32: word spacing applies + // Position vector, glyph space: where the glyph's horizontal origin sits + // relative to its vertical one. Zero unless the font is vertical, in which + // case `width` is the VERTICAL displacement w1y (normally negative) and + // this is what moves the glyph off the vertical baseline. 9.7.4.3. + vx, vy: f64, +} + +Font :: struct { + doc: ^Document, + dict: ^Dict, + kind: Font_Kind, + base_font: string, + + // -- simple fonts (9.6) ---------------------------------------------- + first_char: int, + last_char: int, + // /Widths, indexed from first_char, in glyph space. + widths: []f64, + missing_width: f64, + // The resolved glyph name for each code, or "" where the encoding gives + // none. This is the whole of a simple font's encoding after /BaseEncoding + // and /Differences have been applied over the program's built-in one. + enc_names: [256]string, + // Code to glyph and code to width, resolved once at load. See the header. + code_gid: [FONT_CODES]u32, + code_width: [FONT_CODES]f64, + + // -- composite fonts (9.7) ------------------------------------------- + // Code to CID. + cmap: ^Cmap, + // CID to GID: /CIDToGIDMap, empty when it is /Identity. + cid_to_gid: []u16, + cid_widths: []Cid_Width, + default_width: f64, + vertical: bool, + // /DW2 and /W2, for vertical writing. + default_vy: f64, + default_w1: f64, + cid_vmetrics: []Cid_Vmetric, + // The descendant font dictionary, kept for callers that need /CIDSystemInfo. + descendant: ^Dict, + + // -- the glyph program ------------------------------------------------ + cff: ^Cff_Font, + ttf: ^TrueType_Font, + t1: ^Type1_Font, + // Glyph space to text space. 1/1000 for Type1 and CFF, 1/unitsPerEm for + // TrueType, and whatever /FontMatrix says for Type3. + mtx: Matrix, + embedded: bool, + + // -- Type3 (9.6.4) ---------------------------------------------------- + // Type3 glyphs are content streams, so drawing one re-enters the + // interpreter rather than the rasteriser. + char_procs: ^Dict, + t3_resources: ^Dict, + + // -- text extraction -------------------------------------------------- + to_unicode: ^Cmap, + + // -- descriptor flags (9.8.2) ----------------------------------------- + symbolic: bool, + serif: bool, + fixed_pitch: bool, + italic: bool, + // Set when the program could not be read at all; widths still work. + program_failed: bool, + + // The standard-14 metrics this /BaseFont resolves to, or -1 for a font + // that is not one of them. A code with no /Widths entry is measured with + // these before it falls back to /MissingWidth. + std_index: int, +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// font_load builds a Font from a font dictionary. The result belongs to the +// Document and is cached on it, so loading the same dictionary twice is one +// map lookup. +// +// An error comes back only when there is no dictionary at all. Everything +// else degrades: a font program that will not parse leaves `program_failed` +// set and the widths and encodings intact, because a page whose glyphs cannot +// be drawn must still lay its text out and extract it. +font_load :: proc(doc: ^Document, d: ^Dict) -> (^Font, Error) { + if doc == nil || d == nil { + return nil, .Invalid_Argument + } + key := uintptr(rawptr(d)) + if cached, ok := doc.font_cache[key]; ok { + return cached, .None + } + + f := new(Font, doc.allocator) + f.doc = doc + f.dict = d + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + f.std_index = -1 + f.default_width = 1000 + // ISO 32000-2, 9.7.4.3: /DW2 defaults to [880 -1000]. + f.default_vy = 880 + f.default_w1 = -1000 + + // Cached before the body runs. Nothing here recurses into font_load + // today, but a /Resources cycle reaching back to this font would be the + // file's choice, not ours, and this makes it a lookup rather than a loop. + if doc.font_cache == nil { + doc.font_cache = make(map[uintptr]^Font, 16, doc.allocator) + } + doc.font_cache[key] = f + + f.base_font = dict_name(doc, d, "BaseFont") + f.kind = font_kind(doc, d) + if idx, ok := std_font_lookup(f.base_font); ok { + f.std_index = idx + } + + if tu := dict_get(doc, d, "ToUnicode"); tu != nil { + if _, is_stream := tu.(^Stream); is_stream { + f.to_unicode, _ = cmap_load(doc, tu) + } + } + + switch f.kind { + case .Type0: + font_load_type0(doc, f, d) + case .Type3: + font_load_type3(doc, f, d) + case .Type1, .TrueType: + font_load_simple(doc, f, d) + } + return f, .None +} + +// font_matrix maps glyph space to text space. +// +// Every font but Type3 measures glyphs in thousandths of a text space unit, +// whatever the program's own units are -- the readers hand back outlines in +// their own units and this is the matrix that reconciles them. A Type3 font +// carries its own /FontMatrix and may use any units at all. +font_matrix :: proc(f: ^Font) -> Matrix { + if f == nil { + return Matrix{0.001, 0, 0, 0.001, 0, 0} + } + return f.mtx +} + +// font_is_type3 reports whether drawing a glyph means running a content +// stream rather than filling an outline. +font_is_type3 :: proc(f: ^Font) -> bool { + return f != nil && f.kind == .Type3 +} + +// font_glyph_path appends one glyph's outline, in the program's glyph space, +// to `out`. `font_matrix` maps that space to text space. +// +// A font with no usable program, and a Type3 font (whose glyphs are content +// streams), report .No_Glyph. So does a glyph index the program does not +// have. None of those are page-level failures: the caller draws nothing and +// advances the pen. +font_glyph_path :: proc(f: ^Font, gid: u32, out: ^Path) -> Error { + if f == nil || out == nil { + return .Invalid_Argument + } + if f.kind == .Type3 { + return .No_Glyph + } + if gid > 0xFFFF { + // Every one of the three programs indexes glyphs with a u16. + return .No_Glyph + } + g := u16(gid) + // A font may hold two programs at once: an OpenType wrapper whose + // outlines are CFF is parsed by both readers, and only the CFF can draw. + if f.ttf != nil && tt_has_glyf(f.ttf) { + return tt_glyph_path(f.ttf, g, out) + } + if f.cff != nil { + return cff_glyph_path(f.cff, g, out) + } + if f.t1 != nil { + return t1_glyph_path(f.t1, g, out) + } + return .No_Glyph +} + +// font_kind reads /Subtype, and guesses when it is missing or nonsense. +// +// The guess matters: a font dictionary with no /Subtype is malformed but +// perfectly readable, and its other keys say unambiguously what it is. +@(private) +font_kind :: proc(doc: ^Document, d: ^Dict) -> Font_Kind { + switch dict_name(doc, d, "Subtype") { + case "Type0": return .Type0 + case "Type3": return .Type3 + case "TrueType": return .TrueType + case "Type1", "MMType1": return .Type1 + } + if dict_has(d, "DescendantFonts") { return .Type0 } + if dict_has(d, "CharProcs") || dict_has(d, "FontMatrix") { return .Type3 } + return .Type1 +} + +// --------------------------------------------------------------------------- +// Simple fonts (9.6) +// --------------------------------------------------------------------------- + +@(private) +font_load_simple :: proc(doc: ^Document, f: ^Font, d: ^Dict) { + desc := dict_dict(doc, d, "FontDescriptor") + font_read_descriptor(doc, f, desc) + font_load_program(doc, f, desc) + font_set_matrix(f) + font_build_encoding(doc, f, d) + font_read_widths(doc, f, d) + + for code in 0 ..< FONT_CODES { + f.code_gid[code] = font_simple_gid(f, u8(code)) + f.code_width[code] = font_simple_width(f, u8(code)) + } +} + +// font_read_descriptor takes the flags and /MissingWidth off the descriptor. +// +// A font with no descriptor at all is one of the standard 14 (9.8.1 excuses +// them), and the two symbolic ones are symbolic whatever a missing dictionary +// does not say. +@(private) +font_read_descriptor :: proc(doc: ^Document, f: ^Font, desc: ^Dict) { + if desc == nil { + if f.std_index >= 0 { + f.symbolic = std_font_is_symbolic(f.std_index) + } + return + } + flags := u32(dict_int(doc, desc, "Flags", 0)) + // 9.8.2, table 121. Bit positions are 1-based in the table. + f.fixed_pitch = flags & (1 << 0) != 0 + f.serif = flags & (1 << 1) != 0 + f.italic = flags & (1 << 6) != 0 + // Producers set both Symbolic and Nonsymbolic often enough that the pair + // has to be read as a whole: a font that claims both is claiming nothing, + // and a Latin text font is the safer reading of nothing. + f.symbolic = flags & (1 << 2) != 0 && flags & (1 << 5) == 0 + f.missing_width = font_clamp_width(dict_num(doc, desc, "MissingWidth", 0)) +} + +// font_read_widths reads /FirstChar, /LastChar and /Widths. +// +// /LastChar is recorded and is deliberately NOT used to bound the lookup: the +// array's own length is. A /LastChar that disagrees with the array is far +// commoner than an over-long array, and honouring it would throw away widths +// that are sitting right there. +@(private) +font_read_widths :: proc(doc: ^Document, f: ^Font, d: ^Dict) { + f.first_char = int(dict_int(doc, d, "FirstChar", 0)) + f.last_char = int(dict_int(doc, d, "LastChar", -1)) + a := dict_array(doc, d, "Widths") + if a == nil { + return + } + // Only 256 codes exist, so nothing past the 256th entry can ever be + // indexed however long the array is. + n := min(array_len(a), FONT_CODES) + if n <= 0 { + return + } + w := make([]f64, n, doc.allocator) + for i in 0 ..< n { + w[i] = font_clamp_width(array_num(doc, a, i, 0)) + } + f.widths = w +} + +// font_simple_width is the width of one code, in glyph space. +// +// The order is the format's, with one addition. /Widths first; then the +// standard-14 metrics, which are what a font dictionary with no /Widths at +// all is relying on (9.6.2.2 allows omitting them for the fourteen); then +// /MissingWidth, but only when the descriptor actually carries one; and only +// then the embedded program's own advance. The last step is not in the +// format, and is what saves a subset font that shipped no /Widths and no +// descriptor: without it every glyph on the page would advance by zero and +// the line would collapse into one spot. +@(private) +font_simple_width :: proc(f: ^Font, code: u8) -> f64 { + i := int(code) - f.first_char + if i >= 0 && i < len(f.widths) { + return f.widths[i] + } + name := f.enc_names[code] + if f.std_index >= 0 { + if w, ok := std_font_width(f.std_index, name); ok { + return w + } + } + if f.missing_width != 0 { + return f.missing_width + } + if w, ok := font_program_advance(f, f.code_gid[code]); ok { + return w + } + return f.missing_width +} + +// font_program_advance is the advance the embedded program declares for a +// glyph, converted into the 1/1000 units every width outside a Type3 font is +// in. ok is false when there is no program, or when it declares nothing. +@(private) +font_program_advance :: proc(f: ^Font, gid: u32) -> (f64, bool) { + if gid > 0xFFFF { + return 0, false + } + g := u16(gid) + switch { + case f.ttf != nil && tt_has_glyf(f.ttf): + upem := tt_units_per_em(f.ttf) + if upem <= 0 || !is_finite(upem) { + return 0, false + } + a := tt_glyph_advance(f.ttf, g) * 1000 / upem + return font_clamp_width(a), a != 0 + case f.cff != nil: + // A CFF's advances are in charstring units, which its FontMatrix maps + // to text space; scaling by 1000 puts them back in the thousandths + // every PDF width is written in. + a := cff_glyph_advance(f.cff, g) * cff_font_matrix(f.cff).a * 1000 + return font_clamp_width(a), a != 0 + case f.t1 != nil: + a := t1_glyph_advance(f.t1, g) * t1_font_matrix(f.t1).a * 1000 + return font_clamp_width(a), a != 0 + } + return 0, false +} + +// --------------------------------------------------------------------------- +// The encoding of a simple font (9.6.5) +// +// Four sources, each overriding the one before it: +// +// 1. the font program's own built-in encoding, +// 2. the standard-14 built-in encoding, when there is no program, +// 3. /BaseEncoding, or a bare name in /Encoding, +// 4. /Differences. +// +// A non-symbolic font that still has nothing after (2) uses StandardEncoding; +// a symbolic one deliberately does not, because inventing Latin names for a +// dingbat font would map every code to a glyph it does not have. A TrueType +// program contributes nothing to (1) at all: its built-in encoding is a cmap +// keyed by character, not by name, and it is consulted in font_tt_gid rather +// than here. +// --------------------------------------------------------------------------- + +@(private) +font_build_encoding :: proc(doc: ^Document, f: ^Font, d: ^Dict) { + base: ^[256]string + if f.t1 != nil { + base = t1_builtin_encoding(f.t1) + } else if f.cff != nil { + base = cff_builtin_encoding(f.cff) + } + if base == nil && f.std_index >= 0 { + base = std_font_builtin_encoding(f.std_index) + } + + enc := dict_get(doc, d, "Encoding") + named: ^[256]string + diffs: ^Array + #partial switch v in enc { + case Name: + named = encoding_by_name(string(v)) + case ^Dict: + if bn := dict_name(doc, v, "BaseEncoding"); bn != "" { + named = encoding_by_name(bn) + } + diffs = dict_array(doc, v, "Differences") + } + if named != nil { + base = named + } + if base == nil && !f.symbolic { + base = encoding_standard() + } + if base != nil { + f.enc_names = base^ + } + + if diffs == nil { + return + } + // 9.6.5.1: a /Differences array is numbers and names interleaved. A + // number sets the code the next name applies to; each name advances it. + code := 0 + n := min(array_len(diffs), MAX_DIFFERENCES) + for i in 0 ..< n { + #partial switch v in array_get(doc, diffs, i) { + case i64: + code = int(v) + case f64: + // A real where an integer belongs is malformed, and is how at + // least one generator writes "1.0". + if is_finite(v) && v >= -1e6 && v <= 1e6 { + code = int(v) + } + case Name: + if code >= 0 && code < FONT_CODES { + f.enc_names[code] = string(v) + } + code += 1 + } + } +} + +// font_simple_gid resolves one character code to a glyph index. +@(private) +font_simple_gid :: proc(f: ^Font, code: u8) -> u32 { + if f.kind == .Type3 { + // A Type3 glyph is found by name in /CharProcs; there is no program + // to index. The code stands in for a glyph id so that the rest of the + // pipeline has something to carry. + return u32(code) + } + name := f.enc_names[code] + + // A TrueType-shaped program is character-keyed and has its own rules. + if f.ttf != nil { + if g, ok := font_tt_gid(f, code, name); ok { + return g + } + } + if name != "" { + if f.cff != nil { + if g, ok := cff_gid_for_name(f.cff, name); ok { + return u32(g) + } + } + if f.t1 != nil { + if g, ok := t1_gid_for_name(f.t1, name); ok { + return u32(g) + } + } + // The same character often has two names -- "nbspace" and "space", + // "uni0041" and "A" -- and an encoding and a charset that disagree + // about which to use would otherwise lose the glyph entirely. + if r := glyph_name_to_rune(name); r >= 0 { + if alt := rune_to_glyph_name(r); alt != "" && alt != name { + if f.cff != nil { + if g, ok := cff_gid_for_name(f.cff, alt); ok { + return u32(g) + } + } + if f.t1 != nil { + if g, ok := t1_gid_for_name(f.t1, alt); ok { + return u32(g) + } + } + } + } + } + // A CFF's own Encoding maps codes to glyphs directly, which reaches + // glyphs its charset does not name -- the shape a symbolic Type1C subset + // usually has. + if f.cff != nil { + if g, ok := cff_gid_for_code(f.cff, code); ok { + return u32(g) + } + // A CID-keyed CFF embedded as a SIMPLE font's program has neither + // names nor an encoding (CFF 12), so both routes above are dead ends + // and every glyph would be lost. Producers do emit this -- a + // CIDFontType0C reused under a /Type1 dictionary -- and reading the + // code as a CID through the charset is what recovers it. + if f.cff.is_cid { + if g, ok := cff_gid_for_cid(f.cff, u16(code)); ok { + return u32(g) + } + } + } + return 0 +} + +// font_tt_gid is the TrueType half of a simple font's code -> glyph mapping. +// It is the most error-prone rule in PDF text, so it is written out in full. +// +// ISO 32000-2, 9.6.5.4: +// +// A SYMBOLIC font's /Encoding is ignored. The code is looked up in the +// font's (3,0) Microsoft Symbol cmap, whose keys are conventionally 0xF000 +// plus the byte -- but only conventionally, so the bare code is tried too +// (tt_gid_for_symbol does both). Failing that, the (1,0) Macintosh Roman +// cmap is tried with the raw code. This is the rule that makes Wingdings, +// Symbol and every icon-font subset come out right, and applying the +// /Encoding to one of them instead maps every code to a glyph the font does +// not have: the page comes out blank or, worse, full of the wrong glyphs. +// +// A NON-SYMBOLIC font goes the long way round: the code selects a glyph +// NAME through the encoding, and the name is then resolved by three routes +// in turn. Through the AGL to a Unicode value and that value through the +// (3,1) cmap; through the `post` table, which names this font's own glyphs +// and so answers a subsetter's "g42" that the AGL never will; and through +// MacRomanEncoding to a Mac Roman CODE and that code through the (1,0) +// cmap, which is the only route a font carrying nothing but a (1,0) table +// has. All three are indirections, and all three are needed: the cmaps are +// keyed by character or by Mac code, and the /Differences are written in +// names. +// +// Two departures from the letter of that, both of which every viewer makes: +// +// * a code that the symbolic route does not resolve still tries the name +// route, and the other way about. A font's flags are frequently wrong, +// and a fallback that only fires when the primary route found nothing +// cannot make a correct font worse. +// * a font with NO usable cmap at all -- which a CIDFontType2 subset +// re-used as a simple font, and a stripped TrueType, both are -- treats +// the code as a glyph index. It is the only remaining interpretation, and +// it is what the producers of those files expect. +@(private) +font_tt_gid :: proc(f: ^Font, code: u8, name: string) -> (u32, bool) { + tt := f.ttf + if f.symbolic { + if g, ok := tt_gid_for_symbol(tt, code); ok { + return u32(g), true + } + if g, ok := tt_gid_for_mac_code(tt, code); ok { + return u32(g), true + } + } + if name != "" { + if r := glyph_name_to_rune(name); r >= 0 { + if g, ok := tt_gid_for_unicode(tt, r); ok { + return u32(g), true + } + } + if g, ok := tt_gid_for_name(tt, name); ok { + return u32(g), true + } + // The middle route of 9.6.5.4: the name's position in + // MacRomanEncoding is the key the (1,0) cmap is written in. It is the + // only route a Mac-authored or subsetted font -- one cmap, (1,0), and + // a format 3 `post` with no names in it -- has above ASCII, and + // without it such a code falls through to the raw-code fallback + // below, which over 0x80-0xFF returns a real but different glyph: the + // page renders and is silently wrong. + // + // The clause lists this before `post`; it is tried after because + // `post` names a glyph in THIS font directly while this is an + // indirection through a legacy encoding, so when the two disagree the + // font's own names are the better answer. The order only matters when + // both can answer, and this route exists for the case where `post` + // cannot. + if mac_code, ok := encoding_macroman_code(name); ok { + if g, gok := tt_gid_for_mac_code(tt, mac_code); gok { + return u32(g), true + } + } + } + if !f.symbolic { + if g, ok := tt_gid_for_symbol(tt, code); ok { + return u32(g), true + } + if g, ok := tt_gid_for_mac_code(tt, code); ok { + return u32(g), true + } + } + // The code read as a character. Over ASCII every Latin text encoding + // agrees with Unicode, so this is what a font whose /Encoding was omitted + // means -- and it is also the only route left for the very common subset + // that sets the Symbolic flag but ships a (3,1) cmap and nothing else. + if g, ok := tt_gid_for_unicode(tt, rune(code)); ok { + return u32(g), true + } + if len(tt.cmap_unicode) == 0 && len(tt.cmap_symbol) == 0 && len(tt.cmap_mac) == 0 { + if int(code) < tt_glyph_count(tt) { + return u32(code), true + } + } + return 0, false +} + +// --------------------------------------------------------------------------- +// Type3 (9.6.4) +// --------------------------------------------------------------------------- + +// font_load_type3 records where a Type3 font's glyphs are. It does not run +// them: a Type3 glyph is a content stream, and drawing one re-enters the +// interpreter with this font's /Resources in scope. +@(private) +font_load_type3 :: proc(doc: ^Document, f: ^Font, d: ^Dict) { + f.char_procs = dict_dict(doc, d, "CharProcs") + f.t3_resources = dict_dict(doc, d, "Resources") + + // 9.6.4: /FontMatrix is required and is emphatically not 1/1000 -- it is + // how a Type3 font declares its own glyph space, and a font drawn in + // 1-unit coordinates has [0.001 0 0 0.001 0 0] nowhere in it. A matrix + // that is missing, unreadable or singular would make every glyph + // infinite or invisible, so it degrades to the usual thousandths. + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + if a := dict_array(doc, d, "FontMatrix"); array_len(a) >= 6 { + m := Matrix{ + a = array_num(doc, a, 0, 0), b = array_num(doc, a, 1, 0), + c = array_num(doc, a, 2, 0), d = array_num(doc, a, 3, 0), + e = array_num(doc, a, 4, 0), f = array_num(doc, a, 5, 0), + } + if matrix_is_finite(m) && matrix_determinant(m) != 0 { + f.mtx = m + } else { + add_repair(doc, .Bad_Font_Object, 0) + } + } + + font_read_descriptor(doc, f, dict_dict(doc, d, "FontDescriptor")) + font_build_encoding(doc, f, d) + font_read_widths(doc, f, d) + + for code in 0 ..< FONT_CODES { + f.code_gid[code] = u32(code) + // A Type3 width is in the font's own glyph space, so there is no + // standard-14 metric and no program advance to fall back on: a code + // with no /Widths entry is genuinely zero wide. + i := code - f.first_char + f.code_width[code] = i >= 0 && i < len(f.widths) ? f.widths[i] : f.missing_width + } +} + +// font_type3_proc is the content stream that draws one code, or nil. +// +// Not part of the five procedures the contract names; the interpreter needs +// it, and it is a lookup that belongs to the encoding rather than to the +// content stream. +font_type3_proc :: proc(f: ^Font, code: u32) -> ^Stream { + if f == nil || f.kind != .Type3 || f.char_procs == nil || code >= FONT_CODES { + return nil + } + name := f.enc_names[code] + if name == "" { + return nil + } + return dict_stream(f.doc, f.char_procs, name) +} + +// --------------------------------------------------------------------------- +// Composite fonts (9.7) +// --------------------------------------------------------------------------- + +@(private) +font_load_type0 :: proc(doc: ^Document, f: ^Font, d: ^Dict) { + // The encoding CMap turns bytes into CIDs. A Type0 font without one is + // undrawable, so Identity-H stands in: two-byte codes taken as CIDs is + // what the overwhelming majority of real Type0 fonts use anyway. + if enc := dict_get(doc, d, "Encoding"); enc != nil { + cm, err := cmap_load(doc, enc) + if err == .None && cm != nil { + f.cmap = cm + } + } + if f.cmap == nil { + add_repair(doc, .Bad_Font_Object, 0) + f.cmap = cmap_identity(false, doc.allocator) + } + f.vertical = f.cmap != nil && f.cmap.wmode == 1 + + cid := font_descendant(doc, d) + if cid == nil { + // No descendant: nothing maps a CID to a glyph and nothing gives a + // width. The font still decodes text, which is what /ToUnicode needs. + add_repair(doc, .Bad_Font_Object, 0) + return + } + f.descendant = cid + + desc := dict_dict(doc, cid, "FontDescriptor") + font_read_descriptor(doc, f, desc) + font_load_program(doc, f, desc) + font_set_matrix(f) + + f.default_width = font_clamp_width(dict_num(doc, cid, "DW", 1000)) + if a := dict_array(doc, cid, "W"); a != nil { + font_parse_w(doc, f, a) + } + if f.vertical { + if a := dict_array(doc, cid, "DW2"); array_len(a) >= 2 { + vy := array_num(doc, a, 0, 880) + w1 := array_num(doc, a, 1, -1000) + if is_finite(vy) && is_finite(w1) { + f.default_vy = font_clamp_width(vy) + f.default_w1 = font_clamp_width(w1) + } + } + if a := dict_array(doc, cid, "W2"); a != nil { + font_parse_w2(doc, f, a) + } + } + font_load_cid_to_gid(doc, f, cid) +} + +// font_descendant is /DescendantFonts[0]. The format requires a one-element +// array; a producer that writes the dictionary directly means the same thing +// and is common enough to accept. +@(private) +font_descendant :: proc(doc: ^Document, d: ^Dict) -> ^Dict { + o := dict_get(doc, d, "DescendantFonts") + #partial switch v in o { + case ^Dict: + return v + case ^Array: + return array_dict(doc, v, 0) + } + return nil +} + +// font_load_cid_to_gid reads /CIDToGIDMap: /Identity, or a stream of +// big-endian u16 glyph indices indexed by CID (9.7.4.2). +@(private) +font_load_cid_to_gid :: proc(doc: ^Document, f: ^Font, cid: ^Dict) { + o := dict_get(doc, cid, "CIDToGIDMap") + stm, is_stream := o.(^Stream) + if !is_stream { + return + } + data, err := stream_data(doc, stm) + if err != .None || len(data) < 2 { + add_repair(doc, .Bad_Font_Object, 0, stm.ref) + return + } + n := min(len(data) / 2, MAX_CID_TO_GID) + m := make([]u16, n, doc.allocator) + for i in 0 ..< n { + m[i] = u16(data[2 * i]) << 8 | u16(data[2 * i + 1]) + } + f.cid_to_gid = m +} + +// font_cid_gid maps a CID to a glyph index. +@(private) +font_cid_gid :: proc(f: ^Font, cid: u32) -> u32 { + if len(f.cid_to_gid) > 0 { + // A /CIDToGIDMap that stops short is not saying "identity from here + // on"; it is saying those CIDs have no glyph. 9.7.4.2. + if int(cid) < len(f.cid_to_gid) { + return u32(f.cid_to_gid[cid]) + } + return 0 + } + if f.cff != nil { + // A CID-keyed CFF's charset is the CID -> GID map; a name-keyed one + // reached through Identity-H has the CID as its glyph index, which is + // what cff_gid_for_cid answers for it. + if cid > 0xFFFF { + return 0 + } + if g, ok := cff_gid_for_cid(f.cff, u16(cid)); ok { + return u32(g) + } + return 0 + } + return cid +} + +// font_parse_w reads the /W array (9.7.4.3), which has two forms that alternate +// freely inside one array: +// +// c [w1 w2 ... wn] CIDs c through c+n-1 +// c_first c_last w every CID in the range +@(private) +font_parse_w :: proc(doc: ^Document, f: ^Font, a: ^Array) { + out := make([dynamic]Cid_Width, 0, 16, doc.allocator) + i := 0 + n := array_len(a) + fuel := MAX_W_ITEMS + for i < n && fuel > 0 && len(out) < MAX_CID_WIDTHS { + fuel -= 1 + first, ok := font_cid_operand(doc, a, i) + if !ok { + // Not a number where one belongs. Stepping over one item rather + // than abandoning the array keeps the widths after the damage. + i += 1 + continue + } + i += 1 + if i >= n { + break + } + #partial switch v in array_get(doc, a, i) { + case ^Array: + i += 1 + m := array_len(v) + for k in 0 ..< m { + if len(out) >= MAX_CID_WIDTHS { + break + } + fuel -= 1 + if fuel <= 0 { + break + } + c := u64(first) + u64(k) + if c > 0xFFFF { + break + } + font_push_width(&out, u32(c), u32(c), font_clamp_width(array_num(doc, v, k, 0))) + } + case: + last, ok2 := font_cid_operand(doc, a, i) + i += 1 + if i >= n || !ok2 { + break + } + w := font_clamp_width(array_num(doc, a, i, 0)) + i += 1 + lo, hi := first, last + if hi < lo { + // A backwards range is meaningless as written; read either + // way round it names the same glyphs. + lo, hi = hi, lo + } + font_push_width(&out, lo, hi, w) + } + } + if len(out) == 0 { + return + } + f.cid_widths = font_flatten(out[:], doc.allocator) +} + +// font_push_width appends a run, merging it into the previous one when they +// are adjacent and equally wide. The `c [w w w ...]` form is one entry per +// CID as written, and a CJK font writes tens of thousands of them; merging +// turns the common runs of equal widths into single entries and costs one +// comparison. +@(private) +font_push_width :: proc(out: ^[dynamic]Cid_Width, lo, hi: u32, w: f64) { + if len(out) > 0 { + last := &out[len(out) - 1] + if last.width == w && last.hi + 1 == lo && last.hi < hi { + last.hi = hi + return + } + } + append(out, Cid_Width{lo = lo, hi = hi, width = w}) +} + +// font_parse_w2 reads /W2, the vertical counterpart of /W. Its entries are +// triples rather than single numbers: +// +// c [w1y v1x v1y w1y v1x v1y ...] +// c_first c_last w1y v1x v1y +@(private) +font_parse_w2 :: proc(doc: ^Document, f: ^Font, a: ^Array) { + out := make([dynamic]Cid_Vmetric, 0, 8, doc.allocator) + i := 0 + n := array_len(a) + fuel := MAX_W_ITEMS + for i < n && fuel > 0 && len(out) < MAX_CID_WIDTHS { + fuel -= 1 + first, ok := font_cid_operand(doc, a, i) + if !ok { + i += 1 + continue + } + i += 1 + if i >= n { + break + } + #partial switch v in array_get(doc, a, i) { + case ^Array: + i += 1 + m := array_len(v) / 3 + for k in 0 ..< m { + if len(out) >= MAX_CID_WIDTHS { + break + } + fuel -= 1 + if fuel <= 0 { + break + } + c := u64(first) + u64(k) + if c > 0xFFFF { + break + } + append(&out, Cid_Vmetric{ + lo = u32(c), hi = u32(c), + w1y = font_clamp_width(array_num(doc, v, 3 * k, 0)), + v1x = font_clamp_width(array_num(doc, v, 3 * k + 1, 0)), + v1y = font_clamp_width(array_num(doc, v, 3 * k + 2, 0)), + }) + } + case: + last, ok2 := font_cid_operand(doc, a, i) + i += 1 + if !ok2 || i + 2 >= n { + break + } + lo, hi := first, last + if hi < lo { + lo, hi = hi, lo + } + append(&out, Cid_Vmetric{ + lo = lo, hi = hi, + w1y = font_clamp_width(array_num(doc, a, i, 0)), + v1x = font_clamp_width(array_num(doc, a, i + 1, 0)), + v1y = font_clamp_width(array_num(doc, a, i + 2, 0)), + }) + i += 3 + } + } + if len(out) == 0 { + return + } + f.cid_vmetrics = font_flatten(out[:], doc.allocator) +} + +// font_cid_operand reads array element `i` as a CID. Anything that is not a +// number in range fails rather than being coerced: a /W array that has gone +// out of step must not manufacture a range covering the whole CID space. +@(private) +font_cid_operand :: proc(doc: ^Document, a: ^Array, i: int) -> (u32, bool) { + v, ok := to_number(array_get(doc, a, i)) + if !ok || !is_finite(v) || v < 0 || v > 0xFFFF { + return 0, false + } + return u32(v), true +} + +// font_flatten turns a set of CID runs into an equivalent set that is sorted +// and disjoint, so that a lookup is a binary search and nothing else. +// +// /W and /W2 are meant to name disjoint ranges, and real files overlap them: +// the shape it always takes is a broad run with narrower corrections written +// over parts of it. The rule applied here is that where two runs share a CID, +// the one that STARTS LATER owns it -- the correction is always the later +// start -- and the earlier run keeps what is left, which may be a piece on +// each side of the correction. +// +// The sweep below is a stack of the runs covering the CID it has reached, most +// recently started on top. That top is the run that owns the CIDs from here +// until either it ends or a new run starts, which is exactly one output entry; +// runs that ended while they were shadowed are dropped from the stack when +// they surface. Two runs that start at the same CID are a contradiction the +// file has to answer for, and which of them wins is not defined. +// +// Doing this once, at load, is what lets the lookup be exact. Resolving the +// overlap at lookup time instead means scanning backwards from where the +// binary search lands, and the distance is unbounded: a file that writes one +// run over the whole CID space and then a hundred corrections inside it puts +// the answer a hundred entries back. Either that scan is bounded and sometimes +// wrong, or it is unbounded and sometimes very slow. This is neither. +// +// The result holds at most twice as many runs as it was given: every entry +// emitted ends either at some run's `hi` or at some run's `lo`, and there are +// only so many of each. +@(private) +font_flatten :: proc(src: []$T, allocator: mem.Allocator) -> []T { + if len(src) <= 1 { + return src + } + slice.sort_by(src, proc(x, y: T) -> bool { + return x.lo < y.lo + }) + + out := make([dynamic]T, 0, len(src) + 1, allocator) + // Scratch, and freed here: the depth is the file's to choose and the + // Document's arena never gives anything back. + stack := make([dynamic]T, 0, 16, context.allocator) + defer delete(stack) + + cur: u32 = 0 + for e in src { + if e.hi < e.lo { + continue + } + if e.lo > cur { + font_flatten_emit(&out, &stack, &cur, e.lo - 1) + } + append(&stack, e) + } + // Each pass either pops a run or emits one, and the counts of both are + // bounded by the input, so the drain has fuel rather than a proof that a + // `hi` of 0xFFFFFFFF cannot wrap `cur` back to zero. + for fuel := 2 * len(src) + 2; fuel > 0 && len(stack) > 0; fuel -= 1 { + top := stack[len(stack) - 1] + if top.hi < cur { + pop(&stack) + continue + } + font_flatten_emit(&out, &stack, &cur, top.hi) + } + return out[:] +} + +// font_flatten_emit writes out the CIDs from `cur` through `limit`, taking +// each one's value from the innermost run still covering it, and leaves `cur` +// just past `limit`. CIDs no run covers produce no entry at all, which is what +// makes the caller fall back to /DW. +// +// Every iteration either pops the stack or reaches `limit`, so the loop is +// bounded by the stack's depth without needing a counter to say so. +@(private) +font_flatten_emit :: proc(out, stack: ^[dynamic]$T, cur: ^u32, limit: u32) { + for cur^ <= limit { + if len(stack) > 0 && stack[len(stack) - 1].hi < cur^ { + pop(stack) + continue + } + if len(stack) == 0 { + break + } + e := stack[len(stack) - 1] + e.lo = cur^ + e.hi = min(e.hi, limit) + append(out, e) + cur^ = e.hi + 1 + } + cur^ = max(cur^, limit + 1) +} + +// font_find_range is a binary search over disjoint, sorted runs. +@(private) +font_find_range :: proc(a: []$T, cid: u32) -> (int, bool) { + lo, hi := 0, len(a) + for lo < hi { + mid := lo + (hi - lo) / 2 + if a[mid].lo <= cid { + lo = mid + 1 + } else { + hi = mid + } + } + if lo > 0 && cid <= a[lo - 1].hi { + return lo - 1, true + } + return 0, false +} + +// font_cid_width is the horizontal advance of one CID, in glyph space. +@(private) +font_cid_width :: proc(f: ^Font, cid: u32) -> f64 { + if i, ok := font_find_range(f.cid_widths, cid); ok { + return f.cid_widths[i].width + } + return f.default_width +} + +// font_cid_vmetric is the vertical displacement and position vector of one +// CID. `w0` is the horizontal width, which the default position vector needs. +@(private) +font_cid_vmetric :: proc(f: ^Font, cid: u32, w0: f64) -> (w1y, vx, vy: f64) { + if i, ok := font_find_range(f.cid_vmetrics, cid); ok { + e := f.cid_vmetrics[i] + return e.w1y, e.v1x, e.v1y + } + // 9.7.4.3: with no /W2 entry the position vector is (w0/2, DW2[0]) and + // the displacement is (0, DW2[1]). + return f.default_w1, w0 / 2, f.default_vy +} + +// --------------------------------------------------------------------------- +// Embedded font programs (9.9) +// --------------------------------------------------------------------------- + +// font_load_program reads /FontFile, /FontFile2 or /FontFile3 out of the +// descriptor. +// +// A program that will not parse is recorded and dropped: `program_failed` is +// set, no error is returned, and the font goes on to build its widths and its +// encoding as if it had never had a program. Text that cannot be drawn must +// still lay out and extract, and a subsetter that truncated its own font file +// is not a reason to lose the page. +@(private) +font_load_program :: proc(doc: ^Document, f: ^Font, desc: ^Dict) { + if desc == nil { + return + } + kind := Font_Kind.Type1 + stm := dict_stream(doc, desc, "FontFile") + if stm == nil { + if s2 := dict_stream(doc, desc, "FontFile2"); s2 != nil { + stm = s2 + kind = .TrueType + } + } + sub := "" + if stm == nil { + if s3 := dict_stream(doc, desc, "FontFile3"); s3 != nil { + stm = s3 + sub = dict_name(doc, s3.dict, "Subtype") + // /OpenType is an sfnt wrapper whose outlines may be either kind; + // the sniff below settles it from the bytes. + kind = sub == "OpenType" ? .TrueType : .Type1 + } + } + if stm == nil { + return + } + + data, err := stream_data(doc, stm) + if err != .None || len(data) == 0 { + f.program_failed = true + add_repair(doc, .Bad_Font_Object, 0, stm.ref) + return + } + if font_parse_program(doc, f, data, kind, sub) { + f.embedded = true + return + } + f.program_failed = true + add_repair(doc, .Bad_Font_Object, 0, stm.ref) +} + +// font_parse_program turns the bytes of an embedded program into a reader. +// +// The declared subtype is a hint, not a fact. /FontFile3 /Type1C files that +// are really OpenType, /FontFile2 entries holding a bare CFF and /FontFile +// entries holding a PFB are all in circulation, and every one of them is +// unambiguous from its first four bytes -- so the magic decides and the +// dictionary only breaks ties. +@(private) +font_parse_program :: proc(doc: ^Document, f: ^Font, data: []byte, kind: Font_Kind, sub: string) -> bool { + switch { + case font_is_sfnt(data): + ttf, err := tt_parse(data, doc.allocator) + if err != .None || ttf == nil { + return false + } + f.ttf = ttf + if tt_has_glyf(ttf) { + return true + } + // An OpenType font with no `glyf` keeps its outlines in a `CFF ` + // table. The sfnt wrapper still carries the cmap and hmtx, so both + // readers stay live: the TrueType one answers character lookups and + // the CFF one draws. + cff_data := tt_cff_data(ttf) + if len(cff_data) == 0 { + return false + } + cff, cerr := cff_parse(cff_data, doc.allocator) + if cerr != .None || cff == nil { + return false + } + f.cff = cff + return true + + case font_is_cff(data): + cff, err := cff_parse(data, doc.allocator) + if err != .None || cff == nil { + return false + } + f.cff = cff + return true + + case font_is_type1(data): + t1, err := t1_parse(data, doc.allocator) + if err != .None || t1 == nil { + return false + } + f.t1 = t1 + return true + } + + // No magic matched. Believe the dictionary, which is all that is left. + if kind == .TrueType || sub == "OpenType" { + ttf, err := tt_parse(data, doc.allocator) + if err == .None && ttf != nil { + f.ttf = ttf + return true + } + return false + } + if sub == "Type1C" || sub == "CIDFontType0C" { + cff, err := cff_parse(data, doc.allocator) + if err == .None && cff != nil { + f.cff = cff + return true + } + return false + } + t1, err := t1_parse(data, doc.allocator) + if err == .None && t1 != nil { + f.t1 = t1 + return true + } + return false +} + +@(private) +font_is_sfnt :: proc(data: []byte) -> bool { + if len(data) < 4 { + return false + } + tag := u32(data[0]) << 24 | u32(data[1]) << 16 | u32(data[2]) << 8 | u32(data[3]) + return tag == TT_TAG_V1 || tag == TT_TAG_V2 || tag == TT_TAG_TRUE || + tag == TT_TAG_OTTO || tag == TT_TAG_TTCF +} + +// font_is_cff matches a bare CFF header: major version 1, minor 0, and a +// header size that is at least the four bytes of the header itself. The +// offSize byte that follows must be 1..4. CFF 6. +@(private) +font_is_cff :: proc(data: []byte) -> bool { + return len(data) >= 4 && data[0] == 1 && data[1] == 0 && + data[2] >= 4 && data[3] >= 1 && data[3] <= 4 +} + +// font_is_type1 matches the two containers a Type1 program arrives in: a PFB +// segment header (0x80 then a segment type) or the "%!" of a PFA/PostScript +// clear-text header. +@(private) +font_is_type1 :: proc(data: []byte) -> bool { + if len(data) < 2 { + return false + } + if data[0] == 0x80 && data[1] >= 1 && data[1] <= 3 { + return true + } + return data[0] == '%' && data[1] == '!' +} + +// font_set_matrix fixes the glyph space the program's outlines are in. +@(private) +font_set_matrix :: proc(f: ^Font) { + switch { + // A CFF carries its own matrix and it wins wherever there is one: an + // OpenType wrapper leaves both readers live, and it is the CFF that draws. + case f.cff != nil: + m := cff_font_matrix(f.cff) + f.mtx = matrix_is_finite(m) && matrix_determinant(m) != 0 ? m : Matrix{0.001, 0, 0, 0.001, 0, 0} + case f.ttf != nil: + upem := tt_units_per_em(f.ttf) + if upem <= 0 || !is_finite(upem) { + upem = 1000 + } + f.mtx = Matrix{1 / upem, 0, 0, 1 / upem, 0, 0} + case f.t1 != nil: + m := t1_font_matrix(f.t1) + f.mtx = matrix_is_finite(m) && matrix_determinant(m) != 0 ? m : Matrix{0.001, 0, 0, 0.001, 0, 0} + case: + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + } +} + +// --------------------------------------------------------------------------- +// Decoding a string +// --------------------------------------------------------------------------- + +// font_decode splits a PDF string into character codes and fills in +// everything the interpreter and a text device need about each one. +// +// `out` is CLEARED first, so it may be a scratch buffer reused across every +// string on the page. One byte is one code for a simple font; a composite +// font's codes are as wide as its CMap's codespace ranges say, which is not +// necessarily two and not necessarily constant within one string. +font_decode :: proc(f: ^Font, s: []byte, out: ^[dynamic]Font_Code) { + if out == nil { + return + } + clear(out) + if f == nil || len(s) == 0 { + return + } + + i := 0 + for i < len(s) { + fc: Font_Code + if f.kind == .Type0 { + code, n := cmap_next_code(f.cmap, s[i:]) + // cmap_next_code consumes at least one byte for a non-empty + // string, but the loop's termination must not depend on a + // promise made in another file. + fc.nbytes = max(n, 1) + fc.code = code + if cid, ok := cmap_lookup(f.cmap, code); ok { + fc.cid = cid + } + fc.gid = font_cid_gid(f, fc.cid) + w0 := font_cid_width(f, fc.cid) + fc.width = w0 + if f.vertical { + w1y, vx, vy := font_cid_vmetric(f, fc.cid, w0) + fc.width = w1y + fc.vx, fc.vy = vx, vy + } + } else { + b := s[i] + fc.code = u32(b) + fc.cid = u32(b) + fc.nbytes = 1 + fc.gid = f.code_gid[b] + fc.width = f.code_width[b] + } + // 9.3.3: word spacing applies to a single-byte code 32 and to nothing + // else. A two-byte 0x0020 in a Type0 font is a perfectly ordinary + // CID, and taking it for a space stretches CJK text at random. + fc.is_space = fc.nbytes == 1 && fc.code == 32 + fc.unicode = font_code_unicode(f, fc) + append(out, fc) + i += fc.nbytes + } +} + +// font_code_unicode is the character a code stands for, or -1. +// +// /ToUnicode is authoritative when it is there: it is the producer telling us +// what it meant, and it is the only route that works for a subset font whose +// glyph names are "g17". Failing that a simple font's glyph name is turned +// into a code point through the AGL, which covers every font that uses real +// names. The `post` table is the same trick for a symbolic TrueType that has +// no PDF encoding at all. +@(private) +font_code_unicode :: proc(f: ^Font, fc: Font_Code) -> rune { + if f.to_unicode != nil { + if txt, ok := cmap_lookup_text(f.to_unicode, fc.code); ok && len(txt) > 0 { + r, _ := utf8.decode_rune_in_string(txt) + if r != utf8.RUNE_ERROR { + return r + } + } + } + + if f.kind == .Type0 { + // A Type0 font may wrap a name-keyed CFF, in which case the glyph + // still has a name even though the code never went through one. + if f.cff != nil && fc.gid <= 0xFFFF { + if name := cff_glyph_name(f.cff, u16(fc.gid)); name != "" { + if r := glyph_name_to_rune(name); r >= 0 { + return r + } + } + } + } else if fc.code < FONT_CODES { + if name := f.enc_names[fc.code]; name != "" { + if r := glyph_name_to_rune(name); r >= 0 { + return r + } + } + } + + if f.ttf != nil && fc.gid > 0 && int(fc.gid) < len(f.ttf.names) { + if r := glyph_name_to_rune(f.ttf.names[fc.gid]); r >= 0 { + return r + } + } + + // Last resort, and only for a non-symbolic single-byte code: over ASCII + // every Latin text encoding agrees with Unicode, so a font that offered + // no other answer still extracts as readable text. A symbolic font is + // excluded because its code 0x41 is a dingbat, not an "A". + if !f.symbolic && fc.nbytes == 1 && fc.code >= 0x20 && fc.code < 0x7F { + return rune(fc.code) + } + return -1 +} + +// font_clamp_width keeps a width from the file finite and within an absurd +// but harmless range. A NaN here reaches the text matrix and from there the +// geometry, which is not allowed to see one. +@(private) +font_clamp_width :: proc(w: f64) -> f64 { + if !is_finite(w) { + return 0 + } + return clamp(w, -MAX_GLYPH_WIDTH, MAX_GLYPH_WIDTH) +} diff --git a/font_cff.odin b/font_cff.odin new file mode 100644 index 0000000..ba4e638 --- /dev/null +++ b/font_cff.odin @@ -0,0 +1,1824 @@ +package pdf + +import "core:math" +import "core:mem" +import "core:slice" +import "core:strconv" + +// CFF (Compact Font Format) and its Type 2 charstrings. +// +// This file knows nothing about PDF: bytes in, outlines out. A CFF reaches it +// from three directions -- a /FontFile3 with /Subtype /Type1C or /CIDFontType0C, +// the `CFF ` table of an OpenType font, and a bare .cff file -- and none of +// those distinctions survive the call. That is what keeps it separately +// testable, and it is why nothing here may reference Document, Dict or Object. +// +// References are to Adobe's "The Compact Font Format Specification" (technical +// note 5176, version 1.0, 4 December 2003) and "The Type 2 Charstring Format" +// (technical note 5177, 16 March 2000), cited below as CFF and T2. +// +// Subsetters mangle font programs more thoroughly than any other part of a +// PDF: offsets that point past the end, INDEXes whose offset arrays run +// backwards, charstrings that call subroutines that do not exist, and +// occasionally two subroutines that call each other. Every structure read here +// is therefore clamped rather than trusted, and the interpreter is bounded in +// depth, stack and total operations. A glyph that cannot be decoded draws what +// was decoded before the fault; it never takes the page down with it. +// +// MEMORY: `cff_parse` takes an `allocator` and keeps from it the Cff_Font, the +// offset array of every INDEX it reads, and the per-glyph charset and lookup +// tables. Everything else -- names, charstrings, subroutines -- is a sub-slice +// of `data`, which the caller owns and must keep alive for as long as the font. +// There is no destructor: the intended allocator is the Document's arena, and +// a failed parse likewise leaves what it had already allocated for the arena to +// reclaim. Reading a glyph allocates nothing at all; the interpreter's state is +// the caller's stack frame. + +// --------------------------------------------------------------------------- +// Limits +// +// Everything below is bounded by a value that came out of the file, so every +// one of them is capped. The charstring limits are the spec's own; the rest +// are chosen to be far above any real font and far below anything that hurts. +// --------------------------------------------------------------------------- + +// T2 3.1: "the maximum depth of subroutine calls is 10". +@(private) +CFF_MAX_SUBR_DEPTH :: 10 +// T2 Appendix B: the interpreter's operand stack holds 48 numbers. +@(private) +CFF_MAX_STACK :: 48 +// Total charstring operators executed for one glyph, seac and all subroutine +// calls included. Subroutines cannot loop, but two of them may call each other, +// and the depth cap alone still allows 10 nested copies of a long charstring. +@(private) +CFF_MAX_CHARSTRING_OPS :: 100_000 +// T2 4: the transient array is 32 elements. +@(private) +CFF_MAX_TRANSIENT :: 32 +// CFF 4: "operators may be preceded by up to a maximum of 48 operands". +@(private) +CFF_MAX_DICT_OPERANDS :: 48 +// Longest real number a DICT may spell out, in characters. Reals in a DICT are +// font matrices and blue values; 64 digits is already absurd. +@(private) +CFF_MAX_REAL_CHARS :: 64 +// FDSelect stores the font-DICT index in one byte, so no more than 256 of them +// can ever be selected however many the FDArray lists. +@(private) +CFF_MAX_FDS :: 256 + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +// Cff_Index is a CFF INDEX: a counted array of variable-length items. +// +// `offsets` holds count+1 *absolute* positions into `data`, already clamped to +// be non-decreasing and inside the font, so `cff_index_item` needs no further +// validation and can never produce a slice that escapes the font. +@(private) +Cff_Index :: struct { + data: []byte, + offsets: []u32, + count: int, + // First byte after the INDEX, so the next one can be read from here. + end: int, +} + +// Cff_Private is one Private DICT: the widths every charstring in its scope is +// relative to, and the local subroutines it may call. A non-CID font has one; +// a CIDFont has one per entry in the FDArray. +@(private) +Cff_Private :: struct { + subrs: Cff_Index, + subr_bias: int, + default_width: f64, + nominal_width: f64, +} + +// Cff_Name_Entry is one (glyph name, GID) pair of the by-name lookup table, +// which is sorted so `cff_gid_for_name` is a binary search rather than a walk +// over every glyph in the font for every code in the encoding. +@(private) +Cff_Name_Entry :: struct { + name: string, + gid: u16, +} + +// Cff_Font is the parsed form of a font program. +// +// `data` is borrowed, not copied: every name, every charstring and every +// subroutine is a sub-slice of it, so it must stay alive and unmodified for as +// long as the font does. Everything else comes from the allocator passed to +// `cff_parse`. +Cff_Font :: struct { + data: []byte, + + // The four INDEXes at the head of the file. + names: Cff_Index, + top_dicts: Cff_Index, + strings: Cff_Index, + gsubrs: Cff_Index, + gsubr_bias: int, + + // The glyph outlines themselves. + charstrings: Cff_Index, + + // Top DICT. + font_matrix: Matrix, + font_bbox: Rect, + charstring_type: int, + is_cid: bool, + + // The Private DICT of a non-CID font, or of FD 0 when there is no + // FDSelect. `fds` and `fd_select` are empty for a non-CID font. + priv: Cff_Private, + fds: []Cff_Private, + // One font-DICT index per glyph. Empty means every glyph uses fds[0], or + // `priv` when there is no FDArray either. + fd_select: []u8, + + // charset: SID per glyph for a name-keyed font, CID per glyph for a + // CIDFont. Always `glyph_count` long once parsing succeeds. + charset: []u16, + + // Sorted lookup tables. Exactly one of the two is built, according to what + // the charset holds. + by_name: []Cff_Name_Entry, + // (cid << 16) | gid, sorted, so a binary search on the high half finds the + // lowest GID for a CID. + by_cid: []u32, + + // The font's built-in encoding: the glyph each byte code selects, and the + // name it selects it by. A CIDFont has neither (CFF 12: "CIDFonts do not + // use the Encoding operator"). + code_to_gid: [256]u16, + enc_names: [256]string, +} + +// --------------------------------------------------------------------------- +// Bounds-checked readers +// +// Every one of these returns zero past the end rather than trapping. Structural +// code checks the length it needs before it reads, so a zero here is always a +// value the file really contains -- except in the recovery paths, where zero is +// exactly the harmless answer wanted. +// --------------------------------------------------------------------------- + +@(private) +cff_u16 :: proc "contextless" (d: []byte, p: int) -> u16 { + if p < 0 || p + 1 >= len(d) { return 0 } + return u16(d[p]) << 8 | u16(d[p + 1]) +} + +// cff_offset reads a big-endian unsigned value `size` bytes wide, as INDEX +// offset arrays and DICT offsets are written. +@(private) +cff_offset :: proc "contextless" (d: []byte, p: int, size: int) -> u32 { + if size < 1 || size > 4 || p < 0 || p + size > len(d) { return 0 } + v: u32 = 0 + for i in 0 ..< size { + v = v << 8 | u32(d[p + i]) + } + return v +} + +// --------------------------------------------------------------------------- +// INDEX +// --------------------------------------------------------------------------- + +// cff_index_read parses the INDEX at `pos`. The offset array is materialised as +// absolute positions so that later reads need no arithmetic and no checks. +// +// A count of zero is the whole structure: CFF 5 says an empty INDEX is two +// bytes with no offSize byte at all, and writing the extra byte anyway is a +// mistake that would push every following structure one byte out. +@(private) +cff_index_read :: proc(data: []byte, pos: int, allocator: mem.Allocator) -> (idx: Cff_Index, err: Error) { + idx.data = data + idx.end = pos + if pos < 0 || pos + 2 > len(data) { + return idx, .Truncated + } + count := int(cff_u16(data, pos)) + idx.end = pos + 2 + if count == 0 { + return idx, .None + } + if pos + 3 > len(data) { + return idx, .Truncated + } + off_size := int(data[pos + 2]) + if off_size < 1 || off_size > 4 { + return idx, .Bad_Cff + } + table := pos + 3 + // count <= 65535 and off_size <= 4, so this cannot overflow, and checking + // it against the file is what bounds the allocation below. + need := (count + 1) * off_size + if need > len(data) - table { + return idx, .Truncated + } + // CFF 5: offsets are relative to the byte preceding the item data, so the + // first is always 1. + base := table + need - 1 + limit := u32(len(data) - base) + + offsets := make([]u32, count + 1, allocator) + prev: u32 = 1 + for i in 0 ..= count { + v := cff_offset(data, table + i * off_size, off_size) + // Clamping instead of rejecting: a subset font with one bad offset + // still draws every other glyph, and the item this produces is empty + // rather than out of bounds. + if v < prev { v = prev } + if v > limit { v = limit } + offsets[i] = u32(base) + v + prev = v + } + idx.offsets = offsets + idx.count = count + idx.end = int(offsets[count]) + return idx, .None +} + +@(private) +cff_index_item :: proc(idx: ^Cff_Index, i: int) -> []byte { + if i < 0 || i >= idx.count || len(idx.offsets) < idx.count + 1 { + return nil + } + lo, hi := int(idx.offsets[i]), int(idx.offsets[i + 1]) + if lo > hi || hi > len(idx.data) { + return nil + } + return idx.data[lo:hi] +} + +// cff_subr_bias is the constant added to a callsubr/callgsubr index before it +// selects an entry. T2 4.7: subroutine numbers are biased so that the small +// single-byte encodings reach the middle of the array, where a subsetter puts +// the subroutines it uses most. +@(private) +cff_subr_bias :: proc "contextless" (count: int) -> int { + switch { + case count < 1240: return 107 + case count < 33900: return 1131 + case: return 32768 + } +} + +// --------------------------------------------------------------------------- +// DICT +// +// A DICT is a flat byte string of operands followed by the operator they +// belong to -- postfix, with no nesting and no keys other than the operator +// number. CFF 4 gives the operand encoding, which is not the same as the one +// charstrings use: a DICT has the 5-byte 29 integer and the nibble-packed 30 +// real, and no 16.16 fixed. +// --------------------------------------------------------------------------- + +// Operators are keyed as their byte, with the two-byte "12 x" escapes carried +// as 0x0C00 | x, so one switch covers both forms. +@(private) +CFF_ESC :: 0x0C00 + +@(private) +Cff_Dict_Iter :: struct { + data: []byte, + pos: int, + ops: [CFF_MAX_DICT_OPERANDS]f64, + n: int, +} + +// cff_dict_next advances to the next operator, leaving its operands in +// `it.ops[:it.n]`. It returns ok = false at the end of the DICT and at the +// first byte that cannot be a valid operand or operator, because past that +// point the operand/operator alternation has lost sync and anything read would +// be invented. +@(private) +cff_dict_next :: proc(it: ^Cff_Dict_Iter) -> (op: u16, ok: bool) { + it.n = 0 + for it.pos < len(it.data) { + b0 := it.data[it.pos] + switch { + case b0 == 12: + if it.pos + 1 >= len(it.data) { return 0, false } + op = CFF_ESC | u16(it.data[it.pos + 1]) + it.pos += 2 + return op, true + case b0 <= 21: + // 22..27 and 31 are reserved and never appear as operators. + if b0 >= 22 { return 0, false } + op = u16(b0) + it.pos += 1 + return op, true + case b0 == 28: + if it.pos + 2 >= len(it.data) { return 0, false } + v := i16(u16(it.data[it.pos + 1]) << 8 | u16(it.data[it.pos + 2])) + cff_dict_push(it, f64(v)) + it.pos += 3 + case b0 == 29: + if it.pos + 4 >= len(it.data) { return 0, false } + u := u32(it.data[it.pos + 1]) << 24 | u32(it.data[it.pos + 2]) << 16 | + u32(it.data[it.pos + 3]) << 8 | u32(it.data[it.pos + 4]) + cff_dict_push(it, f64(i32(u))) + it.pos += 5 + case b0 == 30: + v, next := cff_dict_real(it.data, it.pos + 1) + cff_dict_push(it, v) + it.pos = next + case b0 >= 32 && b0 <= 246: + cff_dict_push(it, f64(int(b0) - 139)) + it.pos += 1 + case b0 >= 247 && b0 <= 250: + if it.pos + 1 >= len(it.data) { return 0, false } + cff_dict_push(it, f64((int(b0) - 247) * 256 + int(it.data[it.pos + 1]) + 108)) + it.pos += 2 + case b0 >= 251 && b0 <= 254: + if it.pos + 1 >= len(it.data) { return 0, false } + cff_dict_push(it, f64(-(int(b0) - 251) * 256 - int(it.data[it.pos + 1]) - 108)) + it.pos += 2 + case: + // 31 and 255 are reserved operand bytes. + return 0, false + } + } + return 0, false +} + +// cff_dict_push drops operands past the 48th rather than growing: the format +// forbids more, and every operator this reader cares about takes its arguments +// from the front. +@(private) +cff_dict_push :: proc(it: ^Cff_Dict_Iter, v: f64) { + if it.n >= CFF_MAX_DICT_OPERANDS { return } + it.ops[it.n] = is_finite(v) ? v : 0 + it.n += 1 +} + +// cff_dict_real decodes the nibble-packed real of CFF Table 5, returning the +// value and the position just past its terminating nibble. A malformed one +// yields whatever prefix parsed, which is what keeps a damaged /FontMatrix from +// costing the whole font. +@(private) +cff_dict_real :: proc(d: []byte, start: int) -> (value: f64, next: int) { + buf: [CFF_MAX_REAL_CHARS]u8 + n := 0 + put :: proc(buf: ^[CFF_MAX_REAL_CHARS]u8, n: ^int, c: u8) { + if n^ < len(buf) { + buf[n^] = c + n^ += 1 + } + } + p := start + done := false + for p < len(d) && !done { + b := d[p] + p += 1 + for shift := uint(4); ; shift -= 4 { + nib := (b >> shift) & 0xF + switch nib { + case 0x0 ..= 0x9: put(&buf, &n, u8('0') + nib) + case 0xA: put(&buf, &n, '.') + case 0xB: put(&buf, &n, 'E') + case 0xC: put(&buf, &n, 'E'); put(&buf, &n, '-') + case 0xD: // reserved; skip + case 0xE: put(&buf, &n, '-') + case 0xF: done = true + } + if shift == 0 || done { break } + } + } + v, ok := strconv.parse_f64(string(buf[:n])) + if !ok || !is_finite(v) { + return 0, p + } + return v, p +} + +// --------------------------------------------------------------------------- +// Top DICT and Private DICT +// --------------------------------------------------------------------------- + +@(private) +Cff_Top :: struct { + charset: int, + encoding: int, + charstrings: int, + priv_size: int, + priv_off: int, + fdarray: int, + fdselect: int, + is_cid: bool, + charstring_type: int, + font_matrix: Matrix, + has_matrix: bool, + font_bbox: Rect, +} + +@(private) +cff_top_dict_parse :: proc(d: []byte) -> (top: Cff_Top) { + top.charset = -1 + top.encoding = -1 + top.charstrings = -1 + top.fdarray = -1 + top.fdselect = -1 + top.priv_off = -1 + top.charstring_type = 2 + // CFF Table 9: the default FontMatrix is 0.001 units per em. + top.font_matrix = Matrix{0.001, 0, 0, 0.001, 0, 0} + + it := Cff_Dict_Iter{data = d} + for { + op, ok := cff_dict_next(&it) + if !ok { break } + switch op { + case 15: if it.n >= 1 { top.charset = cff_dict_int(&it, 0) } + case 16: if it.n >= 1 { top.encoding = cff_dict_int(&it, 0) } + case 17: if it.n >= 1 { top.charstrings = cff_dict_int(&it, 0) } + case 18: + if it.n >= 2 { + top.priv_size = cff_dict_int(&it, 0) + top.priv_off = cff_dict_int(&it, 1) + } + case 5: + if it.n >= 4 { + top.font_bbox = rect_normalize(Rect{it.ops[0], it.ops[1], it.ops[2], it.ops[3]}) + } + case CFF_ESC | 6: if it.n >= 1 { top.charstring_type = cff_dict_int(&it, 0) } + case CFF_ESC | 7: + if it.n >= 6 { + m := Matrix{it.ops[0], it.ops[1], it.ops[2], it.ops[3], it.ops[4], it.ops[5]} + if matrix_is_finite(m) { + top.font_matrix = m + top.has_matrix = true + } + } + case CFF_ESC | 30: top.is_cid = true // ROS + case CFF_ESC | 36: if it.n >= 1 { top.fdarray = cff_dict_int(&it, 0) } + case CFF_ESC | 37: if it.n >= 1 { top.fdselect = cff_dict_int(&it, 0) } + } + } + return +} + +// cff_dict_int reads an operand as a file offset or count. Values outside +// int32 cannot be a position in a font and become -1, which every caller +// already treats as "absent". +@(private) +cff_dict_int :: proc(it: ^Cff_Dict_Iter, i: int) -> int { + if i < 0 || i >= it.n { return -1 } + v := it.ops[i] + if !is_finite(v) || v < f64(min(i32)) || v > f64(max(i32)) { return -1 } + return int(v) +} + +// cff_private_parse reads the Private DICT at (off, size) and the local Subrs +// INDEX it points at. Subrs is an offset from the start of the Private DICT +// itself, not from the start of the font (CFF Table 23). +@(private) +cff_private_parse :: proc(data: []byte, off, size: int, allocator: mem.Allocator) -> (p: Cff_Private) { + p.subr_bias = cff_subr_bias(0) + if off < 0 || size <= 0 || off >= len(data) { return } + end := off + size + if end > len(data) || end < off { end = len(data) } + + subrs_off := -1 + it := Cff_Dict_Iter{data = data[off:end]} + for { + op, ok := cff_dict_next(&it) + if !ok { break } + switch op { + case 19: if it.n >= 1 { subrs_off = cff_dict_int(&it, 0) } + case 20: if it.n >= 1 { p.default_width = it.ops[0] } + case 21: if it.n >= 1 { p.nominal_width = it.ops[0] } + } + } + if subrs_off > 0 && off + subrs_off < len(data) { + idx, err := cff_index_read(data, off + subrs_off, allocator) + if err == .None { + p.subrs = idx + } + } + p.subr_bias = cff_subr_bias(p.subrs.count) + return +} + +// --------------------------------------------------------------------------- +// The standard strings +// +// CFF Appendix A. SIDs below 391 name one of these instead of indexing the +// String INDEX, which is how a font that uses only ISO-Adobe glyph names +// carries no strings at all. The order is the specification's and is load +// bearing: the SID is the position. +// --------------------------------------------------------------------------- + +@(private) +CFF_N_STD_STRINGS :: 391 + +@(private) +CFF_STD_STRINGS := [CFF_N_STD_STRINGS]string{ + ".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", + "ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus", + "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", + "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", + "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", + "asciicircum", "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", + "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", + "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", + "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section", + "currency", "quotesingle", "quotedblleft", "guillemotleft", "guilsinglleft", + "guilsinglright", "fi", "fl", "endash", "dagger", "daggerdbl", + "periodcentered", "paragraph", "bullet", "quotesinglbase", "quotedblbase", + "quotedblright", "guillemotright", "ellipsis", "perthousand", "questiondown", + "grave", "acute", "circumflex", "tilde", "macron", "breve", "dotaccent", + "dieresis", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emdash", + "AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae", + "dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior", + "logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn", + "onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters", + "twosuperior", "registered", "minus", "eth", "multiply", "threesuperior", + "copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring", + "Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave", + "Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute", + "Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute", + "Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron", + "aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde", + "ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute", + "icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex", + "odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex", + "udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall", + "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall", + "Acutesmall", "parenleftsuperior", "parenrightsuperior", "twodotenleader", + "onedotenleader", "zerooldstyle", "oneoldstyle", "twooldstyle", + "threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle", + "sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior", + "threequartersemdash", "periodsuperior", "questionsmall", "asuperior", + "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", + "lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", + "tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior", + "Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall", + "Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall", + "Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall", + "Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall", + "Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah", + "Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall", + "Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall", + "Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior", + "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", + "oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird", + "twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior", + "sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior", + "oneinferior", "twoinferior", "threeinferior", "fourinferior", + "fiveinferior", "sixinferior", "seveninferior", "eightinferior", + "nineinferior", "centinferior", "dollarinferior", "periodinferior", + "commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall", + "Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall", + "Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", + "Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall", + "Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", + "Ocircumflexsmall", "Otildesmall", "Odieresissmall", "OEsmall", + "Oslashsmall", "Ugravesmall", "Uacutesmall", "Ucircumflexsmall", + "Udieresissmall", "Yacutesmall", "Thornsmall", "Ydieresissmall", "001.000", + "001.001", "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", + "Regular", "Roman", "Semibold", +} + +// cff_sid_string resolves a SID to its glyph or text name. An out-of-range SID +// yields "" rather than a name from somewhere else in the font. +@(private) +cff_sid_string :: proc(f: ^Cff_Font, sid: u16) -> string { + if int(sid) < CFF_N_STD_STRINGS { + return CFF_STD_STRINGS[sid] + } + idx := &f.strings + item := cff_index_item(idx, int(sid) - CFF_N_STD_STRINGS) + return string(item) +} + +// --------------------------------------------------------------------------- +// charset +// --------------------------------------------------------------------------- + +// cff_charset_parse fills gid -> SID (or, for a CIDFont, gid -> CID). +// +// Offsets 0, 1 and 2 name the predefined ISOAdobe, Expert and ExpertSubset +// charsets (CFF Appendix C). Only ISOAdobe is tabulated, as the identity it +// is; a font using one of the two Expert charsets loses its glyph *names*, and +// so any lookup by name, but keeps every GID and therefore every outline. No +// PDF-embedded CFF in the corpus uses them, and a table recited wrongly would +// be worse than one that is honestly absent. +@(private) +cff_charset_parse :: proc(f: ^Cff_Font, off: int, allocator: mem.Allocator) { + // Every per-glyph allocation in this file is sized by the CharStrings + // count, which an INDEX writes as a Card16 and so can never exceed 65535 + // however damaged the font is. + n := f.charstrings.count + if n <= 0 { return } + cs := make([]u16, n, allocator) + f.charset = cs + + if off <= 2 { + // Identity: SID i for glyph i, which is exactly ISOAdobe for the 229 + // glyphs it defines, and the identity CID mapping a CIDFont with no + // charset needs. + limit := n + if !f.is_cid && limit > 229 { limit = 229 } + for i in 0 ..< limit { + cs[i] = u16(i) + } + return + } + if off >= len(f.data) { return } + + d := f.data + format := d[off] + p := off + 1 + // Glyph 0 is always .notdef and is not listed. + gid := 1 + switch format { + case 0: + for gid < n && p + 1 < len(d) { + cs[gid] = cff_u16(d, p) + p += 2 + gid += 1 + } + case 1, 2: + step := format == 1 ? 3 : 4 + for gid < n && p + step <= len(d) { + first := int(cff_u16(d, p)) + n_left := format == 1 ? int(d[p + 2]) : int(cff_u16(d, p + 2)) + p += step + for i := 0; i <= n_left && gid < n; i += 1 { + v := first + i + // SIDs and CIDs are 16 bit; a range that would run past that + // is a corrupt one, so it stops here instead of wrapping. + if v > 0xFFFF { break } + cs[gid] = u16(v) + gid += 1 + } + } + case: + // An unknown format leaves every glyph nameless, which is recoverable: + // outlines are indexed by GID and do not need the charset at all. + } +} + +// --------------------------------------------------------------------------- +// Encoding +// --------------------------------------------------------------------------- + +// cff_encoding_parse fills the code -> glyph tables from the font's built-in +// Encoding. CFF 12 gives offsets 0 and 1 to the predefined Standard and Expert +// encodings; anything larger is a custom encoding in format 0 or 1, either of +// which may carry supplements. +@(private) +cff_encoding_parse :: proc(f: ^Cff_Font, off: int) { + // CIDFonts have no encoding at all: their charstrings are selected by CID. + if f.is_cid { return } + + n := f.charstrings.count + if off <= 1 || off >= len(f.data) { + // Standard (and, for want of a table, Expert) encoding: the code names + // a glyph, and the charset says which glyph carries that name. + std := encoding_standard() + for code in 0 ..< 256 { + name := std[code] + if name == "" { continue } + gid, ok := cff_gid_for_name(f, name) + if !ok { continue } + f.code_to_gid[code] = gid + f.enc_names[code] = name + } + return + } + + d := f.data + format := d[off] + p := off + 1 + switch format & 0x7F { + case 0: + if p >= len(d) { return } + n_codes := int(d[p]) + p += 1 + for i in 0 ..< n_codes { + if p >= len(d) { break } + code := int(d[p]) + p += 1 + gid := i + 1 + if gid < n { + f.code_to_gid[code] = u16(gid) + } + } + case 1: + if p >= len(d) { return } + n_ranges := int(d[p]) + p += 1 + gid := 1 + for i := 0; i < n_ranges && p + 1 < len(d); i += 1 { + first := int(d[p]) + n_left := int(d[p + 1]) + p += 2 + for k := 0; k <= n_left && gid < n; k += 1 { + code := first + k + if code > 255 { break } + f.code_to_gid[code] = u16(gid) + gid += 1 + } + } + case: + return + } + + // CFF 12: the high bit of the format byte says a supplement list follows, + // mapping extra codes to glyphs by NAME rather than by position. + if format & 0x80 != 0 && p < len(d) { + n_sups := int(d[p]) + p += 1 + for i := 0; i < n_sups && p + 2 < len(d); i += 1 { + code := int(d[p]) + sid := cff_u16(d, p + 1) + p += 3 + name := cff_sid_string(f, sid) + if name == "" { continue } + f.enc_names[code] = name + if gid, ok := cff_gid_for_name(f, name); ok { + f.code_to_gid[code] = gid + } + } + } + + // Fill in the names of everything the format-0/1 part mapped by position. + for code in 0 ..< 256 { + if f.enc_names[code] == "" && f.code_to_gid[code] != 0 { + f.enc_names[code] = cff_glyph_name(f, f.code_to_gid[code]) + } + } +} + +// --------------------------------------------------------------------------- +// FDSelect and FDArray (CIDFonts) +// --------------------------------------------------------------------------- + +// cff_fdselect_parse fills one font-DICT index per glyph. A glyph the table +// does not cover keeps index 0, which is the only safe guess: it at least has +// a Private DICT. +@(private) +cff_fdselect_parse :: proc(f: ^Cff_Font, off: int, allocator: mem.Allocator) { + n := f.charstrings.count + if n <= 0 || off <= 0 || off >= len(f.data) { return } + d := f.data + format := d[off] + sel := make([]u8, n, allocator) + f.fd_select = sel + + switch format { + case 0: + p := off + 1 + for gid in 0 ..< n { + if p + gid >= len(d) { break } + sel[gid] = d[p + gid] + } + case 3: + n_ranges := int(cff_u16(d, off + 1)) + p := off + 3 + // Each range is 3 bytes and the array is followed by a 2-byte sentinel. + for i := 0; i < n_ranges && p + 4 < len(d); i += 1 { + first := int(cff_u16(d, p)) + fd := d[p + 2] + next := int(cff_u16(d, p + 3)) + p += 3 + if next > n { next = n } + for gid := first; gid < next && gid < n; gid += 1 { + if gid >= 0 { + sel[gid] = fd + } + } + } + case: + // Unknown format: every glyph falls back to font DICT 0. + } +} + +@(private) +cff_fdarray_parse :: proc(f: ^Cff_Font, off: int, allocator: mem.Allocator) { + if off <= 0 || off >= len(f.data) { return } + idx, err := cff_index_read(f.data, off, allocator) + if err != .None || idx.count == 0 { return } + count := min(idx.count, CFF_MAX_FDS) + fds := make([]Cff_Private, count, allocator) + for i in 0 ..< count { + fd_dict := cff_index_item(&idx, i) + priv_off, priv_size := -1, 0 + it := Cff_Dict_Iter{data = fd_dict} + for { + op, ok := cff_dict_next(&it) + if !ok { break } + if op == 18 && it.n >= 2 { + priv_size = cff_dict_int(&it, 0) + priv_off = cff_dict_int(&it, 1) + } + } + fds[i] = cff_private_parse(f.data, priv_off, priv_size, allocator) + } + f.fds = fds +} + +// --------------------------------------------------------------------------- +// Parsing the font +// --------------------------------------------------------------------------- + +// cff_parse reads a CFF font program. +// +// `data` is borrowed: the returned font holds sub-slices of it for names, +// charstrings and subroutines, so it must outlive the font and must not be +// modified. Everything else the font owns comes from `allocator` and is +// released only by releasing that allocator -- the intended use is an arena, +// which is what the Document hands fonts. That holds for a failed parse too: +// what was allocated before the failure is left for the arena to reclaim. +// +// An error is returned only when there is nothing usable at all: no header, no +// Top DICT, or no CharStrings INDEX. Everything else degrades -- a broken +// charset costs glyph names, a broken Private DICT costs local subroutines and +// the default width, and neither stops the outlines from being read. +@(private) +cff_parse :: proc(data: []byte, allocator := context.allocator) -> (^Cff_Font, Error) { + // CFF 6: the header is major, minor, hdrSize, offSize. + if len(data) < 4 { + return nil, .Truncated + } + if data[0] != 1 { + // CFF2 (major version 2) drops the Name, String and Encoding INDEXes + // and changes the charstring operators. Reading it as CFF1 would + // produce nonsense, so it is refused rather than guessed at. + return nil, .Bad_Cff + } + hdr_size := int(data[2]) + // A hdrSize smaller than the header itself is a subsetter bug; the four + // bytes the format defines are still there, so start after them. + if hdr_size < 4 { hdr_size = 4 } + if hdr_size > len(data) { + return nil, .Truncated + } + + f := new(Cff_Font, allocator) + f.data = data + f.font_matrix = Matrix{0.001, 0, 0, 0.001, 0, 0} + f.charstring_type = 2 + + err: Error + f.names, err = cff_index_read(data, hdr_size, allocator) + if err != .None { return nil, err } + f.top_dicts, err = cff_index_read(data, f.names.end, allocator) + if err != .None { return nil, err } + // A damaged String or GlobalSubr INDEX costs names and subroutines but + // nothing structural: the Top DICT gives every other position absolutely. + f.strings, _ = cff_index_read(data, f.top_dicts.end, allocator) + f.gsubrs, _ = cff_index_read(data, f.strings.end, allocator) + f.gsubr_bias = cff_subr_bias(f.gsubrs.count) + + top_data := cff_index_item(&f.top_dicts, 0) + if len(top_data) == 0 { + return nil, .Bad_Cff + } + top := cff_top_dict_parse(top_data) + f.is_cid = top.is_cid + f.charstring_type = top.charstring_type + f.font_bbox = top.font_bbox + f.font_matrix = top.font_matrix + + if top.charstrings <= 0 || top.charstrings >= len(data) { + return nil, .Bad_Cff + } + f.charstrings, err = cff_index_read(data, top.charstrings, allocator) + if err != .None { + return nil, .Bad_Cff + } + if f.charstrings.count == 0 { + return nil, .Bad_Cff + } + + f.priv = cff_private_parse(data, top.priv_off, top.priv_size, allocator) + if top.fdarray > 0 { + cff_fdarray_parse(f, top.fdarray, allocator) + } + if top.fdselect > 0 { + cff_fdselect_parse(f, top.fdselect, allocator) + } + // CFF 10: a CIDFont's FDArray entries may carry their own FontMatrix. When + // the Top DICT has none of its own, the first font DICT's is the font's. + if !top.has_matrix && len(f.fds) > 0 { + if m, ok := cff_fd_matrix(f, top.fdarray, allocator); ok { + f.font_matrix = m + } + } + + cff_charset_parse(f, top.charset, allocator) + cff_build_lookups(f, allocator) + cff_encoding_parse(f, top.encoding) + return f, .None +} + +// cff_fd_matrix re-reads the FDArray for a FontMatrix on its first entry. It is +// a second pass over a structure already parsed because the matrix is the one +// thing in a font DICT that is not per-glyph, and hoisting it into +// Cff_Private would put it on every entry to serve one. +@(private) +cff_fd_matrix :: proc(f: ^Cff_Font, off: int, allocator: mem.Allocator) -> (Matrix, bool) { + idx, err := cff_index_read(f.data, off, allocator) + if err != .None || idx.count == 0 { return MATRIX_IDENTITY, false } + it := Cff_Dict_Iter{data = cff_index_item(&idx, 0)} + for { + op, ok := cff_dict_next(&it) + if !ok { break } + if op == CFF_ESC | 7 && it.n >= 6 { + m := Matrix{it.ops[0], it.ops[1], it.ops[2], it.ops[3], it.ops[4], it.ops[5]} + if matrix_is_finite(m) { + return m, true + } + } + } + return MATRIX_IDENTITY, false +} + +// cff_build_lookups builds whichever of the two reverse maps the charset calls +// for. Both are sorted arrays rather than hash tables: they are built once, +// searched a few hundred times, and a sorted array of 16-bit keys is smaller +// than the font's own offset array. +@(private) +cff_build_lookups :: proc(f: ^Cff_Font, allocator: mem.Allocator) { + n := len(f.charset) + if n == 0 { return } + if f.is_cid { + table := make([]u32, n, allocator) + for gid in 0 ..< n { + table[gid] = u32(f.charset[gid]) << 16 | u32(gid) + } + // Sorting the packed pair orders by CID and then by GID, so a binary + // search that walks left over equal CIDs lands on the lowest GID. + slice.sort(table) + f.by_cid = table + return + } + table := make([]Cff_Name_Entry, n, allocator) + for gid in 0 ..< n { + table[gid] = Cff_Name_Entry{cff_sid_string(f, f.charset[gid]), u16(gid)} + } + slice.sort_by(table, proc(a, b: Cff_Name_Entry) -> bool { + if a.name != b.name { return a.name < b.name } + return a.gid < b.gid + }) + f.by_name = table +} + +// --------------------------------------------------------------------------- +// Accessors +// +// Package-private, like every other font-program reader: font.odin is the only +// caller, and the library's public API is the one pdf.odin's doc comment names. +// --------------------------------------------------------------------------- + +@(private) +cff_glyph_count :: proc(f: ^Cff_Font) -> int { + if f == nil { return 0 } + return f.charstrings.count +} + +// cff_font_matrix maps glyph space to text space. CFF Table 9 defaults it to +// 1/1000, which is what all but a handful of fonts use. +@(private) +cff_font_matrix :: proc(f: ^Cff_Font) -> Matrix { + if f == nil { return Matrix{0.001, 0, 0, 0.001, 0, 0} } + return f.font_matrix +} + +// cff_gid_for_name finds the glyph with this name. A CIDFont has no glyph +// names -- its charset holds CIDs -- and always reports false. +@(private) +cff_gid_for_name :: proc(f: ^Cff_Font, name: string) -> (u16, bool) { + if f == nil || name == "" || len(f.by_name) == 0 { return 0, false } + lo, hi := 0, len(f.by_name) + for lo < hi { + mid := lo + (hi - lo) / 2 + if f.by_name[mid].name < name { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(f.by_name) && f.by_name[lo].name == name { + return f.by_name[lo].gid, true + } + return 0, false +} + +// cff_gid_for_cid maps a CID to a glyph through the charset, which for a +// CIDFont holds CIDs rather than name SIDs (CFF 18). For a font that is not a +// CIDFont the CID is the glyph index, which is what a PDF using Identity-H +// with a non-CID CFF expects. +@(private) +cff_gid_for_cid :: proc(f: ^Cff_Font, cid: u16) -> (u16, bool) { + if f == nil { return 0, false } + if !f.is_cid { + if int(cid) < f.charstrings.count { return cid, true } + return 0, false + } + lo, hi := 0, len(f.by_cid) + key := u32(cid) << 16 + for lo < hi { + mid := lo + (hi - lo) / 2 + if f.by_cid[mid] < key { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(f.by_cid) && f.by_cid[lo] >> 16 == u32(cid) { + return u16(f.by_cid[lo] & 0xFFFF), true + } + return 0, false +} + +// cff_glyph_name is the name the charset gives a glyph, or "" for a CIDFont, +// an out-of-range glyph or a SID that names nothing. +@(private) +cff_glyph_name :: proc(f: ^Cff_Font, gid: u16) -> string { + if f == nil || f.is_cid || int(gid) >= len(f.charset) { return "" } + return cff_sid_string(f, f.charset[gid]) +} + +// cff_builtin_encoding is the font's own code-to-glyph-name table, the same +// shape the built-in encodings in font_standard.odin have, so a simple font +// can apply /Differences over a copy of it. Entries are "" where the encoding +// selects nothing, and a CIDFont has no encoding at all. +// +// Not one of the seven procedures the render-layer contract lists; it is here +// because a symbolic Type1C font with a custom Encoding cannot be drawn +// without it, exactly as a Type1 font cannot be drawn without +// `t1_builtin_encoding`. +@(private) +cff_builtin_encoding :: proc(f: ^Cff_Font) -> ^[256]string { + if f == nil { return nil } + return &f.enc_names +} + +// cff_gid_for_code resolves a byte code through the font's built-in encoding. +// The direct route: a custom Encoding names glyphs by position, so going via +// the name would lose any glyph the charset does not name. +@(private) +cff_gid_for_code :: proc(f: ^Cff_Font, code: byte) -> (u16, bool) { + if f == nil { return 0, false } + gid := f.code_to_gid[code] + if gid == 0 { return 0, false } + return gid, true +} + +// --------------------------------------------------------------------------- +// The Type 2 charstring interpreter +// +// T2 is a stack machine whose operators are the drawing commands. Three of its +// rules are where implementations go wrong, so they are stated here once: +// +// 1. The glyph's width is an optional extra leading argument on the FIRST +// stack-clearing operator and nowhere else, expressed as a difference from +// nominalWidthX. Its presence is inferred from the argument count being one +// more than the operator takes -- for the stem operators, from the count +// being odd. +// 2. hintmask and cntrmask take the arguments left on the stack as an implied +// vstem declaration before reading their mask, which is ceil(numStems/8) +// bytes of the charstring itself rather than an operand. +// 3. Subroutine numbers are biased by 107, 1131 or 32768 according to how many +// subroutines the INDEX holds, so that the one-byte operand range reaches +// the middle of the array. +// --------------------------------------------------------------------------- + +@(private) +Cff_Interp :: struct { + f: ^Cff_Font, + // nil to run for the width alone, which is what cff_glyph_advance wants: + // no outline is built and the run stops at the first stack-clearing + // operator. + out: ^Path, + priv: ^Cff_Private, + + stack: [CFF_MAX_STACK]f64, + sp: int, + trans: [CFF_MAX_TRANSIENT]f64, + + x, y: f64, + n_stems: int, + + width: f64, + have_width: bool, + width_only: bool, + + open: bool, + done: bool, + seac: [4]f64, + have_seac: bool, + allow_seac: bool, + + fuel: int, + seed: u32, +} + +@(private) +cff_push :: proc(ip: ^Cff_Interp, v: f64) -> Error { + if ip.sp >= CFF_MAX_STACK { + // T2 Appendix B fixes the stack at 48. A charstring that overruns it + // is either corrupt or hostile; either way the rest of it cannot be + // trusted, so the glyph stops here with what it has drawn so far. + return .Bad_Cff + } + ip.stack[ip.sp] = is_finite(v) ? v : 0 + ip.sp += 1 + return .None +} + +// cff_take_width settles the width question on the first stack-clearing +// operator. `nargs` is how many arguments the operator itself takes; one more +// on the stack means the first is the width. +@(private) +cff_take_width :: proc(ip: ^Cff_Interp, nargs: int) { + if ip.have_width { return } + ip.have_width = true + if ip.sp > nargs && ip.priv != nil { + w := ip.priv.nominal_width + ip.stack[0] + if is_finite(w) { ip.width = w } + } +} + +@(private) +cff_moveto :: proc(ip: ^Cff_Interp) { + if ip.out != nil { + // Every subpath of a charstring is closed: T2 has no explicit close + // operator, a moveto ends the previous contour. + if ip.open { path_close(ip.out) } + path_move_to(ip.out, ip.x, ip.y) + } + ip.open = true +} + +@(private) +cff_lineto :: proc(ip: ^Cff_Interp) { + if ip.out != nil { path_line_to(ip.out, ip.x, ip.y) } +} + +@(private) +cff_curveto :: proc(ip: ^Cff_Interp, x1, y1, x2, y2: f64) { + if ip.out != nil { path_curve_to(ip.out, x1, y1, x2, y2, ip.x, ip.y) } +} + +// cff_int reads an operand that is going to be used as an index or a code. +// Anything that is not a small whole number cannot be either. +@(private) +cff_int :: proc(v: f64) -> (int, bool) { + if !is_finite(v) || v < -1e9 || v > 1e9 { return 0, false } + return int(v), true +} + +@(private) +cff_run :: proc(ip: ^Cff_Interp, code: []byte, depth: int) -> Error { + if depth > CFF_MAX_SUBR_DEPTH { + return .Bad_Cff + } + s := &ip.stack + i := 0 + for i < len(code) { + if ip.done { return .None } + // Once the width is known there is nothing else a width-only run wants, + // and stopping here is what keeps cff_glyph_advance from interpreting + // whole outlines. + if ip.width_only && ip.have_width { + ip.done = true + return .None + } + ip.fuel -= 1 + if ip.fuel <= 0 { return .Bad_Cff } + + b0 := code[i] + + // -- operands (T2 4) ------------------------------------------------ + if b0 == 28 { + if i + 2 >= len(code) { return .Bad_Cff } + cff_push(ip, f64(i16(u16(code[i + 1]) << 8 | u16(code[i + 2])))) or_return + i += 3 + continue + } + if b0 >= 32 { + switch { + case b0 <= 246: + cff_push(ip, f64(int(b0) - 139)) or_return + i += 1 + case b0 <= 250: + if i + 1 >= len(code) { return .Bad_Cff } + cff_push(ip, f64((int(b0) - 247) * 256 + int(code[i + 1]) + 108)) or_return + i += 2 + case b0 <= 254: + if i + 1 >= len(code) { return .Bad_Cff } + cff_push(ip, f64(-(int(b0) - 251) * 256 - int(code[i + 1]) - 108)) or_return + i += 2 + case: + // 255 is a 16.16 fixed-point number, the only fractional + // operand a charstring has. + if i + 4 >= len(code) { return .Bad_Cff } + u := u32(code[i + 1]) << 24 | u32(code[i + 2]) << 16 | + u32(code[i + 3]) << 8 | u32(code[i + 4]) + cff_push(ip, f64(i32(u)) / 65536) or_return + i += 5 + } + continue + } + + i += 1 + + // -- operators ------------------------------------------------------ + switch b0 { + case 1, 3, 18, 23: // hstem, vstem, hstemhm, vstemhm + cff_take_width(ip, (ip.sp / 2) * 2) + ip.n_stems += ip.sp / 2 + ip.sp = 0 + + case 19, 20: // hintmask, cntrmask + // The arguments still on the stack are an implied vstem list. + cff_take_width(ip, (ip.sp / 2) * 2) + ip.n_stems += ip.sp / 2 + ip.sp = 0 + n_bytes := (ip.n_stems + 7) / 8 + if i + n_bytes > len(code) { return .Bad_Cff } + i += n_bytes + + case 21: // rmoveto + cff_take_width(ip, 2) + if ip.sp >= 2 { + ip.x += s[ip.sp - 2] + ip.y += s[ip.sp - 1] + } + cff_moveto(ip) + ip.sp = 0 + + case 22: // hmoveto + cff_take_width(ip, 1) + if ip.sp >= 1 { ip.x += s[ip.sp - 1] } + cff_moveto(ip) + ip.sp = 0 + + case 4: // vmoveto + cff_take_width(ip, 1) + if ip.sp >= 1 { ip.y += s[ip.sp - 1] } + cff_moveto(ip) + ip.sp = 0 + + case 5: // rlineto + for k := 0; k + 1 < ip.sp; k += 2 { + ip.x += s[k] + ip.y += s[k + 1] + cff_lineto(ip) + } + ip.sp = 0 + + case 6, 7: // hlineto, vlineto + horiz := b0 == 6 + for k in 0 ..< ip.sp { + if horiz { ip.x += s[k] } else { ip.y += s[k] } + cff_lineto(ip) + horiz = !horiz + } + ip.sp = 0 + + case 8: // rrcurveto + for k := 0; k + 5 < ip.sp; k += 6 { + cff_rrcurve(ip, s[k], s[k+1], s[k+2], s[k+3], s[k+4], s[k+5]) + } + ip.sp = 0 + + case 24: // rcurveline + k := 0 + for k + 6 <= ip.sp - 2 { + cff_rrcurve(ip, s[k], s[k+1], s[k+2], s[k+3], s[k+4], s[k+5]) + k += 6 + } + if k + 2 <= ip.sp { + ip.x += s[k] + ip.y += s[k + 1] + cff_lineto(ip) + } + ip.sp = 0 + + case 25: // rlinecurve + k := 0 + for k + 2 <= ip.sp - 6 { + ip.x += s[k] + ip.y += s[k + 1] + cff_lineto(ip) + k += 2 + } + if k + 6 <= ip.sp { + cff_rrcurve(ip, s[k], s[k+1], s[k+2], s[k+3], s[k+4], s[k+5]) + } + ip.sp = 0 + + case 26: // vvcurveto + k := 0 + dx1 := 0.0 + if ip.sp % 4 == 1 { + dx1 = s[0] + k = 1 + } + for ; k + 3 < ip.sp; k += 4 { + x1 := ip.x + dx1 + y1 := ip.y + s[k] + x2 := x1 + s[k + 1] + y2 := y1 + s[k + 2] + ip.x = x2 + ip.y = y2 + s[k + 3] + cff_curveto(ip, x1, y1, x2, y2) + dx1 = 0 + } + ip.sp = 0 + + case 27: // hhcurveto + k := 0 + dy1 := 0.0 + if ip.sp % 4 == 1 { + dy1 = s[0] + k = 1 + } + for ; k + 3 < ip.sp; k += 4 { + x1 := ip.x + s[k] + y1 := ip.y + dy1 + x2 := x1 + s[k + 1] + y2 := y1 + s[k + 2] + ip.x = x2 + s[k + 3] + ip.y = y2 + cff_curveto(ip, x1, y1, x2, y2) + dy1 = 0 + } + ip.sp = 0 + + case 30, 31: // vhcurveto, hvcurveto + horiz := b0 == 31 + k := 0 + for k + 3 < ip.sp { + // The odd 5th argument of the last group is the other axis of + // the final point, which is otherwise implied to be unchanged. + last := ip.sp - k == 5 + x1, y1, x2, y2: f64 + if horiz { + x1 = ip.x + s[k] + y1 = ip.y + x2 = x1 + s[k + 1] + y2 = y1 + s[k + 2] + ip.y = y2 + s[k + 3] + ip.x = last ? x2 + s[k + 4] : x2 + } else { + x1 = ip.x + y1 = ip.y + s[k] + x2 = x1 + s[k + 1] + y2 = y1 + s[k + 2] + ip.x = x2 + s[k + 3] + ip.y = last ? y2 + s[k + 4] : y2 + } + cff_curveto(ip, x1, y1, x2, y2) + k += 4 + horiz = !horiz + } + ip.sp = 0 + + case 10, 29: // callsubr, callgsubr + if ip.sp < 1 { return .Bad_Cff } + ip.sp -= 1 + n, ok := cff_int(s[ip.sp]) + if !ok { return .Bad_Cff } + idx: ^Cff_Index + bias: int + if b0 == 10 { + if ip.priv == nil { return .Bad_Cff } + idx = &ip.priv.subrs + bias = ip.priv.subr_bias + } else { + idx = &ip.f.gsubrs + bias = ip.f.gsubr_bias + } + body := cff_index_item(idx, n + bias) + if body == nil { return .Bad_Cff } + cff_run(ip, body, depth + 1) or_return + if ip.done { return .None } + + case 11: // return + return .None + + case 14: // endchar + // endchar takes nothing, or the four arguments of the deprecated + // seac composition, either of which may be preceded by the width. + if !ip.have_width { + ip.have_width = true + if (ip.sp == 1 || ip.sp == 5) && ip.priv != nil { + w := ip.priv.nominal_width + s[0] + if is_finite(w) { ip.width = w } + } + } + if ip.sp >= 4 && ip.allow_seac { + ip.seac = {s[ip.sp - 4], s[ip.sp - 3], s[ip.sp - 2], s[ip.sp - 1]} + ip.have_seac = true + } + if ip.open && ip.out != nil { path_close(ip.out) } + ip.open = false + ip.sp = 0 + ip.done = true + return .None + + case 12: // escape + if i >= len(code) { return .Bad_Cff } + b1 := code[i] + i += 1 + cff_escape(ip, b1) or_return + + case: + // 0, 2, 9, 13, 15, 16 and 17 are reserved. Reaching one means the + // operand/operator alternation has lost sync, and anything read + // after it would be invented. + return .Bad_Cff + } + } + return .None +} + +// cff_rrcurve is one relative cubic: two relative control points and a +// relative end point. +@(private) +cff_rrcurve :: proc(ip: ^Cff_Interp, dxa, dya, dxb, dyb, dxc, dyc: f64) { + x1 := ip.x + dxa + y1 := ip.y + dya + x2 := x1 + dxb + y2 := y1 + dyb + ip.x = x2 + dxc + ip.y = y2 + dyc + cff_curveto(ip, x1, y1, x2, y2) +} + +// cff_escape runs the two-byte "12 x" operators: the four flex variants and +// the arithmetic, storage and conditional operators. +// +// The arithmetic set exists for multiple-master interpolation and is close to +// unused in practice, but a charstring that reaches an unimplemented operator +// would silently draw the wrong shape, so all of it is here. +@(private) +cff_escape :: proc(ip: ^Cff_Interp, b1: u8) -> Error { + s := &ip.stack + switch b1 { + case 35: // flex + if ip.sp >= 13 { + cff_rrcurve(ip, s[0], s[1], s[2], s[3], s[4], s[5]) + cff_rrcurve(ip, s[6], s[7], s[8], s[9], s[10], s[11]) + } + ip.sp = 0 + + case 34: // hflex + if ip.sp >= 7 { + y0 := ip.y + x1 := ip.x + s[0] + y1 := ip.y + x2 := x1 + s[1] + y2 := y1 + s[2] + ip.x = x2 + s[3] + ip.y = y2 + cff_curveto(ip, x1, y1, x2, y2) + x3 := ip.x + s[4] + y3 := ip.y + x4 := x3 + s[5] + y4 := y0 + ip.x = x4 + s[6] + ip.y = y0 + cff_curveto(ip, x3, y3, x4, y4) + } + ip.sp = 0 + + case 36: // hflex1 + if ip.sp >= 9 { + y0 := ip.y + x1 := ip.x + s[0] + y1 := ip.y + s[1] + x2 := x1 + s[2] + y2 := y1 + s[3] + ip.x = x2 + s[4] + ip.y = y2 + cff_curveto(ip, x1, y1, x2, y2) + x3 := ip.x + s[5] + y3 := ip.y + x4 := x3 + s[6] + y4 := y3 + s[7] + ip.x = x4 + s[8] + ip.y = y0 + cff_curveto(ip, x3, y3, x4, y4) + } + ip.sp = 0 + + case 37: // flex1 + if ip.sp >= 11 { + x0 := ip.x + y0 := ip.y + dx, dy := 0.0, 0.0 + for k in 0 ..< 5 { + dx += s[k * 2] + dy += s[k * 2 + 1] + } + x1 := ip.x + s[0] + y1 := ip.y + s[1] + x2 := x1 + s[2] + y2 := y1 + s[3] + ip.x = x2 + s[4] + ip.y = y2 + s[5] + cff_curveto(ip, x1, y1, x2, y2) + x3 := ip.x + s[6] + y3 := ip.y + s[7] + x4 := x3 + s[8] + y4 := y3 + s[9] + // The 11th argument is whichever coordinate moved further overall; + // the other returns to where the flex started. + if abs(dx) > abs(dy) { + ip.x = x4 + s[10] + ip.y = y0 + } else { + ip.x = x0 + ip.y = y4 + s[10] + } + cff_curveto(ip, x3, y3, x4, y4) + } + ip.sp = 0 + + case 3: cff_binary(ip, .And) + case 4: cff_binary(ip, .Or) + case 5: cff_unary(ip, .Not) + case 9: cff_unary(ip, .Abs) + case 10: cff_binary(ip, .Add) + case 11: cff_binary(ip, .Sub) + case 12: cff_binary(ip, .Div) + case 14: cff_unary(ip, .Neg) + case 15: cff_binary(ip, .Eq) + case 24: cff_binary(ip, .Mul) + case 26: cff_unary(ip, .Sqrt) + + case 18: // drop + if ip.sp > 0 { ip.sp -= 1 } + + case 20: // put + if ip.sp >= 2 { + ip.sp -= 2 + if j, ok := cff_int(s[ip.sp + 1]); ok && j >= 0 && j < CFF_MAX_TRANSIENT { + ip.trans[j] = s[ip.sp] + } + } + + case 21: // get + if ip.sp >= 1 { + v := 0.0 + if j, ok := cff_int(s[ip.sp - 1]); ok && j >= 0 && j < CFF_MAX_TRANSIENT { + v = ip.trans[j] + } + s[ip.sp - 1] = v + } + + case 22: // ifelse + if ip.sp >= 4 { + ip.sp -= 3 + s[ip.sp - 1] = s[ip.sp + 1] <= s[ip.sp + 2] ? s[ip.sp - 1] : s[ip.sp] + } + + case 23: // random + // A deterministic generator: the operator only promises a value in + // (0,1], and a reader that renders the same glyph differently twice is + // worse than one that is boringly repeatable. + ip.seed = ip.seed * 1664525 + 1013904223 + cff_push(ip, f64(ip.seed >> 8) / f64(1 << 24) * 0.999 + 0.001) or_return + + case 27: // dup + if ip.sp >= 1 { + cff_push(ip, s[ip.sp - 1]) or_return + } + + case 28: // exch + if ip.sp >= 2 { + s[ip.sp - 1], s[ip.sp - 2] = s[ip.sp - 2], s[ip.sp - 1] + } + + case 29: // index + if ip.sp >= 1 { + j, ok := cff_int(s[ip.sp - 1]) + if !ok || j < 0 { j = 0 } + if j >= ip.sp - 1 { + s[ip.sp - 1] = 0 + } else { + s[ip.sp - 1] = s[ip.sp - 2 - j] + } + } + + case 30: // roll + cff_roll(ip) + + case: + // Reserved escapes. As with the one-byte operators, continuing past + // one would be guesswork. + return .Bad_Cff + } + return .None +} + +@(private) +Cff_Unary :: enum u8 { Not, Abs, Neg, Sqrt } + +@(private) +Cff_Binary :: enum u8 { And, Or, Add, Sub, Div, Eq, Mul } + +@(private) +cff_unary :: proc(ip: ^Cff_Interp, op: Cff_Unary) { + if ip.sp < 1 { return } + a := ip.stack[ip.sp - 1] + r := 0.0 + switch op { + case .Not: r = a == 0 ? 1 : 0 + case .Abs: r = abs(a) + case .Neg: r = -a + // A negative operand has no square root, and a NaN would poison every + // coordinate it later touches. + case .Sqrt: r = a > 0 ? math.sqrt(a) : 0 + } + ip.stack[ip.sp - 1] = is_finite(r) ? r : 0 +} + +@(private) +cff_binary :: proc(ip: ^Cff_Interp, op: Cff_Binary) { + if ip.sp < 2 { return } + a := ip.stack[ip.sp - 2] + b := ip.stack[ip.sp - 1] + ip.sp -= 1 + r := 0.0 + switch op { + case .And: r = (a != 0 && b != 0) ? 1 : 0 + case .Or: r = (a != 0 || b != 0) ? 1 : 0 + case .Add: r = a + b + case .Sub: r = a - b + case .Div: r = b != 0 ? a / b : 0 + case .Eq: r = a == b ? 1 : 0 + case .Mul: r = a * b + } + ip.stack[ip.sp - 1] = is_finite(r) ? r : 0 +} + +// cff_roll rotates the top N elements of the stack by J places, upwards for a +// positive J (T2 4.4). +@(private) +cff_roll :: proc(ip: ^Cff_Interp) { + if ip.sp < 2 { return } + j_raw := ip.stack[ip.sp - 1] + n_raw := ip.stack[ip.sp - 2] + ip.sp -= 2 + n, n_ok := cff_int(n_raw) + j, j_ok := cff_int(j_raw) + if !n_ok || !j_ok || n <= 0 || n > ip.sp { return } + j %= n + if j < 0 { j += n } + if j == 0 { return } + tmp: [CFF_MAX_STACK]f64 + base := ip.sp - n + for k in 0 ..< n { + tmp[(k + j) % n] = ip.stack[base + k] + } + for k in 0 ..< n { + ip.stack[base + k] = tmp[k] + } +} + +// --------------------------------------------------------------------------- +// Glyphs +// --------------------------------------------------------------------------- + +// cff_private_for is the Private DICT a glyph's charstring runs against. A +// CIDFont picks it per glyph through the FDSelect, which is why a CID font read +// with the wrong FDSelect draws with the wrong subroutines and produces the +// characteristic garbage of a mis-parsed CJK font. +@(private) +cff_private_for :: proc(f: ^Cff_Font, gid: u16) -> ^Cff_Private { + if len(f.fds) > 0 { + fd := 0 + if int(gid) < len(f.fd_select) { fd = int(f.fd_select[gid]) } + if fd >= len(f.fds) { fd = 0 } + return &f.fds[fd] + } + return &f.priv +} + +@(private) +cff_charstring :: proc(f: ^Cff_Font, gid: u16) -> ([]byte, bool) { + if int(gid) >= f.charstrings.count { return nil, false } + return cff_index_item(&f.charstrings, int(gid)), true +} + +// cff_execute runs one glyph's charstring into `ip`, which the caller supplies +// so the 700-odd bytes of interpreter state stay on its stack. +@(private) +cff_execute :: proc( + ip: ^Cff_Interp, + f: ^Cff_Font, + gid: u16, + out: ^Path, + width_only: bool, + allow_seac: bool, +) -> Error { + ip^ = Cff_Interp{ + f = f, + out = out, + width_only = width_only, + allow_seac = allow_seac, + fuel = CFF_MAX_CHARSTRING_OPS, + seed = 0x2545_F491, + } + ip.priv = cff_private_for(f, gid) + ip.width = ip.priv.default_width + if f.charstring_type != 2 { + // CharstringType 1 means the charstrings are Type 1, which is a + // different language with a different interpreter. The font still + // reports its widths and its glyph names; only outlines are lost. + return .Not_Implemented + } + cs, ok := cff_charstring(f, gid) + if !ok { return .No_Glyph } + err := cff_run(ip, cs, 0) + // A charstring that runs off its end without an endchar still leaves a + // usable outline, so its last contour is closed here. + if ip.open && out != nil { + path_close(out) + ip.open = false + } + return err +} + +// cff_glyph_path appends the outline of `gid` to `out`, in glyph space -- the +// charstring's own units, which `cff_font_matrix` maps to text space. +// +// The path is appended, not assigned, so a caller may accumulate several +// glyphs. When an error is returned, `out` holds whatever was decoded before +// the fault: a partly drawn glyph is usually still recognisable, and is always +// better than a page that stops. +@(private) +cff_glyph_path :: proc(f: ^Cff_Font, gid: u16, out: ^Path) -> Error { + if f == nil || out == nil { return .Invalid_Argument } + ip: Cff_Interp + cff_execute(&ip, f, gid, out, false, true) or_return + if ip.have_seac { + return cff_seac_compose(f, ip.seac, out) + } + return .None +} + +// cff_seac_compose draws the two components of a seac -- the deprecated +// "standard encoding accented character", which lives on as the four-argument +// form of endchar (T2 4.1). The two components are named by their positions in +// the Standard Encoding, not by anything in this font's own encoding. +@(private) +cff_seac_compose :: proc(f: ^Cff_Font, args: [4]f64, out: ^Path) -> Error { + adx, ady := args[0], args[1] + bcode, b_ok := cff_int(args[2]) + acode, a_ok := cff_int(args[3]) + if !b_ok || !a_ok || bcode < 0 || bcode > 255 || acode < 0 || acode > 255 { + return .Bad_Cff + } + std := encoding_standard() + bgid, has_b := cff_gid_for_name(f, std[bcode]) + agid, has_a := cff_gid_for_name(f, std[acode]) + if !has_b || !has_a { return .No_Glyph } + + sub: Cff_Interp + cff_execute(&sub, f, bgid, out, false, false) or_return + mark := len(out.points) + cff_execute(&sub, f, agid, out, false, false) or_return + if !is_finite(adx) || !is_finite(ady) { return .None } + shift := Point{adx, ady} + for k in mark ..< len(out.points) { + out.points[k] += shift + } + if mark < len(out.points) { + out.cur += shift + out.start += shift + } + return .None +} + +// cff_glyph_advance is the glyph's horizontal advance in glyph space. +// +// A CFF keeps widths in the charstrings themselves rather than in a table: the +// width is either defaultWidthX, or an optional first argument expressed as a +// difference from nominalWidthX. Finding it therefore means interpreting the +// charstring, but only as far as its first stack-clearing operator. +@(private) +cff_glyph_advance :: proc(f: ^Cff_Font, gid: u16) -> f64 { + if f == nil { return 0 } + ip: Cff_Interp + if err := cff_execute(&ip, f, gid, nil, true, false); err == .No_Glyph { + return 0 + } + return ip.width +} diff --git a/font_cff_test.odin b/font_cff_test.odin new file mode 100644 index 0000000..c556325 --- /dev/null +++ b/font_cff_test.odin @@ -0,0 +1,1621 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:testing" + +// --------------------------------------------------------------------------- +// Fixture +// +// Every font under test is assembled into a local arena and read out of it, so +// a failing assertion cannot leak: the arena goes away on the way out whatever +// happened. +// --------------------------------------------------------------------------- + +@(private = "file") +Cf_Fix :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private = "file") +cff_fix_init :: proc(f: ^Cf_Fix) { + _ = virtual.arena_init_growing(&f.arena) + f.alloc = virtual.arena_allocator(&f.arena) +} + +@(private = "file") +cff_fix_destroy :: proc(f: ^Cf_Fix) { + virtual.arena_destroy(&f.arena) +} + +// --------------------------------------------------------------------------- +// Building fonts to read back +// +// The builder writes every DICT offset in the fixed 5-byte form, so a DICT's +// length does not change when its offsets do and the layout can be computed in +// one pass. It is deliberately not the reader run backwards: offsets, INDEX +// offSize selection and the Subrs-relative-to-Private rule are all written out +// here independently, and one hand-checked font (CFF_MINIMAL below) is a byte +// literal that does not go through the builder at all. +// --------------------------------------------------------------------------- + +@(private = "file") +cb_index :: proc(items: [][]byte, alloc: mem.Allocator) -> []byte { + if len(items) == 0 { + return cb_bytes({0, 0}, alloc) + } + total := 1 + for it in items { total += len(it) } + off_size := 4 + switch { + case total <= 0xFF: off_size = 1 + case total <= 0xFFFF: off_size = 2 + } + out := make([dynamic]u8, 0, 16, alloc) + append(&out, u8(len(items) >> 8), u8(len(items))) + append(&out, u8(off_size)) + cb_off :: proc(out: ^[dynamic]u8, v, size: int) { + for i := size - 1; i >= 0; i -= 1 { + append(out, u8(v >> uint(8 * i))) + } + } + off := 1 + cb_off(&out, off, off_size) + for it in items { + off += len(it) + cb_off(&out, off, off_size) + } + for it in items { append(&out, ..it) } + return out[:] +} + +@(private = "file") +cb_bytes :: proc(b: []u8, alloc: mem.Allocator) -> []byte { + out := make([]byte, len(b), alloc) + copy(out, b) + return out +} + +@(private = "file") +cb_cat :: proc(alloc: mem.Allocator, parts: ..[]byte) -> []byte { + n := 0 + for p in parts { n += len(p) } + out := make([]byte, n, alloc) + i := 0 + for p in parts { + copy(out[i:], p) + i += len(p) + } + return out +} + +// cb_int is the 5-byte DICT integer, used for every offset so that patching one +// cannot change a DICT's length. +@(private = "file") +cb_int :: proc(v: int) -> [5]u8 { + u := u32(i32(v)) + return {29, u8(u >> 24), u8(u >> 16), u8(u >> 8), u8(u)} +} + +// cb_dict appends `operands... operator`, the postfix order a DICT is written +// in. An operator above 255 is one of the two-byte 12/x escapes. +@(private = "file") +cb_dict :: proc(out: ^[dynamic]u8, op: int, operands: ..int) { + for v in operands { + b := cb_int(v) + append(out, ..b[:]) + } + if op >= 0x0C00 { + append(out, 12, u8(op & 0xFF)) + } else { + append(out, u8(op)) + } +} + +@(private = "file") +Cff_Spec :: struct { + charstrings: [][]byte, + charset: []byte, // raw charset table; empty means predefined 0 + encoding: []byte, // raw Encoding table; empty means predefined 0 + strings: [][]byte, + gsubrs: [][]byte, + // Non-CID: one Private DICT, whose extra operators and local subrs these + // are. + priv_extra: []byte, + subrs: [][]byte, + // CID: an FDArray of this many Private DICTs, plus an FDSelect. `ros` puts + // the ROS operator in the Top DICT, which is what marks a CIDFont. + ros: bool, + fdselect: []byte, + fd_extra: [][]byte, + fd_subrs: [][][]byte, + top_extra: []byte, // raw extra Top DICT bytes, e.g. a FontMatrix +} + +// cb_private builds one Private DICT together with the local Subrs INDEX that +// follows it. The Subrs operand is an offset from the start of the Private +// DICT itself (CFF Table 23), which is what `size` below stands for. +@(private = "file") +cb_private :: proc(extra: []byte, subrs: [][]byte, alloc: mem.Allocator) -> (blob: []byte, dict_len: int) { + d := make([dynamic]u8, 0, 32, alloc) + append(&d, ..extra) + if len(subrs) > 0 { + cb_dict(&d, 19, 0) + dict_len = len(d) + // Patch the placeholder now that the DICT's own length is known. + patch := cb_int(dict_len) + copy(d[len(d) - 6:], patch[:]) + return cb_cat(alloc, d[:], cb_index(subrs, alloc)), dict_len + } + return d[:], len(d) +} + +@(private = "file") +cb_build :: proc(spec: Cff_Spec, alloc: mem.Allocator) -> []byte { + hdr := cb_bytes({1, 0, 4, 1}, alloc) + names := cb_index({transmute([]byte)string("Test")}, alloc) + strings_idx := cb_index(spec.strings, alloc) + gsubrs_idx := cb_index(spec.gsubrs, alloc) + cs_idx := cb_index(spec.charstrings, alloc) + + n_fd := len(spec.fd_subrs) + if len(spec.fd_extra) > n_fd { n_fd = len(spec.fd_extra) } + + // Private DICTs, and the font DICTs that point at them for a CIDFont. + privs := make([][]byte, max(n_fd, 1), alloc) + priv_dict_lens := make([]int, max(n_fd, 1), alloc) + if n_fd == 0 { + privs[0], priv_dict_lens[0] = cb_private(spec.priv_extra, spec.subrs, alloc) + } else { + for i in 0 ..< n_fd { + extra: []byte + if i < len(spec.fd_extra) { extra = spec.fd_extra[i] } + subrs: [][]byte + if i < len(spec.fd_subrs) { subrs = spec.fd_subrs[i] } + privs[i], priv_dict_lens[i] = cb_private(extra, subrs, alloc) + } + } + + // A font DICT is always "size offset 18", 12 bytes, whatever the offsets. + fd_dicts := make([][]byte, n_fd, alloc) + for i in 0 ..< n_fd { + d := make([dynamic]u8, 0, 16, alloc) + cb_dict(&d, 18, priv_dict_lens[i], 0) + fd_dicts[i] = d[:] + } + fdarray_idx := n_fd > 0 ? cb_index(fd_dicts, alloc) : nil + + top :: proc(spec: Cff_Spec, charset, encoding, fdselect, charstrings, fdarray, priv_off, priv_len: int, + alloc: mem.Allocator) -> []byte { + d := make([dynamic]u8, 0, 64, alloc) + append(&d, ..spec.top_extra) + if spec.ros { + // ROS: two SIDs and a supplement number. The values do not matter; + // its presence is what declares a CIDFont. + cb_dict(&d, 0x0C00 | 30, 391, 392, 0) + } + if len(spec.charset) > 0 { cb_dict(&d, 15, charset) } + if len(spec.encoding) > 0 { cb_dict(&d, 16, encoding) } + cb_dict(&d, 17, charstrings) + if fdarray > 0 { + cb_dict(&d, 0x0C00 | 36, fdarray) + if len(spec.fdselect) > 0 { cb_dict(&d, 0x0C00 | 37, fdselect) } + } else { + cb_dict(&d, 18, priv_len, priv_off) + } + return d[:] + } + + // First pass: sizes only. Every offset is written 5 bytes wide, so the + // lengths this produces are the lengths the real thing has. + top0 := top(spec, 0, 0, 0, 0, n_fd > 0 ? 1 : 0, 0, priv_dict_lens[0], alloc) + head := len(hdr) + len(names) + len(cb_index({top0}, alloc)) + len(strings_idx) + len(gsubrs_idx) + + charset_off := head + encoding_off := charset_off + len(spec.charset) + fdselect_off := encoding_off + len(spec.encoding) + cs_off := fdselect_off + len(spec.fdselect) + fdarray_off := cs_off + len(cs_idx) + priv_offs := make([]int, len(privs), alloc) + p := fdarray_off + len(fdarray_idx) + for i in 0 ..< len(privs) { + priv_offs[i] = p + p += len(privs[i]) + } + + // Second pass: the same DICTs with their offsets filled in. + for i in 0 ..< n_fd { + d := make([dynamic]u8, 0, 16, alloc) + cb_dict(&d, 18, priv_dict_lens[i], priv_offs[i]) + fd_dicts[i] = d[:] + } + if n_fd > 0 { fdarray_idx = cb_index(fd_dicts, alloc) } + + top1 := top(spec, charset_off, encoding_off, fdselect_off, cs_off, + n_fd > 0 ? fdarray_off : 0, priv_offs[0], priv_dict_lens[0], alloc) + tops := cb_index({top1}, alloc) + + out := make([dynamic]u8, 0, 256, alloc) + append(&out, ..hdr) + append(&out, ..names) + append(&out, ..tops) + append(&out, ..strings_idx) + append(&out, ..gsubrs_idx) + append(&out, ..spec.charset) + append(&out, ..spec.encoding) + append(&out, ..spec.fdselect) + append(&out, ..cs_idx) + if fdarray_idx != nil { append(&out, ..fdarray_idx) } + for i in 0 ..< len(privs) { + if n_fd == 0 && len(privs[i]) == 0 { break } + append(&out, ..privs[i]) + } + return out[:] +} + +// cf_one builds a font whose glyph 1 is the given charstring, which is what +// most of the operator tests want. Glyph 0 is an empty .notdef. +@(private = "file") +cf_one :: proc(t: ^testing.T, fx: ^Cf_Fix, code: []byte, spec := Cff_Spec{}) -> ^Cff_Font { + s := spec + s.charstrings = {{14}, code} + data := cb_build(s, fx.alloc) + f, err := cff_parse(data, fx.alloc) + testing.expect_value(t, err, Error.None) + return f +} + +// cf_path runs a glyph and hands back the outline, so a test reads as +// "charstring in, points out". +@(private = "file") +cf_path :: proc(t: ^testing.T, fx: ^Cf_Fix, code: []byte, spec := Cff_Spec{}) -> (Path, Error) { + f := cf_one(t, fx, code, spec) + p := path_make(fx.alloc) + if f == nil { return p, .Bad_Cff } + err := cff_glyph_path(f, 1, &p) + return p, err +} + +// cf_expect compares an outline against hand-computed verbs and points. +@(private = "file") +cf_expect :: proc(t: ^testing.T, p: ^Path, verbs: []Path_Verb, pts: []Point, loc := #caller_location) { + if !testing.expectf(t, len(p.verbs) == len(verbs), "verbs: got %v, want %v", p.verbs, verbs, loc = loc) { + return + } + for v, i in verbs { + testing.expectf(t, p.verbs[i] == v, "verb %d: got %v, want %v", i, p.verbs[i], v, loc = loc) + } + if !testing.expectf(t, len(p.points) == len(pts), "points: got %v, want %v", p.points, pts, loc = loc) { + return + } + for want, i in pts { + got := p.points[i] + ok := abs(got.x - want.x) < 1e-9 && abs(got.y - want.y) < 1e-9 + testing.expectf(t, ok, "point %d: got %v, want %v", i, got, want, loc = loc) + } +} + +// cs_n is the one-byte charstring operand for a small integer: T2 4 encodes +// -107..107 as the single byte v+139. +@(private = "file") +cs_n :: proc(v: int) -> u8 { + // The assertion is against a bug in a test rather than in the library: a + // value out of range would silently truncate into some other operand or, + // worse, into an operator. + assert(v >= -107 && v <= 107, "cs_n takes -107..107") + return u8(v + 139) +} + +// --------------------------------------------------------------------------- +// A font written out by hand +// +// Generated once by a throwaway script and checked by hand against CFF 6-19. +// Two glyphs: an empty .notdef, and glyph 1 -- named "A" by a format 0 charset +// -- drawing the rectangle (100,200)-(400,600) with an explicit width of 500. +// The Private DICT sets defaultWidthX 250 and nominalWidthX 100, so glyph 1's +// advance is 100+500 and glyph 0's is 250. +// --------------------------------------------------------------------------- + +@(private = "file") +CFF_MINIMAL := [83]u8{ + // header + 0x01, 0x00, 0x04, 0x01, + // Name INDEX: one item, "Test" + 0x00, 0x01, 0x01, 0x01, 0x05, 0x54, 0x65, 0x73, 0x74, + // Top DICT INDEX: charset 45, CharStrings 48, Private (12 bytes at 71) + 0x00, 0x01, 0x01, 0x01, 0x18, 0x1D, 0x00, 0x00, 0x00, 0x2D, 0x0F, 0x1D, + 0x00, 0x00, 0x00, 0x30, 0x11, 0x1D, 0x00, 0x00, 0x00, 0x0C, 0x1D, 0x00, + 0x00, 0x00, 0x47, 0x12, + // String INDEX: empty, and so two bytes long + 0x00, 0x00, + // GlobalSubr INDEX: empty + 0x00, 0x00, + // charset: format 0, glyph 1 is SID 34 = "A" + 0x00, 0x00, 0x22, + // CharStrings INDEX: {endchar}, {500 100 200 rmoveto 300 hlineto + // 400 vlineto -300 hlineto endchar} + 0x00, 0x02, 0x01, 0x01, 0x02, 0x12, 0x0E, 0xF8, 0x88, 0xEF, 0xF7, 0x5C, + 0x15, 0xF7, 0xC0, 0x06, 0xF8, 0x24, 0x07, 0xFB, 0xC0, 0x06, 0x0E, + // Private DICT: defaultWidthX 250, nominalWidthX 100 + 0x1D, 0x00, 0x00, 0x00, 0xFA, 0x14, 0x1D, 0x00, 0x00, 0x00, 0x64, 0x15, +} + +@(test) +cff_test_minimal_font :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + f, err := cff_parse(CFF_MINIMAL[:], fx.alloc) + testing.expect_value(t, err, Error.None) + if f == nil { return } + testing.expect_value(t, cff_glyph_count(f), 2) + testing.expect_value(t, f.is_cid, false) + testing.expect_value(t, cff_font_matrix(f), Matrix{0.001, 0, 0, 0.001, 0, 0}) + + gid, ok := cff_gid_for_name(f, "A") + testing.expect_value(t, ok, true) + testing.expect_value(t, gid, u16(1)) + _, missing := cff_gid_for_name(f, "B") + testing.expect_value(t, missing, false) + + // The built-in encoding is the Standard one, which puts "A" at code 65. + code_gid, code_ok := cff_gid_for_code(f, 65) + testing.expect_value(t, code_ok, true) + testing.expect_value(t, code_gid, u16(1)) + testing.expect_value(t, cff_builtin_encoding(f)[65], "A") + + p := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p), Error.None) + cf_expect(t, &p, + {.Move, .Line, .Line, .Line, .Close}, + {{100, 200}, {400, 200}, {400, 600}, {100, 600}}) + + testing.expect_value(t, cff_glyph_advance(f, 1), 600.0) + testing.expect_value(t, cff_glyph_advance(f, 0), 250.0) + // Past the end of the CharStrings INDEX: no glyph, and no crash. + testing.expect_value(t, cff_glyph_advance(f, 9), 0.0) + q := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 9, &q), Error.No_Glyph) +} + +// --------------------------------------------------------------------------- +// INDEX +// --------------------------------------------------------------------------- + +@(test) +cff_test_index_empty :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // CFF 5: an empty INDEX is a count of zero and nothing else -- no offSize + // byte. Reading one as three bytes would push every later structure out. + data := []byte{0xAA, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x5A} + idx, err := cff_index_read(data, 1, fx.alloc) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, idx.count, 0) + testing.expect_value(t, idx.end, 3) + + next, err2 := cff_index_read(data, idx.end, fx.alloc) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, next.count, 1) + testing.expect_value(t, string(cff_index_item(&next, 0)), "Z") +} + +@(test) +cff_test_index_items :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // count 3, offSize 1, offsets 1,3,5,6 over "abcde" + data := []byte{0x00, 0x03, 0x01, 1, 3, 5, 6, 'a', 'b', 'c', 'd', 'e'} + idx, err := cff_index_read(data, 0, fx.alloc) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, idx.count, 3) + testing.expect_value(t, string(cff_index_item(&idx, 0)), "ab") + testing.expect_value(t, string(cff_index_item(&idx, 1)), "cd") + testing.expect_value(t, string(cff_index_item(&idx, 2)), "e") + testing.expect_value(t, idx.end, len(data)) + testing.expect_value(t, len(cff_index_item(&idx, 3)), 0) + testing.expect_value(t, len(cff_index_item(&idx, -1)), 0) + + // Two-byte offsets over the same items. + wide := []byte{0x00, 0x02, 0x02, 0, 1, 0, 3, 0, 6, 'a', 'b', 'c', 'd', 'e'} + idx2, err2 := cff_index_read(wide, 0, fx.alloc) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, string(cff_index_item(&idx2, 0)), "ab") + testing.expect_value(t, string(cff_index_item(&idx2, 1)), "cde") +} + +@(test) +cff_test_index_bad_offsets :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Offsets that run off the end are clamped to it, so the items they name + // come back empty instead of out of bounds. + past := []byte{0x00, 0x02, 0x01, 1, 200, 250, 'a', 'b', 'c'} + idx, err := cff_index_read(past, 0, fx.alloc) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(cff_index_item(&idx, 0)), "abc") + testing.expect_value(t, len(cff_index_item(&idx, 1)), 0) + testing.expect(t, idx.end <= len(past), "INDEX ended past the data") + + // Offsets that run backwards are clamped to be non-decreasing. + back := []byte{0x00, 0x02, 0x01, 3, 5, 2, 'a', 'b', 'c', 'd', 'e'} + idx2, err2 := cff_index_read(back, 0, fx.alloc) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, string(cff_index_item(&idx2, 0)), "cd") + testing.expect_value(t, len(cff_index_item(&idx2, 1)), 0) + + // A count that cannot fit its own offset array is truncation, not a + // 400 MB allocation. + huge := []byte{0xFF, 0xFF, 0x04, 1, 2, 3} + _, err3 := cff_index_read(huge, 0, fx.alloc) + testing.expect_value(t, err3, Error.Truncated) + + // offSize outside 1..4 is not recoverable: the offsets cannot be located. + bad := []byte{0x00, 0x01, 0x09, 1, 2, 'a'} + _, err4 := cff_index_read(bad, 0, fx.alloc) + testing.expect_value(t, err4, Error.Bad_Cff) + + for cut in 0 ..= len(past) { + _, _ = cff_index_read(past[:cut], 0, fx.alloc) + } +} + +@(test) +cff_test_subr_bias :: proc(t: ^testing.T) { + // T2 4.7. The thresholds are exclusive on the low side: 1239 subroutines + // still bias by 107. + testing.expect_value(t, cff_subr_bias(0), 107) + testing.expect_value(t, cff_subr_bias(1239), 107) + testing.expect_value(t, cff_subr_bias(1240), 1131) + testing.expect_value(t, cff_subr_bias(33899), 1131) + testing.expect_value(t, cff_subr_bias(33900), 32768) +} + +// --------------------------------------------------------------------------- +// DICT +// --------------------------------------------------------------------------- + +@(test) +cff_test_dict_operands :: proc(t: ^testing.T) { + // Every integer form CFF 4 defines, then a two-byte operator. + data := []byte{ + 139, // 0 + 32, // -107 + 246, // 107 + 247, 0, // 108 + 250, 255, // 1131 + 251, 0, // -108 + 254, 255, // -1131 + 28, 0x7F, 0xFF, // 32767 + 28, 0x80, 0x00, // -32768 + 29, 0x00, 0x01, 0x00, 0x00, // 65536 + 29, 0xFF, 0xFF, 0xFF, 0xFF, // -1 + 12, 7, // operator 12 7 + } + it := Cff_Dict_Iter{data = data} + op, ok := cff_dict_next(&it) + testing.expect_value(t, ok, true) + testing.expect_value(t, op, u16(CFF_ESC | 7)) + testing.expect_value(t, it.n, 11) + want := []f64{0, -107, 107, 108, 1131, -108, -1131, 32767, -32768, 65536, -1} + for v, i in want { + testing.expect_value(t, it.ops[i], v) + } + + _, more := cff_dict_next(&it) + testing.expect_value(t, more, false) +} + +@(test) +cff_test_dict_reals :: proc(t: ^testing.T) { + // CFF Table 5: nibbles 0-9 are digits, a is '.', b is 'E', c is 'E-', + // e is '-', f ends the number. + check :: proc(t: ^testing.T, bytes: []byte, want: f64, loc := #caller_location) { + it := Cff_Dict_Iter{data = bytes} + op, ok := cff_dict_next(&it) + testing.expectf(t, ok, "no operator", loc = loc) + testing.expectf(t, op == 5, "operator %v", op, loc = loc) + if it.n < 1 { + testing.expectf(t, false, "no operand", loc = loc) + return + } + testing.expectf(t, abs(it.ops[0] - want) < 1e-12, + "got %v, want %v", it.ops[0], want, loc = loc) + } + check(t, {30, 0x0A, 0x00, 0x05, 0xFF, 5}, 0.0005) // 0.0005 + check(t, {30, 0x1A, 0x5C, 0x3F, 5}, 0.0015) // 1.5E-3 + check(t, {30, 0xE2, 0xA5, 0xFF, 5}, -2.5) // -2.5 + check(t, {30, 0x1B, 0x2F, 5}, 100) // 1E2 + check(t, {30, 0x0F, 5}, 0) // "0" + // A real that never terminates runs to the end of the DICT rather than + // past it; the operator is then missing, which ends the iteration. + it := Cff_Dict_Iter{data = {30, 0x11, 0x11}} + _, ok := cff_dict_next(&it) + testing.expect_value(t, ok, false) +} + +@(test) +cff_test_dict_desync :: proc(t: ^testing.T) { + // A reserved operand byte means the operand/operator alternation has been + // lost; the iteration stops rather than inventing values. + it := Cff_Dict_Iter{data = {139, 31, 139, 5}} + _, ok := cff_dict_next(&it) + testing.expect_value(t, ok, false) + + // A truncated multi-byte operand is not read past the end. + it2 := Cff_Dict_Iter{data = {29, 1, 2}} + _, ok2 := cff_dict_next(&it2) + testing.expect_value(t, ok2, false) + + it3 := Cff_Dict_Iter{data = {12}} + _, ok3 := cff_dict_next(&it3) + testing.expect_value(t, ok3, false) +} + +// --------------------------------------------------------------------------- +// Charstring operators +// +// Every expected point below is worked out by hand from T2 4.1 and 4.2. The +// glyph starts at the origin with an empty path, and endchar closes the last +// contour, which is why every outline ends in a Close. +// --------------------------------------------------------------------------- + +@(test) +cff_test_moveto_lineto :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // 10 20 rmoveto 30 40 rlineto -30 0 rlineto endchar + p, err := cf_path(t, &fx, {cs_n(10), cs_n(20), 21, + cs_n(30), cs_n(40), 5, + cs_n(-30), cs_n(0), 5, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Line, .Line, .Close}, {{10, 20}, {40, 60}, {10, 60}}) + + // 50 hmoveto 10 hlineto endchar + p2, err2 := cf_path(t, &fx, {cs_n(50), 22, cs_n(10), 6, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Line, .Close}, {{50, 0}, {60, 0}}) + + // 50 vmoveto 10 vlineto endchar + p3, err3 := cf_path(t, &fx, {cs_n(50), 4, cs_n(10), 7, 14}) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Line, .Close}, {{0, 50}, {0, 60}}) + + // A second rmoveto ends the first contour and begins another. Its + // displacement is from the current point -- where the line ended, not + // where the previous move began. + p4, err4 := cf_path(t, &fx, {cs_n(10), cs_n(10), 21, cs_n(5), cs_n(0), 5, + cs_n(20), cs_n(20), 21, cs_n(5), cs_n(0), 5, 14}) + testing.expect_value(t, err4, Error.None) + cf_expect(t, &p4, + {.Move, .Line, .Close, .Move, .Line, .Close}, + {{10, 10}, {15, 10}, {35, 30}, {40, 30}}) +} + +@(test) +cff_test_alternating_lines :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // hlineto alternates h,v,h... starting horizontal, one argument each. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, cs_n(10), cs_n(20), cs_n(30), 6, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {10, 0}, {10, 20}, {40, 20}}) + + // vlineto starts vertical. + p2, err2 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, cs_n(10), cs_n(20), 7, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Line, .Line, .Close}, {{0, 0}, {0, 10}, {20, 10}}) +} + +@(test) +cff_test_curves :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // rrcurveto: three relative points. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(10), cs_n(20), cs_n(0), cs_n(10), cs_n(-10), 8, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Curve, .Close}, {{0, 0}, {10, 10}, {30, 10}, {40, 0}}) + + // hhcurveto, even: both tangents horizontal. + p2, err2 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), 27, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Curve, .Close}, {{0, 0}, {10, 0}, {30, 30}, {70, 30}}) + + // hhcurveto, odd: the leading argument is dy1, on the first control point + // of the first curve only. + p3, err3 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(5), cs_n(10), cs_n(20), cs_n(30), cs_n(40), 27, 14}) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Curve, .Close}, {{0, 0}, {10, 5}, {30, 35}, {70, 35}}) + + // vvcurveto, odd: the leading argument is dx1. + p4, err4 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(5), cs_n(10), cs_n(20), cs_n(30), cs_n(40), 26, 14}) + testing.expect_value(t, err4, Error.None) + cf_expect(t, &p4, {.Move, .Curve, .Close}, {{0, 0}, {5, 10}, {25, 40}, {25, 80}}) + + // Two curves from one hhcurveto: dy1 applies only to the first. + p5, err5 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(5), cs_n(10), cs_n(20), cs_n(30), cs_n(40), + cs_n(1), cs_n(2), cs_n(3), cs_n(4), 27, 14}) + testing.expect_value(t, err5, Error.None) + cf_expect(t, &p5, {.Move, .Curve, .Curve, .Close}, + {{0, 0}, {10, 5}, {30, 35}, {70, 35}, {71, 35}, {73, 38}, {77, 38}}) +} + +@(test) +cff_test_alternating_curves :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // hvcurveto, four arguments: starts horizontal, ends vertical. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), 31, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Curve, .Close}, {{0, 0}, {10, 0}, {30, 30}, {30, 70}}) + + // Five: the odd last argument is the other coordinate of the final point. + p2, err2 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), cs_n(50), 31, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Curve, .Close}, {{0, 0}, {10, 0}, {30, 30}, {80, 70}}) + + // Eight: two curves, the second starting vertical. + p3, err3 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), + cs_n(50), cs_n(60), cs_n(70), cs_n(80), 31, 14}) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Curve, .Curve, .Close}, + {{0, 0}, {10, 0}, {30, 30}, {30, 70}, {30, 120}, {90, 190}, {170, 190}}) + + // vhcurveto is the same alternation started the other way round. + p4, err4 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), 30, 14}) + testing.expect_value(t, err4, Error.None) + cf_expect(t, &p4, {.Move, .Curve, .Close}, {{0, 0}, {0, 10}, {20, 40}, {60, 40}}) +} + +@(test) +cff_test_curve_line_mixtures :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // rcurveline: curves, then exactly one line from the last two arguments. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), + cs_n(20), cs_n(0), 24, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Curve, .Line, .Close}, + {{0, 0}, {10, 10}, {20, 20}, {30, 30}, {50, 30}}) + + // rlinecurve: lines, then exactly one curve from the last six. + p2, err2 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(0), + cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), 25, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Line, .Curve, .Close}, + {{0, 0}, {10, 0}, {20, 10}, {30, 20}, {40, 30}}) + + // Two lines then the curve. + p3, err3 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(0), cs_n(0), cs_n(10), + cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), 25, 14}) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Line, .Line, .Curve, .Close}, + {{0, 0}, {10, 0}, {10, 10}, {20, 20}, {30, 30}, {40, 40}}) +} + +@(test) +cff_test_flex :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // flex: two curves and a flex depth that is read and ignored. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), cs_n(-10), + cs_n(10), cs_n(-10), cs_n(10), cs_n(10), cs_n(10), cs_n(10), + cs_n(50), 12, 35, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Curve, .Curve, .Close}, + {{0, 0}, {10, 10}, {20, 20}, {30, 10}, {40, 0}, {50, 10}, {60, 20}}) + + // hflex: seven arguments, and the curve returns to the starting y. + p2, err2 := cf_path(t, &fx, {cs_n(0), cs_n(5), 21, + cs_n(10), cs_n(20), cs_n(30), cs_n(40), cs_n(50), cs_n(60), cs_n(70), + 12, 34, 14}) + testing.expect_value(t, err2, Error.None) + cf_expect(t, &p2, {.Move, .Curve, .Curve, .Close}, + {{0, 5}, {10, 5}, {30, 35}, {70, 35}, {120, 35}, {180, 5}, {250, 5}}) + + // hflex1: nine arguments, and again the end returns to the starting y. + p3, err3 := cf_path(t, &fx, {cs_n(0), cs_n(7), 21, + cs_n(10), cs_n(5), cs_n(20), cs_n(30), cs_n(40), + cs_n(50), cs_n(60), cs_n(-20), cs_n(70), 12, 36, 14}) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Curve, .Curve, .Close}, + {{0, 7}, {10, 12}, {30, 42}, {70, 42}, {120, 42}, {180, 22}, {250, 7}}) + + // flex1: the eleventh argument goes on whichever axis moved further; here + // dx totals 150 and dy totals 5, so it is dx6 and y returns to the start. + p4, err4 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(10), cs_n(5), cs_n(20), cs_n(10), cs_n(30), cs_n(-5), + cs_n(40), cs_n(-10), cs_n(50), cs_n(5), cs_n(60), 12, 37, 14}) + testing.expect_value(t, err4, Error.None) + cf_expect(t, &p4, {.Move, .Curve, .Curve, .Close}, + {{0, 0}, {10, 5}, {30, 15}, {60, 10}, {100, 0}, {150, 5}, {210, 0}}) + + // flex1 the other way up: dy dominates, so the last argument is dy6 and x + // returns to where the flex began. + p5, err5 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, + cs_n(5), cs_n(10), cs_n(10), cs_n(20), cs_n(-5), cs_n(30), + cs_n(-10), cs_n(40), cs_n(5), cs_n(50), cs_n(60), 12, 37, 14}) + testing.expect_value(t, err5, Error.None) + cf_expect(t, &p5, {.Move, .Curve, .Curve, .Close}, + {{0, 0}, {5, 10}, {15, 30}, {10, 60}, {0, 100}, {5, 150}, {0, 210}}) +} + +// --------------------------------------------------------------------------- +// The width argument +// --------------------------------------------------------------------------- + +// cf_widths builds a Private DICT carrying just defaultWidthX and nominalWidthX. +@(private = "file") +cf_widths :: proc(fx: ^Cf_Fix, default_w, nominal_w: int) -> []byte { + d := make([dynamic]u8, 0, 16, fx.alloc) + cb_dict(&d, 20, default_w) + cb_dict(&d, 21, nominal_w) + return d[:] +} + +@(test) +cff_test_width_argument :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + spec := Cff_Spec{priv_extra = cf_widths(&fx, 250, 100)} + + // Two arguments to rmoveto: no width, so defaultWidthX stands. + f1 := cf_one(t, &fx, {cs_n(100), cs_n(60), 21, 14}, spec) + testing.expect_value(t, cff_glyph_advance(f1, 1), 250.0) + + // Three: the first is the width, as a difference from nominalWidthX, and + // the move still uses the last two. + f2 := cf_one(t, &fx, {cs_n(50), cs_n(100), cs_n(60), 21, 14}, spec) + testing.expect_value(t, cff_glyph_advance(f2, 1), 150.0) + p2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f2, 1, &p2), Error.None) + cf_expect(t, &p2, {.Move, .Close}, {{100, 60}}) + + // One argument to hmoveto is the displacement; two means a width first. + f3 := cf_one(t, &fx, {cs_n(60), cs_n(100), 22, 14}, spec) + testing.expect_value(t, cff_glyph_advance(f3, 1), 160.0) + p3 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f3, 1, &p3), Error.None) + cf_expect(t, &p3, {.Move, .Close}, {{100, 0}}) + + // An odd argument count on a stem operator means a width. + f4 := cf_one(t, &fx, {cs_n(70), cs_n(10), cs_n(20), 1, cs_n(0), cs_n(0), 21, 14}, spec) + testing.expect_value(t, cff_glyph_advance(f4, 1), 170.0) + + // endchar alone may carry one. + f5 := cf_one(t, &fx, {cs_n(80), 14}, spec) + testing.expect_value(t, cff_glyph_advance(f5, 1), 180.0) + + // An empty charstring never says anything about its width. + f6 := cf_one(t, &fx, {}, spec) + testing.expect_value(t, cff_glyph_advance(f6, 1), 250.0) + + // Only the FIRST stack-clearing operator may carry a width. The three + // arguments of the second rmoveto are a displacement from the current + // point and nothing else. + f7 := cf_one(t, &fx, {cs_n(10), cs_n(20), 21, cs_n(50), cs_n(100), cs_n(60), 21, 14}, spec) + testing.expect_value(t, cff_glyph_advance(f7, 1), 250.0) + // cff_glyph_advance stops at the first stack-clearing operator, so it + // could not see a second width even if one were taken. Running the whole + // charstring is what holds the rule to "the first operator, and no other". + ip: Cff_Interp + testing.expect_value(t, cff_execute(&ip, f7, 1, nil, false, false), Error.None) + testing.expect_value(t, ip.width, 250.0) + p7 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f7, 1, &p7), Error.None) + cf_expect(t, &p7, {.Move, .Close, .Move, .Close}, {{10, 20}, {110, 80}}) +} + +// --------------------------------------------------------------------------- +// Hints +// --------------------------------------------------------------------------- + +@(test) +cff_test_hintmask_implicit_vstems :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Eight hstem hints, then a hintmask with two arguments left on the stack. + // Those are an implied vstem, making nine hints and so a two-byte mask. + // The second mask byte here is 0x15, which is the rmoveto operator: a + // reader that consumed only one mask byte would execute it and leave an + // extra contour in the outline. + code := make([dynamic]u8, 0, 40, fx.alloc) + for i in 1 ..= 16 { append(&code, cs_n(i)) } + append(&code, 18) // hstemhm: 8 hints + append(&code, cs_n(30), cs_n(40), 19, 0xFF, 0x15) // hintmask + 2 mask bytes + append(&code, cs_n(10), cs_n(20), 21, 14) // 10 20 rmoveto endchar + + p, err := cf_path(t, &fx, code[:]) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Close}, {{10, 20}}) + + // A mask that runs past the end of the charstring is a fault, not a read + // past the end of the buffer. + _, err2 := cf_path(t, &fx, {cs_n(10), cs_n(20), 1, 19}) + testing.expect_value(t, err2, Error.Bad_Cff) + + // cntrmask counts and skips exactly as hintmask does. + code2 := make([dynamic]u8, 0, 40, fx.alloc) + for i in 1 ..= 16 { append(&code2, cs_n(i)) } + append(&code2, 18) + append(&code2, cs_n(30), cs_n(40), 20, 0xFF, 0x15) + append(&code2, cs_n(10), cs_n(20), 21, 14) + p3, err3 := cf_path(t, &fx, code2[:]) + testing.expect_value(t, err3, Error.None) + cf_expect(t, &p3, {.Move, .Close}, {{10, 20}}) + + // A width may precede the implied vstem arguments, and is recognised by + // the argument count being odd. + code4 := make([dynamic]u8, 0, 8, fx.alloc) + append(&code4, cs_n(90), cs_n(10), cs_n(20), 19, 0x80) + append(&code4, cs_n(0), cs_n(0), 21, 14) + f4 := cf_one(t, &fx, code4[:], Cff_Spec{priv_extra = cf_widths(&fx, 250, 100)}) + testing.expect_value(t, cff_glyph_advance(f4, 1), 190.0) +} + +// --------------------------------------------------------------------------- +// Subroutines +// --------------------------------------------------------------------------- + +// cf_subrs builds `count` subroutines of which only the first does anything, +// so that which index a biased operand selects is visible in the outline. +@(private = "file") +cf_subrs :: proc(fx: ^Cf_Fix, count: int, first: []byte) -> [][]byte { + out := make([][]byte, count, fx.alloc) + out[0] = first + return out +} + +@(test) +cff_test_subr_bias_thresholds :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // 10 20 rmoveto, then return to the caller. + draw := []byte{cs_n(10), cs_n(20), 21, 11} + want_verbs := []Path_Verb{.Move, .Close} + want_pts := []Point{{10, 20}} + + // 1239 local subroutines: bias 107, so operand -107 is subroutine 0. + f1 := cf_one(t, &fx, {cs_n(-107), 10, 14}, Cff_Spec{subrs = cf_subrs(&fx, 1239, draw)}) + p1 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f1, 1, &p1), Error.None) + cf_expect(t, &p1, want_verbs, want_pts) + + // 1240: the bias becomes 1131. The same operand now selects subroutine + // 1024, which is empty, and -1131 selects subroutine 0. + spec2 := Cff_Spec{subrs = cf_subrs(&fx, 1240, draw)} + f2 := cf_one(t, &fx, {cs_n(-107), 10, 14}, spec2) + p2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f2, 1, &p2), Error.None) + testing.expect_value(t, len(p2.verbs), 0) + + f3 := cf_one(t, &fx, {254, 255, 10, 14}, spec2) + p3 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f3, 1, &p3), Error.None) + cf_expect(t, &p3, want_verbs, want_pts) + + // 33900: the bias becomes 32768, reached with the two-byte 28 operand. + spec4 := Cff_Spec{subrs = cf_subrs(&fx, 33900, draw)} + f4 := cf_one(t, &fx, {28, 0x80, 0x00, 10, 14}, spec4) + p4 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f4, 1, &p4), Error.None) + cf_expect(t, &p4, want_verbs, want_pts) + + f5 := cf_one(t, &fx, {254, 255, 10, 14}, spec4) + p5 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f5, 1, &p5), Error.None) + testing.expect_value(t, len(p5.verbs), 0) + + // Global subroutines are biased by their own count, independently. + f6 := cf_one(t, &fx, {cs_n(-107), 29, 14}, Cff_Spec{gsubrs = cf_subrs(&fx, 3, draw)}) + p6 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f6, 1, &p6), Error.None) + cf_expect(t, &p6, want_verbs, want_pts) + + // A subroutine index outside the INDEX is an error, not a wild read. + f7 := cf_one(t, &fx, {cs_n(107), 29, 14}, Cff_Spec{gsubrs = cf_subrs(&fx, 3, draw)}) + p7 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f7, 1, &p7), Error.Bad_Cff) +} + +@(test) +cff_test_subr_cycles_terminate :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // A subroutine that calls itself: bounded by the depth cap, not by luck. + self := []byte{cs_n(-107), 29, 11} + f := cf_one(t, &fx, {cs_n(-107), 29, 14}, Cff_Spec{gsubrs = cf_subrs(&fx, 1, self)}) + p := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p), Error.Bad_Cff) + + // Two subroutines that call each other. + pair := make([][]byte, 2, fx.alloc) + pair[0] = {cs_n(-106), 29, 11} // -106 + 107 = 1 + pair[1] = {cs_n(-107), 29, 11} // -107 + 107 = 0 + f2 := cf_one(t, &fx, {cs_n(-107), 29, 14}, Cff_Spec{gsubrs = pair}) + p2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f2, 1, &p2), Error.Bad_Cff) + + // Depth exactly at the cap is still allowed: ten nested calls draw. + nest := make([][]byte, 12, fx.alloc) + nest[11] = {cs_n(10), cs_n(20), 21, 11} + for i in 0 ..< 11 { + nest[i] = {cs_n(i + 1 - 107), 29, 11} + } + f3 := cf_one(t, &fx, {cs_n(-107), 29, 14}, Cff_Spec{gsubrs = nest}) + p3 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f3, 1, &p3), Error.None) + cf_expect(t, &p3, {.Move, .Close}, {{10, 20}}) +} + +@(test) +cff_test_stack_overflow :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // 48 operands is the limit T2 sets, and 24 line segments come out of it. + ok_code := make([dynamic]u8, 0, 64, fx.alloc) + append(&ok_code, cs_n(0), cs_n(0), 21) + for _ in 0 ..< 48 { append(&ok_code, cs_n(1)) } + append(&ok_code, 5, 14) + p, err := cf_path(t, &fx, ok_code[:]) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(p.verbs), 26) // one move, 24 lines, one close + + // 49 overruns it, and the glyph stops rather than writing past the stack. + bad := make([dynamic]u8, 0, 64, fx.alloc) + append(&bad, cs_n(0), cs_n(0), 21) + for _ in 0 ..< 49 { append(&bad, cs_n(1)) } + append(&bad, 5, 14) + _, err2 := cf_path(t, &fx, bad[:]) + testing.expect_value(t, err2, Error.Bad_Cff) + + // The same overrun spread across a subroutine call. + push := make([dynamic]u8, 0, 64, fx.alloc) + for _ in 0 ..< 40 { append(&push, cs_n(1)) } + append(&push, 11) + long := make([dynamic]u8, 0, 16, fx.alloc) + for _ in 0 ..< 40 { append(&long, cs_n(1)) } + append(&long, cs_n(-107), 29, 5, 14) + f := cf_one(t, &fx, long[:], Cff_Spec{gsubrs = cf_subrs(&fx, 1, push[:])}) + p3 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p3), Error.Bad_Cff) +} + +@(private = "file") +cf_build_parse :: proc(t: ^testing.T, fx: ^Cf_Fix, spec: Cff_Spec) -> ^Cff_Font { + data := cb_build(spec, fx.alloc) + f, err := cff_parse(data, fx.alloc) + testing.expect_value(t, err, Error.None) + return f +} + +@(test) +cff_test_fuel_stops_exponential_calls :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Nine subroutines, each calling the next ten times. Nothing here is + // cyclic and nothing exceeds the depth cap, but the call tree has 10^8 + // leaves, so only the operation budget stops it. + subrs := make([][]byte, 9, fx.alloc) + subrs[8] = {cs_n(1), cs_n(1), 21, 11} + for i in 0 ..< 8 { + b := make([dynamic]u8, 0, 24, fx.alloc) + for _ in 0 ..< 10 { + append(&b, cs_n(i + 1 - 107), 29) + } + append(&b, 11) + subrs[i] = b[:] + } + f := cf_one(t, &fx, {cs_n(-107), 29, 14}, Cff_Spec{gsubrs = subrs}) + p := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p), Error.Bad_Cff) +} + +// --------------------------------------------------------------------------- +// seac +// --------------------------------------------------------------------------- + +@(test) +cff_test_seac :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + base := []byte{cs_n(0), cs_n(0), 21, cs_n(10), cs_n(0), 5, 14} + accent := []byte{cs_n(0), cs_n(0), 21, cs_n(5), cs_n(0), 5, 14} + // 100 200 65 194 endchar: compose the glyphs the Standard Encoding puts at + // codes 65 ("A") and 194 ("acute"), the accent displaced by (100,200). + composite := []byte{239, 247, 92, cs_n(65), 247, 86, 14} + spec := Cff_Spec{ + charstrings = {{14}, base, accent, composite}, + // format 0: SIDs 34 "A", 125 "acute", 1 "space" + charset = {0, 0, 34, 0, 125, 0, 1}, + } + f := cf_build_parse(t, &fx, spec) + if f == nil { return } + + gid, ok := cff_gid_for_name(f, "acute") + testing.expect_value(t, ok, true) + testing.expect_value(t, gid, u16(2)) + + p := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 3, &p), Error.None) + cf_expect(t, &p, + {.Move, .Line, .Close, .Move, .Line, .Close}, + {{0, 0}, {10, 0}, {100, 200}, {105, 200}}) + + // The same composition with a width in front of it: endchar's four seac + // arguments are the LAST four, not the first four. + with_width := []byte{cs_n(55), 239, 247, 92, cs_n(65), 247, 86, 14} + spec_w := spec + spec_w.charstrings = {{14}, base, accent, with_width} + spec_w.priv_extra = cf_widths(&fx, 250, 100) + fw := cf_build_parse(t, &fx, spec_w) + testing.expect_value(t, cff_glyph_advance(fw, 3), 155.0) + pw := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(fw, 3, &pw), Error.None) + cf_expect(t, &pw, + {.Move, .Line, .Close, .Move, .Line, .Close}, + {{0, 0}, {10, 0}, {100, 200}, {105, 200}}) + + // A seac naming a glyph this font does not have degrades to No_Glyph + // rather than drawing something arbitrary. + missing := []byte{239, 247, 92, cs_n(66), 247, 86, 14} + spec2 := spec + spec2.charstrings = {{14}, base, accent, missing} + f2 := cf_build_parse(t, &fx, spec2) + p2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f2, 3, &p2), Error.No_Glyph) +} + +// --------------------------------------------------------------------------- +// CIDFonts +// --------------------------------------------------------------------------- + +@(test) +cff_test_cid_font :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + fd0 := make([][]byte, 1, fx.alloc) + fd0[0] = {cs_n(10), cs_n(20), 21, 11} + fd1 := make([][]byte, 1, fx.alloc) + fd1[0] = {cs_n(30), cs_n(40), 21, 11} + + call := []byte{cs_n(-107), 10, 14} + spec := Cff_Spec{ + ros = true, + charstrings = {{14}, call, call, {14}}, + // charset format 2: one range, first CID 100, nLeft 2, so glyphs 1..3 + // carry CIDs 100..102. + charset = {2, 0, 100, 0, 2}, + // FDSelect format 3: glyphs 0-1 use font DICT 0, glyphs 2-3 use 1. + fdselect = {3, 0, 2, 0, 0, 0, 0, 2, 1, 0, 4}, + fd_extra = {cf_widths(&fx, 111, 0), cf_widths(&fx, 222, 0)}, + fd_subrs = {fd0, fd1}, + } + f := cf_build_parse(t, &fx, spec) + if f == nil { return } + testing.expect_value(t, f.is_cid, true) + testing.expect_value(t, len(f.fds), 2) + + // The charset maps CIDs, not names. + check :: proc(t: ^testing.T, f: ^Cff_Font, cid, want: u16, loc := #caller_location) { + gid, ok := cff_gid_for_cid(f, cid) + testing.expectf(t, ok, "CID %d not found", cid, loc = loc) + testing.expectf(t, gid == want, "CID %d -> gid %d, want %d", cid, gid, want, loc = loc) + } + check(t, f, 100, 1) + check(t, f, 101, 2) + check(t, f, 102, 3) + _, has99 := cff_gid_for_cid(f, 99) + testing.expect_value(t, has99, false) + _, has_name := cff_gid_for_name(f, "A") + testing.expect_value(t, has_name, false) + testing.expect_value(t, cff_builtin_encoding(f)[65], "") + + // Each glyph takes its local subroutines from its own font DICT: the same + // charstring draws different things in the two FDSelect ranges. + p1 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p1), Error.None) + cf_expect(t, &p1, {.Move, .Close}, {{10, 20}}) + + p2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 2, &p2), Error.None) + cf_expect(t, &p2, {.Move, .Close}, {{30, 40}}) + + // And its widths. + testing.expect_value(t, cff_glyph_advance(f, 1), 111.0) + testing.expect_value(t, cff_glyph_advance(f, 2), 222.0) + + // FDSelect format 0 is one byte per glyph. + spec0 := spec + spec0.fdselect = {0, 0, 0, 1, 1} + f0 := cf_build_parse(t, &fx, spec0) + q1 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f0, 1, &q1), Error.None) + cf_expect(t, &q1, {.Move, .Close}, {{10, 20}}) + q2 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f0, 2, &q2), Error.None) + cf_expect(t, &q2, {.Move, .Close}, {{30, 40}}) + + // A truncated FDSelect leaves the glyphs it did not reach on font DICT 0 + // instead of reading past its end. + specx := spec + specx.fdselect = {3, 0, 9, 0, 0, 0} + fx2 := cf_build_parse(t, &fx, specx) + q3 := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(fx2, 2, &q3), Error.None) + cf_expect(t, &q3, {.Move, .Close}, {{10, 20}}) +} + +@(test) +cff_test_cid_identity_charset :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // A CIDFont with no charset of its own maps CID to GID directly, which is + // what a subset font using Identity-H expects. + fd0 := make([][]byte, 1, fx.alloc) + fd0[0] = {cs_n(1), cs_n(2), 21, 11} + f := cf_build_parse(t, &fx, Cff_Spec{ + ros = true, + charstrings = {{14}, {14}, {14}}, + fd_subrs = {fd0}, + }) + if f == nil { return } + gid, ok := cff_gid_for_cid(f, 2) + testing.expect_value(t, ok, true) + testing.expect_value(t, gid, u16(2)) + _, past := cff_gid_for_cid(f, 3) + testing.expect_value(t, past, false) + + // A font that is not a CIDFont treats the CID as the glyph index, which is + // what a PDF pairing Identity-H with a name-keyed CFF means by it. + f2 := cf_one(t, &fx, {14}) + g2, ok2 := cff_gid_for_cid(f2, 1) + testing.expect_value(t, ok2, true) + testing.expect_value(t, g2, u16(1)) + _, ok3 := cff_gid_for_cid(f2, 2) + testing.expect_value(t, ok3, false) +} + +// --------------------------------------------------------------------------- +// charset, Encoding, strings and the FontMatrix +// --------------------------------------------------------------------------- + +@(test) +cff_test_charset_formats :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + four := [][]byte{{14}, {14}, {14}, {14}} + expect_abc :: proc(t: ^testing.T, f: ^Cff_Font, loc := #caller_location) { + names := []string{"A", "B", "C"} + for name, i in names { + gid, ok := cff_gid_for_name(f, name) + testing.expectf(t, ok, "%s missing", name, loc = loc) + testing.expectf(t, gid == u16(i + 1), "%s -> %d", name, gid, loc = loc) + } + } + // Format 0 lists one SID per glyph. + expect_abc(t, cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = {0, 0, 34, 0, 35, 0, 36}})) + // Format 1 lists ranges with a one-byte nLeft. + expect_abc(t, cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = {1, 0, 34, 2}})) + // Format 2 is the same with a two-byte nLeft. + expect_abc(t, cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = {2, 0, 34, 0, 2}})) + + // A charset that stops early leaves the rest of the glyphs unnamed rather + // than reading past it. + f := cf_build_parse(t, &fx, Cff_Spec{charstrings = four, charset = {0, 0, 34}}) + gid, ok := cff_gid_for_name(f, "A") + testing.expect_value(t, ok, true) + testing.expect_value(t, gid, u16(1)) + _, has_b := cff_gid_for_name(f, "B") + testing.expect_value(t, has_b, false) + + // A name that sorts between two the font does have must not come back as + // whichever neighbour the search landed on. + named := cf_build_parse(t, &fx, Cff_Spec{charstrings = four, charset = {0, 0, 34, 0, 35, 0, 36}}) + for probe in ([]string{"AA", "0", "ZZ", "Bb", ""}) { + gid2, ok2 := cff_gid_for_name(named, probe) + testing.expectf(t, !ok2, "%q matched glyph %d", probe, gid2) + } + + // With no charset at all the predefined ISOAdobe order applies, in which + // SID 1 is "space". + f2 := cf_build_parse(t, &fx, Cff_Spec{charstrings = four}) + sp, sp_ok := cff_gid_for_name(f2, "space") + testing.expect_value(t, sp_ok, true) + testing.expect_value(t, sp, u16(1)) + ex, ex_ok := cff_gid_for_name(f2, "exclam") + testing.expect_value(t, ex_ok, true) + testing.expect_value(t, ex, u16(2)) + + // A SID at or past 391 names an entry of the String INDEX. + f3 := cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, + charset = {0, 1, 135, 1, 136, 0, 36}, // SIDs 391, 392, 36 + strings = {transmute([]byte)string("Custom"), transmute([]byte)string("Other")}, + }) + cu, cu_ok := cff_gid_for_name(f3, "Custom") + testing.expect_value(t, cu_ok, true) + testing.expect_value(t, cu, u16(1)) + ot, ot_ok := cff_gid_for_name(f3, "Other") + testing.expect_value(t, ot_ok, true) + testing.expect_value(t, ot, u16(2)) + // A SID past the end of the String INDEX names nothing at all. + testing.expect_value(t, cff_sid_string(f3, 400), "") + testing.expect_value(t, cff_sid_string(f3, 0), ".notdef") + testing.expect_value(t, cff_sid_string(f3, 390), "Semibold") +} + +@(test) +cff_test_encoding_formats :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + four := [][]byte{{14}, {14}, {14}, {14}} + charset := []byte{0, 0, 34, 0, 35, 0, 36} // A, B, C + + // Format 0: a code per glyph, in glyph order. + f := cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = charset, encoding = {0, 2, 0x41, 0x42}}) + g, ok := cff_gid_for_code(f, 0x41) + testing.expect_value(t, ok, true) + testing.expect_value(t, g, u16(1)) + g2, ok2 := cff_gid_for_code(f, 0x42) + testing.expect_value(t, ok2, true) + testing.expect_value(t, g2, u16(2)) + testing.expect_value(t, cff_builtin_encoding(f)[0x41], "A") + _, unset := cff_gid_for_code(f, 0x43) + testing.expect_value(t, unset, false) + + // Format 1: ranges of consecutive codes. + f2 := cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = charset, encoding = {1, 1, 0x41, 1}}) + h1, h1ok := cff_gid_for_code(f2, 0x41) + testing.expect_value(t, h1ok, true) + testing.expect_value(t, h1, u16(1)) + h2, h2ok := cff_gid_for_code(f2, 0x42) + testing.expect_value(t, h2ok, true) + testing.expect_value(t, h2, u16(2)) + + // The high bit of the format byte adds supplements, which name their glyph + // by SID rather than by position. + f3 := cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = charset, + encoding = {0x80, 1, 0x41, 1, 0x2A, 0, 34}}) + s1, s1ok := cff_gid_for_code(f3, 0x2A) + testing.expect_value(t, s1ok, true) + testing.expect_value(t, s1, u16(1)) + testing.expect_value(t, cff_builtin_encoding(f3)[0x2A], "A") + + // A truncated encoding table stops where the data does. + f4 := cf_build_parse(t, &fx, Cff_Spec{ + charstrings = four, charset = charset, encoding = {0, 9, 0x41}}) + t1, t1ok := cff_gid_for_code(f4, 0x41) + testing.expect_value(t, t1ok, true) + testing.expect_value(t, t1, u16(1)) +} + +@(test) +cff_test_font_matrix :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // No FontMatrix operator: 1/1000 units per em. + testing.expect_value(t, cff_font_matrix(cf_one(t, &fx, {14})), + Matrix{0.001, 0, 0, 0.001, 0, 0}) + + // 0.0005 0 0 0.0005 0 0 FontMatrix, the first and fourth written as the + // nibble-packed reals CFF Table 5 defines. + d := make([dynamic]u8, 0, 32, fx.alloc) + half := []u8{30, 0x0A, 0x00, 0x05, 0xFF} // 0.0005 + zero := cb_int(0) + append(&d, ..half) + append(&d, ..zero[:]) + append(&d, ..zero[:]) + append(&d, ..half) + append(&d, ..zero[:]) + append(&d, ..zero[:]) + append(&d, 12, 7) + f := cf_one(t, &fx, {14}, Cff_Spec{top_extra = d[:]}) + testing.expect_value(t, cff_font_matrix(f), Matrix{0.0005, 0, 0, 0.0005, 0, 0}) + testing.expect_value(t, cff_font_matrix(nil), Matrix{0.001, 0, 0, 0.001, 0, 0}) +} + +// --------------------------------------------------------------------------- +// Arithmetic, storage and conditionals +// --------------------------------------------------------------------------- + +@(test) +cff_test_arithmetic_operators :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Each case computes one relative line from the origin, so the end point + // is the operator's result. + check :: proc(t: ^testing.T, fx: ^Cf_Fix, tail: []byte, want: Point, loc := #caller_location) { + code := make([dynamic]u8, 0, 32, fx.alloc) + append(&code, cs_n(0), cs_n(0), 21) + append(&code, ..tail) + append(&code, 5, 14) + p, err := cf_path(t, fx, code[:]) + testing.expectf(t, err == .None, "error %v", err, loc = loc) + if len(p.points) < 2 { + testing.expectf(t, false, "no line drawn: %v", p.points, loc = loc) + return + } + got := p.points[1] + testing.expectf(t, abs(got.x - want.x) < 1e-9 && abs(got.y - want.y) < 1e-9, + "got %v, want %v", got, want, loc = loc) + } + // (5 + 3) * 2, then 0 + check(t, &fx, {cs_n(5), cs_n(3), 12, 10, cs_n(2), 12, 24, cs_n(0)}, {16, 0}) + // 10 - 3, and 20 / 4 + check(t, &fx, {cs_n(10), cs_n(3), 12, 11, cs_n(20), cs_n(4), 12, 12}, {7, 5}) + // division by zero yields zero rather than an infinity + check(t, &fx, {cs_n(10), cs_n(0), 12, 12, cs_n(0)}, {0, 0}) + // -7 neg, and abs of -9 + check(t, &fx, {cs_n(-7), 12, 14, cs_n(-9), 12, 9}, {7, 9}) + // sqrt 16, and sqrt of a negative is zero, not a NaN + check(t, &fx, {cs_n(16), 12, 26, cs_n(-4), 12, 26}, {4, 0}) + // exch swaps the top two + check(t, &fx, {cs_n(10), cs_n(20), 12, 28}, {20, 10}) + // dup, then drop the copy + check(t, &fx, {cs_n(11), 12, 27, 12, 18, cs_n(3)}, {11, 3}) + // index 1 reaches past the top of the stack + check(t, &fx, {cs_n(7), cs_n(8), cs_n(1), 12, 29, cs_n(0), 5, cs_n(0)}, {7, 8}) + // put 42 at 0, then get it back + check(t, &fx, {cs_n(42), cs_n(0), 12, 20, cs_n(0), 12, 21, cs_n(3)}, {42, 3}) + // 10 20 1 2 ifelse: 1 <= 2, so the first of the two is kept + check(t, &fx, {cs_n(10), cs_n(20), cs_n(1), cs_n(2), 12, 22, cs_n(0)}, {10, 0}) + // 10 20 3 2 ifelse: 3 > 2, so the second is kept + check(t, &fx, {cs_n(10), cs_n(20), cs_n(3), cs_n(2), 12, 22, cs_n(0)}, {20, 0}) + // and / or / not on the truth of a number + check(t, &fx, {cs_n(1), cs_n(0), 12, 3, cs_n(1), cs_n(0), 12, 4}, {0, 1}) + check(t, &fx, {cs_n(0), 12, 5, cs_n(4), cs_n(4), 12, 15}, {1, 1}) + // 3 elements rolled up by 1: 1 2 3 -> 3 1 2 + check(t, &fx, {cs_n(1), cs_n(2), cs_n(3), cs_n(3), cs_n(1), 12, 30, + 12, 18, 5, cs_n(0), cs_n(0)}, {3, 1}) + // A roll of more elements than the stack holds does nothing. + check(t, &fx, {cs_n(4), cs_n(9), cs_n(40), cs_n(1), 12, 30}, {4, 9}) + + // The 16.16 fixed operand is the only fractional number a charstring has. + p, err := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 255, 0x00, 0x01, 0x80, 0x00, cs_n(0), 5, 14}) + testing.expect_value(t, err, Error.None) + cf_expect(t, &p, {.Move, .Line, .Close}, {{0, 0}, {1.5, 0}}) +} + +// --------------------------------------------------------------------------- +// Malformed input +// --------------------------------------------------------------------------- + +@(test) +cff_test_rejects_unreadable :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + _, e1 := cff_parse({}, fx.alloc) + testing.expect_value(t, e1, Error.Truncated) + _, e2 := cff_parse({1, 0, 4}, fx.alloc) + testing.expect_value(t, e2, Error.Truncated) + + // CFF2 is a different format with different charstring operators, so it is + // refused rather than misread as CFF1. + cff2 := make([]byte, len(CFF_MINIMAL), fx.alloc) + copy(cff2, CFF_MINIMAL[:]) + cff2[0] = 2 + _, e3 := cff_parse(cff2, fx.alloc) + testing.expect_value(t, e3, Error.Bad_Cff) + + // A header claiming to be longer than the file. + short := make([]byte, len(CFF_MINIMAL), fx.alloc) + copy(short, CFF_MINIMAL[:]) + short[2] = 0xFF + _, e4 := cff_parse(short, fx.alloc) + testing.expect_value(t, e4, Error.Truncated) + + // A CharStrings offset past the end of the font. + no_cs := make([]byte, len(CFF_MINIMAL), fx.alloc) + copy(no_cs, CFF_MINIMAL[:]) + no_cs[29] = 0xFF // the low byte of the CharStrings offset operand + _, e5 := cff_parse(no_cs, fx.alloc) + testing.expect_value(t, e5, Error.Bad_Cff) +} + +@(test) +cff_test_charstring_index_past_end :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // The CharStrings INDEX at offset 48 has one-byte offsets at 51..53. The + // last one is pushed past the end of the font: the item it names is + // clamped, and the glyph either draws or fails, but nothing is read out of + // bounds. + data := make([]byte, len(CFF_MINIMAL), fx.alloc) + copy(data, CFF_MINIMAL[:]) + data[53] = 0xFF + f, err := cff_parse(data, fx.alloc) + testing.expect_value(t, err, Error.None) + if f == nil { return } + testing.expect_value(t, cff_glyph_count(f), 2) + p := path_make(fx.alloc) + _ = cff_glyph_path(f, 1, &p) + _ = cff_glyph_advance(f, 1) + + // The first offset pushed past the second, so every item is empty. + data2 := make([]byte, len(CFF_MINIMAL), fx.alloc) + copy(data2, CFF_MINIMAL[:]) + data2[51] = 0xF0 + f2, err2 := cff_parse(data2, fx.alloc) + testing.expect_value(t, err2, Error.None) + if f2 == nil { return } + q := path_make(fx.alloc) + _ = cff_glyph_path(f2, 1, &q) +} + +@(test) +cff_test_reserved_operators :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Reserved one-byte operator. + _, e1 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 2, 14}) + testing.expect_value(t, e1, Error.Bad_Cff) + // Reserved two-byte escape. + _, e2 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 12, 40, 14}) + testing.expect_value(t, e2, Error.Bad_Cff) + // An escape with no second byte. + _, e3 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 12}) + testing.expect_value(t, e3, Error.Bad_Cff) + // An operand cut short by the end of the charstring. + _, e4 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 28, 1}) + testing.expect_value(t, e4, Error.Bad_Cff) + _, e5 := cf_path(t, &fx, {cs_n(0), cs_n(0), 21, 255, 1, 2}) + testing.expect_value(t, e5, Error.Bad_Cff) + + // A charstring that simply runs out still leaves its outline closed. + p, e6 := cf_path(t, &fx, {cs_n(10), cs_n(20), 21, cs_n(5), cs_n(0), 5}) + testing.expect_value(t, e6, Error.None) + cf_expect(t, &p, {.Move, .Line, .Close}, {{10, 20}, {15, 20}}) +} + +@(test) +cff_test_charstring_type_1 :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // A CFF whose charstrings are Type 1 is a different language; the font + // still loads, so widths and names survive, but outlines do not. + d := make([dynamic]u8, 0, 16, fx.alloc) + cb_dict(&d, 0x0C00 | 6, 1) + f := cf_one(t, &fx, {cs_n(10), cs_n(20), 21, 14}, Cff_Spec{top_extra = d[:]}) + if f == nil { return } + testing.expect_value(t, f.charstring_type, 1) + p := path_make(fx.alloc) + testing.expect_value(t, cff_glyph_path(f, 1, &p), Error.Not_Implemented) + testing.expect_value(t, len(p.verbs), 0) +} + +@(test) +cff_test_truncation_and_bit_flips :: proc(t: ^testing.T) { + fx: Cf_Fix + cff_fix_init(&fx) + defer cff_fix_destroy(&fx) + + // Every prefix of a font, and every byte of it in turn set to three + // different values: the reader must come back either way, and any font it + // does return must survive having every glyph walked. + walk :: proc(fx: ^Cf_Fix, data: []byte) { + f, err := cff_parse(data, fx.alloc) + if err != .None || f == nil { return } + for gid in 0 ..< min(cff_glyph_count(f), 8) { + p := path_make(fx.alloc) + _ = cff_glyph_path(f, u16(gid), &p) + _ = cff_glyph_advance(f, u16(gid)) + path_destroy(&p) + } + _, _ = cff_gid_for_name(f, "A") + _, _ = cff_gid_for_cid(f, 1) + _ = cff_font_matrix(f) + } + + for cut in 0 ..= len(CFF_MINIMAL) { + walk(&fx, CFF_MINIMAL[:cut]) + } + + scratch := make([]byte, len(CFF_MINIMAL), fx.alloc) + values := []u8{0x00, 0x80, 0xFF} + for i in 0 ..< len(CFF_MINIMAL) { + for v in values { + copy(scratch, CFF_MINIMAL[:]) + scratch[i] = v + walk(&fx, scratch) + } + } + + // The same over a font with subroutines, a custom encoding and a CIDFont + // layout, which reach the parts of the reader the minimal font does not. + fd0 := make([][]byte, 1, fx.alloc) + fd0[0] = {cs_n(1), cs_n(2), 21, 11} + rich := cb_build(Cff_Spec{ + ros = true, + charstrings = {{14}, {cs_n(-107), 10, 14}, {14}}, + charset = {2, 0, 100, 0, 1}, + fdselect = {3, 0, 1, 0, 0, 0, 0, 3}, + fd_extra = {cf_widths(&fx, 111, 5)}, + fd_subrs = {fd0}, + }, fx.alloc) + for cut in 0 ..= len(rich) { + walk(&fx, rich[:cut]) + } + scratch2 := make([]byte, len(rich), fx.alloc) + for i in 0 ..< len(rich) { + for v in values { + copy(scratch2, rich) + scratch2[i] = v + walk(&fx, scratch2) + } + } +} diff --git a/font_standard.odin b/font_standard.odin new file mode 100644 index 0000000..bf8534a --- /dev/null +++ b/font_standard.odin @@ -0,0 +1,1991 @@ +package pdf + +// The standard 14 fonts' metrics, the built-in encodings, and the Adobe Glyph +// List (ISO 32000-2, 9.6.2.2 and Annex D). +// +// A font dictionary is allowed to name Helvetica, Times, Courier, Symbol or +// ZapfDingbats and then supply neither a font program nor a /Widths array: the +// reader is expected to know the metrics already. Getting a width wrong here is +// not a cosmetic problem -- every glyph after it on the line is displaced -- so +// none of the numbers below were typed by hand. +// +// WHERE THE NUMBERS CAME FROM +// +// Widths the Adobe Core 14 AFM files (Adobe Systems, 1997, AFM 4.1), +// taken verbatim from their redistribution in Apache PDFBox. +// Cross-checked glyph by glyph against two independent sources: +// reportlab's built-in width tables (0 differences over the 229 +// glyphs it carries) and the URW++ metric-compatible AFMs shipped +// with Ghostscript (0 differences for Courier, Symbol and +// ZapfDingbats; the handful for Helvetica and Times -- fraction, +// summation, Delta, lessequal, greaterequal, lozenge, dcaron, +// lcaron -- are URW drawing those glyphs differently, and Adobe's +// value is the one PDF means). +// Encodings ISO 32000-2 Annex D, via reportlab's tables, checked against +// pdf.js's encodings.js and, for StandardEncoding and the two +// built-in symbol encodings, against the C column of the AFMs +// themselves. +// Glyph names Adobe Glyph List 2.0 (glyphlist.txt), Adobe Glyph List For New +// Fonts 1.7 (aglfn.txt, used for the Unicode -> name direction) +// and Adobe's zapfdingbats.txt. +// +// All fourteen are 1000 units per em, so a width here is already in the glyph +// space the rest of the font code expects. + +// --------------------------------------------------------------------------- +// The fourteen fonts +// --------------------------------------------------------------------------- + +// Indices into the metric tables, and what std_font_lookup returns. The order +// within a family is regular, bold, italic, bold-italic, so a style can be +// added to a family's base index. +@(private) +STD_COURIER :: 0 +@(private) +STD_COURIER_BOLD :: 1 +@(private) +STD_COURIER_OBLIQUE :: 2 +@(private) +STD_COURIER_BOLD_OBLIQUE :: 3 +@(private) +STD_HELVETICA :: 4 +@(private) +STD_HELVETICA_BOLD :: 5 +@(private) +STD_HELVETICA_OBLIQUE :: 6 +@(private) +STD_HELVETICA_BOLD_OBLIQUE :: 7 +@(private) +STD_TIMES_ROMAN :: 8 +@(private) +STD_TIMES_BOLD :: 9 +@(private) +STD_TIMES_ITALIC :: 10 +@(private) +STD_TIMES_BOLD_ITALIC :: 11 +@(private) +STD_SYMBOL :: 12 +@(private) +STD_ZAPF_DINGBATS :: 13 +@(private) +STD_FONT_COUNT :: 14 + +// The canonical PostScript name of each, in index order. +@(private, rodata) +STD_FONT_NAMES := [STD_FONT_COUNT]string{ + "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", + "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique", + "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic", + "Symbol", "ZapfDingbats", +} + +// --------------------------------------------------------------------------- +// Tables, generated mechanically from the sources named at the top of this file +// -- the AFMs, glyphlist.txt, aglfn.txt and zapfdingbats.txt -- so that no +// number below was typed by a person. Do not hand-edit one: correct the source +// and regenerate, or the next regeneration silently undoes the fix. +// --------------------------------------------------------------------------- + +// The 315 glyph names Adobe's AFMs give for every one of the twelve text +// fonts. All twelve carry exactly this set, so one sorted name table serves +// them all and a width lookup is a binary search over it. +@(private, rodata) +STD_TEXT_GLYPHS := [315]string{ + "A", "AE", "Aacute", "Abreve", "Acircumflex", "Adieresis", + "Agrave", "Amacron", "Aogonek", "Aring", "Atilde", "B", + "C", "Cacute", "Ccaron", "Ccedilla", "D", "Dcaron", + "Dcroat", "Delta", "E", "Eacute", "Ecaron", "Ecircumflex", + "Edieresis", "Edotaccent", "Egrave", "Emacron", "Eogonek", "Eth", + "Euro", "F", "G", "Gbreve", "Gcommaaccent", "H", + "I", "Iacute", "Icircumflex", "Idieresis", "Idotaccent", "Igrave", + "Imacron", "Iogonek", "J", "K", "Kcommaaccent", "L", + "Lacute", "Lcaron", "Lcommaaccent", "Lslash", "M", "N", + "Nacute", "Ncaron", "Ncommaaccent", "Ntilde", "O", "OE", + "Oacute", "Ocircumflex", "Odieresis", "Ograve", "Ohungarumlaut", "Omacron", + "Oslash", "Otilde", "P", "Q", "R", "Racute", + "Rcaron", "Rcommaaccent", "S", "Sacute", "Scaron", "Scedilla", + "Scommaaccent", "T", "Tcaron", "Tcommaaccent", "Thorn", "U", + "Uacute", "Ucircumflex", "Udieresis", "Ugrave", "Uhungarumlaut", "Umacron", + "Uogonek", "Uring", "V", "W", "X", "Y", + "Yacute", "Ydieresis", "Z", "Zacute", "Zcaron", "Zdotaccent", + "a", "aacute", "abreve", "acircumflex", "acute", "adieresis", + "ae", "agrave", "amacron", "ampersand", "aogonek", "aring", + "asciicircum", "asciitilde", "asterisk", "at", "atilde", "b", + "backslash", "bar", "braceleft", "braceright", "bracketleft", "bracketright", + "breve", "brokenbar", "bullet", "c", "cacute", "caron", + "ccaron", "ccedilla", "cedilla", "cent", "circumflex", "colon", + "comma", "commaaccent", "copyright", "currency", "d", "dagger", + "daggerdbl", "dcaron", "dcroat", "degree", "dieresis", "divide", + "dollar", "dotaccent", "dotlessi", "e", "eacute", "ecaron", + "ecircumflex", "edieresis", "edotaccent", "egrave", "eight", "ellipsis", + "emacron", "emdash", "endash", "eogonek", "equal", "eth", + "exclam", "exclamdown", "f", "fi", "five", "fl", + "florin", "four", "fraction", "g", "gbreve", "gcommaaccent", + "germandbls", "grave", "greater", "greaterequal", "guillemotleft", "guillemotright", + "guilsinglleft", "guilsinglright", "h", "hungarumlaut", "hyphen", "i", + "iacute", "icircumflex", "idieresis", "igrave", "imacron", "iogonek", + "j", "k", "kcommaaccent", "l", "lacute", "lcaron", + "lcommaaccent", "less", "lessequal", "logicalnot", "lozenge", "lslash", + "m", "macron", "minus", "mu", "multiply", "n", + "nacute", "ncaron", "ncommaaccent", "nine", "notequal", "ntilde", + "numbersign", "o", "oacute", "ocircumflex", "odieresis", "oe", + "ogonek", "ograve", "ohungarumlaut", "omacron", "one", "onehalf", + "onequarter", "onesuperior", "ordfeminine", "ordmasculine", "oslash", "otilde", + "p", "paragraph", "parenleft", "parenright", "partialdiff", "percent", + "period", "periodcentered", "perthousand", "plus", "plusminus", "q", + "question", "questiondown", "quotedbl", "quotedblbase", "quotedblleft", "quotedblright", + "quoteleft", "quoteright", "quotesinglbase", "quotesingle", "r", "racute", + "radical", "rcaron", "rcommaaccent", "registered", "ring", "s", + "sacute", "scaron", "scedilla", "scommaaccent", "section", "semicolon", + "seven", "six", "slash", "space", "sterling", "summation", + "t", "tcaron", "tcommaaccent", "thorn", "three", "threequarters", + "threesuperior", "tilde", "trademark", "two", "twosuperior", "u", + "uacute", "ucircumflex", "udieresis", "ugrave", "uhungarumlaut", "umacron", + "underscore", "uogonek", "uring", "v", "w", "x", + "y", "yacute", "ydieresis", "yen", "z", "zacute", + "zcaron", "zdotaccent", "zero", +} + +// Widths in glyph space (1/1000 em) for the eight proportional fonts, +// parallel to STD_TEXT_GLYPHS. Courier is not here: all four of its faces +// are 600 for every glyph, which is what std_font_width returns directly. +@(private, rodata) +STD_TEXT_WIDTHS := [8][315]u16{ + { // Helvetica + 667, 1000, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 722, 722, 722, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 556, 611, + 778, 778, 778, 722, 278, 278, 278, 278, 278, 278, 278, 278, 500, 667, 667, 556, + 556, 556, 556, 556, 833, 722, 722, 722, 722, 722, 778, 1000, 778, 778, 778, 778, + 778, 778, 778, 778, 667, 778, 722, 722, 722, 722, 667, 667, 667, 667, 667, 611, + 611, 611, 667, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 944, 667, 667, + 667, 667, 611, 611, 611, 611, 556, 556, 556, 556, 333, 556, 889, 556, 556, 667, + 556, 556, 469, 584, 389, 1015, 556, 556, 278, 260, 334, 334, 278, 278, 333, 260, + 350, 500, 500, 333, 500, 500, 333, 556, 333, 278, 278, 250, 737, 556, 556, 556, + 556, 643, 556, 400, 333, 584, 556, 333, 278, 556, 556, 556, 556, 556, 556, 556, + 556, 1000, 556, 1000, 556, 556, 584, 556, 278, 333, 278, 500, 556, 500, 556, 556, + 167, 556, 556, 556, 611, 333, 584, 549, 556, 556, 333, 333, 556, 333, 333, 222, + 278, 278, 278, 278, 278, 222, 222, 500, 500, 222, 222, 299, 222, 584, 549, 584, + 471, 222, 833, 333, 584, 556, 584, 556, 556, 556, 556, 556, 549, 556, 556, 556, + 556, 556, 556, 944, 333, 556, 556, 556, 556, 834, 834, 333, 370, 365, 611, 556, + 556, 537, 333, 333, 476, 889, 278, 278, 1000, 584, 584, 556, 556, 611, 355, 333, + 333, 333, 222, 222, 222, 191, 333, 333, 453, 333, 333, 737, 333, 500, 500, 500, + 500, 500, 556, 278, 556, 556, 278, 278, 556, 600, 278, 317, 278, 556, 556, 834, + 333, 333, 1000, 556, 333, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 500, + 722, 500, 500, 500, 500, 556, 500, 500, 500, 500, 556, + }, + { // Helvetica-Bold + 722, 1000, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 556, 611, + 778, 778, 778, 722, 278, 278, 278, 278, 278, 278, 278, 278, 556, 722, 722, 611, + 611, 611, 611, 611, 833, 722, 722, 722, 722, 722, 778, 1000, 778, 778, 778, 778, + 778, 778, 778, 778, 667, 778, 722, 722, 722, 722, 667, 667, 667, 667, 667, 611, + 611, 611, 667, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 944, 667, 667, + 667, 667, 611, 611, 611, 611, 556, 556, 556, 556, 333, 556, 889, 556, 556, 722, + 556, 556, 584, 584, 389, 975, 556, 611, 278, 280, 389, 389, 333, 333, 333, 280, + 350, 556, 556, 333, 556, 556, 333, 556, 333, 333, 278, 250, 737, 556, 611, 556, + 556, 743, 611, 400, 333, 584, 556, 333, 278, 556, 556, 556, 556, 556, 556, 556, + 556, 1000, 556, 1000, 556, 556, 584, 611, 333, 333, 333, 611, 556, 611, 556, 556, + 167, 611, 611, 611, 611, 333, 584, 549, 556, 556, 333, 333, 611, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 278, 556, 556, 278, 278, 400, 278, 584, 549, 584, + 494, 278, 889, 333, 584, 611, 584, 611, 611, 611, 611, 556, 549, 611, 556, 611, + 611, 611, 611, 944, 333, 611, 611, 611, 556, 834, 834, 333, 370, 365, 611, 611, + 611, 556, 333, 333, 494, 889, 278, 278, 1000, 584, 584, 611, 611, 611, 474, 500, + 500, 500, 278, 278, 278, 238, 389, 389, 549, 389, 389, 737, 333, 556, 556, 556, + 556, 556, 556, 333, 556, 556, 278, 278, 556, 600, 333, 389, 333, 611, 556, 834, + 333, 333, 1000, 556, 333, 611, 611, 611, 611, 611, 611, 611, 556, 611, 611, 556, + 778, 556, 556, 556, 556, 556, 500, 500, 500, 500, 556, + }, + { // Helvetica-Oblique + 667, 1000, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 722, 722, 722, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 556, 611, + 778, 778, 778, 722, 278, 278, 278, 278, 278, 278, 278, 278, 500, 667, 667, 556, + 556, 556, 556, 556, 833, 722, 722, 722, 722, 722, 778, 1000, 778, 778, 778, 778, + 778, 778, 778, 778, 667, 778, 722, 722, 722, 722, 667, 667, 667, 667, 667, 611, + 611, 611, 667, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 944, 667, 667, + 667, 667, 611, 611, 611, 611, 556, 556, 556, 556, 333, 556, 889, 556, 556, 667, + 556, 556, 469, 584, 389, 1015, 556, 556, 278, 260, 334, 334, 278, 278, 333, 260, + 350, 500, 500, 333, 500, 500, 333, 556, 333, 278, 278, 250, 737, 556, 556, 556, + 556, 643, 556, 400, 333, 584, 556, 333, 278, 556, 556, 556, 556, 556, 556, 556, + 556, 1000, 556, 1000, 556, 556, 584, 556, 278, 333, 278, 500, 556, 500, 556, 556, + 167, 556, 556, 556, 611, 333, 584, 549, 556, 556, 333, 333, 556, 333, 333, 222, + 278, 278, 278, 278, 278, 222, 222, 500, 500, 222, 222, 299, 222, 584, 549, 584, + 471, 222, 833, 333, 584, 556, 584, 556, 556, 556, 556, 556, 549, 556, 556, 556, + 556, 556, 556, 944, 333, 556, 556, 556, 556, 834, 834, 333, 370, 365, 611, 556, + 556, 537, 333, 333, 476, 889, 278, 278, 1000, 584, 584, 556, 556, 611, 355, 333, + 333, 333, 222, 222, 222, 191, 333, 333, 453, 333, 333, 737, 333, 500, 500, 500, + 500, 500, 556, 278, 556, 556, 278, 278, 556, 600, 278, 317, 278, 556, 556, 834, + 333, 333, 1000, 556, 333, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 500, + 722, 500, 500, 500, 500, 556, 500, 500, 500, 500, 556, + }, + { // Helvetica-BoldOblique + 722, 1000, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 556, 611, + 778, 778, 778, 722, 278, 278, 278, 278, 278, 278, 278, 278, 556, 722, 722, 611, + 611, 611, 611, 611, 833, 722, 722, 722, 722, 722, 778, 1000, 778, 778, 778, 778, + 778, 778, 778, 778, 667, 778, 722, 722, 722, 722, 667, 667, 667, 667, 667, 611, + 611, 611, 667, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 944, 667, 667, + 667, 667, 611, 611, 611, 611, 556, 556, 556, 556, 333, 556, 889, 556, 556, 722, + 556, 556, 584, 584, 389, 975, 556, 611, 278, 280, 389, 389, 333, 333, 333, 280, + 350, 556, 556, 333, 556, 556, 333, 556, 333, 333, 278, 250, 737, 556, 611, 556, + 556, 743, 611, 400, 333, 584, 556, 333, 278, 556, 556, 556, 556, 556, 556, 556, + 556, 1000, 556, 1000, 556, 556, 584, 611, 333, 333, 333, 611, 556, 611, 556, 556, + 167, 611, 611, 611, 611, 333, 584, 549, 556, 556, 333, 333, 611, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 278, 556, 556, 278, 278, 400, 278, 584, 549, 584, + 494, 278, 889, 333, 584, 611, 584, 611, 611, 611, 611, 556, 549, 611, 556, 611, + 611, 611, 611, 944, 333, 611, 611, 611, 556, 834, 834, 333, 370, 365, 611, 611, + 611, 556, 333, 333, 494, 889, 278, 278, 1000, 584, 584, 611, 611, 611, 474, 500, + 500, 500, 278, 278, 278, 238, 389, 389, 549, 389, 389, 737, 333, 556, 556, 556, + 556, 556, 556, 333, 556, 556, 278, 278, 556, 600, 333, 389, 333, 611, 556, 834, + 333, 333, 1000, 556, 333, 611, 611, 611, 611, 611, 611, 611, 556, 611, 611, 556, + 778, 556, 556, 556, 556, 556, 500, 500, 500, 500, 556, + }, + { // Times-Roman + 722, 889, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 667, 667, 667, 667, + 722, 722, 722, 612, 611, 611, 611, 611, 611, 611, 611, 611, 611, 722, 500, 556, + 722, 722, 722, 722, 333, 333, 333, 333, 333, 333, 333, 333, 389, 722, 722, 611, + 611, 611, 611, 611, 889, 722, 722, 722, 722, 722, 722, 889, 722, 722, 722, 722, + 722, 722, 722, 722, 556, 722, 667, 667, 667, 667, 556, 556, 556, 556, 556, 611, + 611, 611, 556, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 944, 722, 722, + 722, 722, 611, 611, 611, 611, 444, 444, 444, 444, 333, 444, 667, 444, 444, 778, + 444, 444, 469, 541, 500, 921, 444, 500, 278, 200, 480, 480, 333, 333, 333, 200, + 350, 444, 444, 333, 444, 444, 333, 500, 333, 278, 250, 250, 760, 500, 500, 500, + 500, 588, 500, 400, 333, 564, 500, 333, 278, 444, 444, 444, 444, 444, 444, 444, + 500, 1000, 444, 1000, 500, 444, 564, 500, 333, 333, 333, 556, 500, 556, 500, 500, + 167, 500, 500, 500, 500, 333, 564, 549, 500, 500, 333, 333, 500, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 278, 500, 500, 278, 278, 344, 278, 564, 549, 564, + 471, 278, 778, 333, 564, 500, 564, 500, 500, 500, 500, 500, 549, 500, 500, 500, + 500, 500, 500, 722, 333, 500, 500, 500, 500, 750, 750, 300, 276, 310, 500, 500, + 500, 453, 333, 333, 476, 833, 250, 250, 1000, 564, 564, 500, 444, 444, 408, 444, + 444, 444, 333, 333, 333, 180, 333, 333, 453, 333, 333, 760, 333, 389, 389, 389, + 389, 389, 500, 278, 500, 500, 278, 250, 500, 600, 278, 326, 278, 500, 500, 750, + 300, 333, 980, 500, 300, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, + 722, 500, 500, 500, 500, 500, 444, 444, 444, 444, 500, + }, + { // Times-Bold + 722, 1000, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 722, 722, 722, 722, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 500, 611, + 778, 778, 778, 778, 389, 389, 389, 389, 389, 389, 389, 389, 500, 778, 778, 667, + 667, 667, 667, 667, 944, 722, 722, 722, 722, 722, 778, 1000, 778, 778, 778, 778, + 778, 778, 778, 778, 611, 778, 722, 722, 722, 722, 556, 556, 556, 556, 556, 667, + 667, 667, 611, 722, 722, 722, 722, 722, 722, 722, 722, 722, 722, 1000, 722, 722, + 722, 722, 667, 667, 667, 667, 500, 500, 500, 500, 333, 500, 722, 500, 500, 833, + 500, 500, 581, 520, 500, 930, 500, 556, 278, 220, 394, 394, 333, 333, 333, 220, + 350, 444, 444, 333, 444, 444, 333, 500, 333, 333, 250, 250, 747, 500, 556, 500, + 500, 672, 556, 400, 333, 570, 500, 333, 278, 444, 444, 444, 444, 444, 444, 444, + 500, 1000, 444, 1000, 500, 444, 570, 500, 333, 333, 333, 556, 500, 556, 500, 500, + 167, 500, 500, 500, 556, 333, 570, 549, 500, 500, 333, 333, 556, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 333, 556, 556, 278, 278, 394, 278, 570, 549, 570, + 494, 278, 833, 333, 570, 556, 570, 556, 556, 556, 556, 500, 549, 556, 500, 500, + 500, 500, 500, 722, 333, 500, 500, 500, 500, 750, 750, 300, 300, 330, 500, 500, + 556, 540, 333, 333, 494, 1000, 250, 250, 1000, 570, 570, 556, 500, 500, 555, 500, + 500, 500, 333, 333, 333, 278, 444, 444, 549, 444, 444, 747, 333, 389, 389, 389, + 389, 389, 500, 333, 500, 500, 278, 250, 500, 600, 333, 416, 333, 556, 500, 750, + 300, 333, 1000, 500, 300, 556, 556, 556, 556, 556, 556, 556, 500, 556, 556, 500, + 722, 500, 500, 500, 500, 500, 444, 444, 444, 444, 500, + }, + { // Times-Italic + 611, 889, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 667, 667, 667, 667, + 722, 722, 722, 612, 611, 611, 611, 611, 611, 611, 611, 611, 611, 722, 500, 611, + 722, 722, 722, 722, 333, 333, 333, 333, 333, 333, 333, 333, 444, 667, 667, 556, + 556, 611, 556, 556, 833, 667, 667, 667, 667, 667, 722, 944, 722, 722, 722, 722, + 722, 722, 722, 722, 611, 722, 611, 611, 611, 611, 500, 500, 500, 500, 500, 556, + 556, 556, 611, 722, 722, 722, 722, 722, 722, 722, 722, 722, 611, 833, 611, 556, + 556, 556, 556, 556, 556, 556, 500, 500, 500, 500, 333, 500, 667, 500, 500, 778, + 500, 500, 422, 541, 500, 920, 500, 500, 278, 275, 400, 400, 389, 389, 333, 275, + 350, 444, 444, 333, 444, 444, 333, 500, 333, 333, 250, 250, 760, 500, 500, 500, + 500, 544, 500, 400, 333, 675, 500, 333, 278, 444, 444, 444, 444, 444, 444, 444, + 500, 889, 444, 889, 500, 444, 675, 500, 333, 389, 278, 500, 500, 500, 500, 500, + 167, 500, 500, 500, 500, 333, 675, 549, 500, 500, 333, 333, 500, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 278, 444, 444, 278, 278, 300, 278, 675, 549, 675, + 471, 278, 722, 333, 675, 500, 675, 500, 500, 500, 500, 500, 549, 500, 500, 500, + 500, 500, 500, 667, 333, 500, 500, 500, 500, 750, 750, 300, 276, 310, 500, 500, + 500, 523, 333, 333, 476, 833, 250, 250, 1000, 675, 675, 500, 500, 500, 420, 556, + 556, 556, 333, 333, 333, 214, 389, 389, 453, 389, 389, 760, 333, 389, 389, 389, + 389, 389, 500, 333, 500, 500, 278, 250, 500, 600, 278, 300, 278, 500, 500, 750, + 300, 333, 980, 500, 300, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 444, + 667, 444, 444, 444, 444, 500, 389, 389, 389, 389, 500, + }, + { // Times-BoldItalic + 667, 944, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, 667, + 722, 722, 722, 612, 667, 667, 667, 667, 667, 667, 667, 667, 667, 722, 500, 667, + 722, 722, 722, 778, 389, 389, 389, 389, 389, 389, 389, 389, 500, 667, 667, 611, + 611, 611, 611, 611, 889, 722, 722, 722, 722, 722, 722, 944, 722, 722, 722, 722, + 722, 722, 722, 722, 611, 722, 667, 667, 667, 667, 556, 556, 556, 556, 556, 611, + 611, 611, 611, 722, 722, 722, 722, 722, 722, 722, 722, 722, 667, 889, 667, 611, + 611, 611, 611, 611, 611, 611, 500, 500, 500, 500, 333, 500, 722, 500, 500, 778, + 500, 500, 570, 570, 500, 832, 500, 500, 278, 220, 348, 348, 333, 333, 333, 220, + 350, 444, 444, 333, 444, 444, 333, 500, 333, 333, 250, 250, 747, 500, 500, 500, + 500, 608, 500, 400, 333, 570, 500, 333, 278, 444, 444, 444, 444, 444, 444, 444, + 500, 1000, 444, 1000, 500, 444, 570, 500, 389, 389, 333, 556, 500, 556, 500, 500, + 167, 500, 500, 500, 500, 333, 570, 549, 500, 500, 333, 333, 556, 333, 333, 278, + 278, 278, 278, 278, 278, 278, 278, 500, 500, 278, 278, 382, 278, 570, 549, 606, + 494, 278, 778, 333, 606, 576, 570, 556, 556, 556, 556, 500, 549, 556, 500, 500, + 500, 500, 500, 722, 333, 500, 500, 500, 500, 750, 750, 300, 266, 300, 500, 500, + 500, 500, 333, 333, 494, 833, 250, 250, 1000, 570, 570, 500, 500, 500, 555, 500, + 500, 500, 333, 333, 333, 278, 389, 389, 549, 389, 389, 747, 333, 389, 389, 389, + 389, 389, 500, 333, 500, 500, 278, 250, 500, 600, 278, 366, 278, 500, 500, 750, + 300, 333, 1000, 500, 300, 556, 556, 556, 556, 556, 556, 556, 500, 556, 556, 444, + 667, 500, 444, 444, 444, 500, 389, 389, 389, 389, 500, + }, +} + +// Symbol.afm, sorted by glyph name. +@(private, rodata) +STD_SYMBOL_GLYPHS := [190]string{ + "Alpha", "Beta", "Chi", "Delta", "Epsilon", "Eta", + "Euro", "Gamma", "Ifraktur", "Iota", "Kappa", "Lambda", + "Mu", "Nu", "Omega", "Omicron", "Phi", "Pi", + "Psi", "Rfraktur", "Rho", "Sigma", "Tau", "Theta", + "Upsilon", "Upsilon1", "Xi", "Zeta", "aleph", "alpha", + "ampersand", "angle", "angleleft", "angleright", "apple", "approxequal", + "arrowboth", "arrowdblboth", "arrowdbldown", "arrowdblleft", "arrowdblright", "arrowdblup", + "arrowdown", "arrowhorizex", "arrowleft", "arrowright", "arrowup", "arrowvertex", + "asteriskmath", "bar", "beta", "braceex", "braceleft", "braceleftbt", + "braceleftmid", "bracelefttp", "braceright", "bracerightbt", "bracerightmid", "bracerighttp", + "bracketleft", "bracketleftbt", "bracketleftex", "bracketlefttp", "bracketright", "bracketrightbt", + "bracketrightex", "bracketrighttp", "bullet", "carriagereturn", "chi", "circlemultiply", + "circleplus", "club", "colon", "comma", "congruent", "copyrightsans", + "copyrightserif", "degree", "delta", "diamond", "divide", "dotmath", + "eight", "element", "ellipsis", "emptyset", "epsilon", "equal", + "equivalence", "eta", "exclam", "existential", "five", "florin", + "four", "fraction", "gamma", "gradient", "greater", "greaterequal", + "heart", "infinity", "integral", "integralbt", "integralex", "integraltp", + "intersection", "iota", "kappa", "lambda", "less", "lessequal", + "logicaland", "logicalnot", "logicalor", "lozenge", "minus", "minute", + "mu", "multiply", "nine", "notelement", "notequal", "notsubset", + "nu", "numbersign", "omega", "omega1", "omicron", "one", + "parenleft", "parenleftbt", "parenleftex", "parenlefttp", "parenright", "parenrightbt", + "parenrightex", "parenrighttp", "partialdiff", "percent", "period", "perpendicular", + "phi", "phi1", "pi", "plus", "plusminus", "product", + "propersubset", "propersuperset", "proportional", "psi", "question", "radical", + "radicalex", "reflexsubset", "reflexsuperset", "registersans", "registerserif", "rho", + "second", "semicolon", "seven", "sigma", "sigma1", "similar", + "six", "slash", "space", "spade", "suchthat", "summation", + "tau", "therefore", "theta", "theta1", "three", "trademarksans", + "trademarkserif", "two", "underscore", "union", "universal", "upsilon", + "weierstrass", "xi", "zero", "zeta", +} + +@(private, rodata) +STD_SYMBOL_WIDTHS := [190]u16{ + 722, 667, 722, 612, 611, 722, 750, 603, 686, 333, 722, 686, 889, 722, 768, 722, + 763, 768, 795, 795, 556, 592, 611, 741, 690, 620, 645, 611, 823, 631, 778, 768, + 329, 329, 790, 549, 1042, 1042, 603, 987, 987, 603, 603, 1000, 987, 987, 603, 603, + 500, 200, 549, 494, 480, 494, 494, 494, 480, 494, 494, 494, 333, 384, 384, 384, + 333, 384, 384, 384, 460, 658, 549, 768, 768, 753, 278, 250, 549, 790, 790, 400, + 494, 753, 549, 250, 500, 713, 1000, 823, 439, 549, 549, 603, 333, 549, 500, 500, + 500, 167, 411, 713, 549, 549, 753, 713, 274, 686, 686, 686, 768, 329, 549, 549, + 549, 549, 603, 713, 603, 494, 549, 247, 576, 549, 500, 713, 549, 713, 521, 500, + 686, 713, 549, 500, 333, 384, 384, 384, 333, 384, 384, 384, 494, 833, 250, 658, + 521, 603, 549, 549, 549, 823, 713, 713, 713, 686, 444, 549, 500, 713, 713, 790, + 790, 549, 411, 278, 500, 603, 439, 549, 500, 278, 250, 753, 439, 713, 439, 863, + 521, 631, 500, 786, 890, 500, 500, 768, 713, 576, 987, 493, 500, 494, +} + +// ZapfDingbats.afm, sorted by glyph name. +@(private, rodata) +STD_ZAPF_GLYPHS := [202]string{ + "a1", "a10", "a100", "a101", "a102", "a103", + "a104", "a105", "a106", "a107", "a108", "a109", + "a11", "a110", "a111", "a112", "a117", "a118", + "a119", "a12", "a120", "a121", "a122", "a123", + "a124", "a125", "a126", "a127", "a128", "a129", + "a13", "a130", "a131", "a132", "a133", "a134", + "a135", "a136", "a137", "a138", "a139", "a14", + "a140", "a141", "a142", "a143", "a144", "a145", + "a146", "a147", "a148", "a149", "a15", "a150", + "a151", "a152", "a153", "a154", "a155", "a156", + "a157", "a158", "a159", "a16", "a160", "a161", + "a162", "a163", "a164", "a165", "a166", "a167", + "a168", "a169", "a17", "a170", "a171", "a172", + "a173", "a174", "a175", "a176", "a177", "a178", + "a179", "a18", "a180", "a181", "a182", "a183", + "a184", "a185", "a186", "a187", "a188", "a189", + "a19", "a190", "a191", "a192", "a193", "a194", + "a195", "a196", "a197", "a198", "a199", "a2", + "a20", "a200", "a201", "a202", "a203", "a204", + "a205", "a206", "a21", "a22", "a23", "a24", + "a25", "a26", "a27", "a28", "a29", "a3", + "a30", "a31", "a32", "a33", "a34", "a35", + "a36", "a37", "a38", "a39", "a4", "a40", + "a41", "a42", "a43", "a44", "a45", "a46", + "a47", "a48", "a49", "a5", "a50", "a51", + "a52", "a53", "a54", "a55", "a56", "a57", + "a58", "a59", "a6", "a60", "a61", "a62", + "a63", "a64", "a65", "a66", "a67", "a68", + "a69", "a7", "a70", "a71", "a72", "a73", + "a74", "a75", "a76", "a77", "a78", "a79", + "a8", "a81", "a82", "a83", "a84", "a85", + "a86", "a87", "a88", "a89", "a9", "a90", + "a91", "a92", "a93", "a94", "a95", "a96", + "a97", "a98", "a99", "space", +} + +@(private, rodata) +STD_ZAPF_WIDTHS := [202]u16{ + 974, 692, 668, 732, 544, 544, 910, 911, 667, 760, 760, 626, 960, 694, 595, 776, + 690, 791, 790, 939, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 549, 788, + 788, 788, 788, 788, 788, 788, 788, 788, 788, 855, 788, 788, 788, 788, 788, 788, + 788, 788, 788, 788, 911, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 933, + 894, 838, 924, 1016, 458, 924, 918, 927, 928, 928, 945, 834, 873, 828, 924, 917, + 930, 931, 463, 883, 836, 974, 867, 696, 874, 760, 946, 865, 967, 831, 873, 927, + 755, 970, 918, 748, 836, 771, 888, 748, 771, 888, 867, 961, 846, 696, 874, 974, + 762, 759, 509, 410, 762, 761, 571, 677, 763, 760, 759, 754, 786, 980, 788, 788, + 790, 793, 794, 816, 823, 789, 841, 823, 719, 833, 816, 831, 923, 744, 723, 749, + 790, 792, 695, 789, 776, 768, 792, 759, 707, 708, 682, 701, 826, 815, 494, 789, + 789, 707, 687, 696, 689, 786, 787, 713, 791, 552, 785, 791, 873, 761, 762, 759, + 892, 892, 788, 784, 537, 438, 138, 277, 415, 509, 410, 234, 234, 390, 577, 390, + 276, 276, 317, 317, 334, 334, 392, 392, 668, 278, +} + +// StandardEncoding (ISO 32000-2, Annex D.2). Note quoteright at 0x27 and +// quotesingle at 0xA9 -- the reverse of WinAnsi, and the reason a document +// that gets its /Encoding wrong shows curly apostrophes as straight ones. +@(private, rodata) +ENC_STANDARD := [256]string{ + 32 = "space", 33 = "exclam", 34 = "quotedbl", 35 = "numbersign", + 36 = "dollar", 37 = "percent", 38 = "ampersand", 39 = "quoteright", + 40 = "parenleft", 41 = "parenright", 42 = "asterisk", 43 = "plus", + 44 = "comma", 45 = "hyphen", 46 = "period", 47 = "slash", + 48 = "zero", 49 = "one", 50 = "two", 51 = "three", + 52 = "four", 53 = "five", 54 = "six", 55 = "seven", + 56 = "eight", 57 = "nine", 58 = "colon", 59 = "semicolon", + 60 = "less", 61 = "equal", 62 = "greater", 63 = "question", + 64 = "at", 65 = "A", 66 = "B", 67 = "C", + 68 = "D", 69 = "E", 70 = "F", 71 = "G", + 72 = "H", 73 = "I", 74 = "J", 75 = "K", + 76 = "L", 77 = "M", 78 = "N", 79 = "O", + 80 = "P", 81 = "Q", 82 = "R", 83 = "S", + 84 = "T", 85 = "U", 86 = "V", 87 = "W", + 88 = "X", 89 = "Y", 90 = "Z", 91 = "bracketleft", + 92 = "backslash", 93 = "bracketright", 94 = "asciicircum", 95 = "underscore", + 96 = "quoteleft", 97 = "a", 98 = "b", 99 = "c", + 100 = "d", 101 = "e", 102 = "f", 103 = "g", + 104 = "h", 105 = "i", 106 = "j", 107 = "k", + 108 = "l", 109 = "m", 110 = "n", 111 = "o", + 112 = "p", 113 = "q", 114 = "r", 115 = "s", + 116 = "t", 117 = "u", 118 = "v", 119 = "w", + 120 = "x", 121 = "y", 122 = "z", 123 = "braceleft", + 124 = "bar", 125 = "braceright", 126 = "asciitilde", 161 = "exclamdown", + 162 = "cent", 163 = "sterling", 164 = "fraction", 165 = "yen", + 166 = "florin", 167 = "section", 168 = "currency", 169 = "quotesingle", + 170 = "quotedblleft", 171 = "guillemotleft", 172 = "guilsinglleft", 173 = "guilsinglright", + 174 = "fi", 175 = "fl", 177 = "endash", 178 = "dagger", + 179 = "daggerdbl", 180 = "periodcentered", 182 = "paragraph", 183 = "bullet", + 184 = "quotesinglbase", 185 = "quotedblbase", 186 = "quotedblright", 187 = "guillemotright", + 188 = "ellipsis", 189 = "perthousand", 191 = "questiondown", 193 = "grave", + 194 = "acute", 195 = "circumflex", 196 = "tilde", 197 = "macron", + 198 = "breve", 199 = "dotaccent", 200 = "dieresis", 202 = "ring", + 203 = "cedilla", 205 = "hungarumlaut", 206 = "ogonek", 207 = "caron", + 208 = "emdash", 225 = "AE", 227 = "ordfeminine", 232 = "Lslash", + 233 = "Oslash", 234 = "OE", 235 = "ordmasculine", 241 = "ae", + 245 = "dotlessi", 248 = "lslash", 249 = "oslash", 250 = "oe", + 251 = "germandbls", +} + +// WinAnsiEncoding (ISO 32000-2, Annex D.2). Latin-1 except for 0x80-0x9F, +// which hold the CP1252 typographic characters; every code above 0x20 that +// CP1252 leaves undefined maps to bullet, as the annex requires. 0xA0 is a +// no-break space and 0xAD a soft hyphen, both drawn as their plain forms. +@(private, rodata) +ENC_WINANSI := [256]string{ + 32 = "space", 33 = "exclam", 34 = "quotedbl", 35 = "numbersign", + 36 = "dollar", 37 = "percent", 38 = "ampersand", 39 = "quotesingle", + 40 = "parenleft", 41 = "parenright", 42 = "asterisk", 43 = "plus", + 44 = "comma", 45 = "hyphen", 46 = "period", 47 = "slash", + 48 = "zero", 49 = "one", 50 = "two", 51 = "three", + 52 = "four", 53 = "five", 54 = "six", 55 = "seven", + 56 = "eight", 57 = "nine", 58 = "colon", 59 = "semicolon", + 60 = "less", 61 = "equal", 62 = "greater", 63 = "question", + 64 = "at", 65 = "A", 66 = "B", 67 = "C", + 68 = "D", 69 = "E", 70 = "F", 71 = "G", + 72 = "H", 73 = "I", 74 = "J", 75 = "K", + 76 = "L", 77 = "M", 78 = "N", 79 = "O", + 80 = "P", 81 = "Q", 82 = "R", 83 = "S", + 84 = "T", 85 = "U", 86 = "V", 87 = "W", + 88 = "X", 89 = "Y", 90 = "Z", 91 = "bracketleft", + 92 = "backslash", 93 = "bracketright", 94 = "asciicircum", 95 = "underscore", + 96 = "grave", 97 = "a", 98 = "b", 99 = "c", + 100 = "d", 101 = "e", 102 = "f", 103 = "g", + 104 = "h", 105 = "i", 106 = "j", 107 = "k", + 108 = "l", 109 = "m", 110 = "n", 111 = "o", + 112 = "p", 113 = "q", 114 = "r", 115 = "s", + 116 = "t", 117 = "u", 118 = "v", 119 = "w", + 120 = "x", 121 = "y", 122 = "z", 123 = "braceleft", + 124 = "bar", 125 = "braceright", 126 = "asciitilde", 127 = "bullet", + 128 = "Euro", 129 = "bullet", 130 = "quotesinglbase", 131 = "florin", + 132 = "quotedblbase", 133 = "ellipsis", 134 = "dagger", 135 = "daggerdbl", + 136 = "circumflex", 137 = "perthousand", 138 = "Scaron", 139 = "guilsinglleft", + 140 = "OE", 141 = "bullet", 142 = "Zcaron", 143 = "bullet", + 144 = "bullet", 145 = "quoteleft", 146 = "quoteright", 147 = "quotedblleft", + 148 = "quotedblright", 149 = "bullet", 150 = "endash", 151 = "emdash", + 152 = "tilde", 153 = "trademark", 154 = "scaron", 155 = "guilsinglright", + 156 = "oe", 157 = "bullet", 158 = "zcaron", 159 = "Ydieresis", + 160 = "space", 161 = "exclamdown", 162 = "cent", 163 = "sterling", + 164 = "currency", 165 = "yen", 166 = "brokenbar", 167 = "section", + 168 = "dieresis", 169 = "copyright", 170 = "ordfeminine", 171 = "guillemotleft", + 172 = "logicalnot", 173 = "hyphen", 174 = "registered", 175 = "macron", + 176 = "degree", 177 = "plusminus", 178 = "twosuperior", 179 = "threesuperior", + 180 = "acute", 181 = "mu", 182 = "paragraph", 183 = "periodcentered", + 184 = "cedilla", 185 = "onesuperior", 186 = "ordmasculine", 187 = "guillemotright", + 188 = "onequarter", 189 = "onehalf", 190 = "threequarters", 191 = "questiondown", + 192 = "Agrave", 193 = "Aacute", 194 = "Acircumflex", 195 = "Atilde", + 196 = "Adieresis", 197 = "Aring", 198 = "AE", 199 = "Ccedilla", + 200 = "Egrave", 201 = "Eacute", 202 = "Ecircumflex", 203 = "Edieresis", + 204 = "Igrave", 205 = "Iacute", 206 = "Icircumflex", 207 = "Idieresis", + 208 = "Eth", 209 = "Ntilde", 210 = "Ograve", 211 = "Oacute", + 212 = "Ocircumflex", 213 = "Otilde", 214 = "Odieresis", 215 = "multiply", + 216 = "Oslash", 217 = "Ugrave", 218 = "Uacute", 219 = "Ucircumflex", + 220 = "Udieresis", 221 = "Yacute", 222 = "Thorn", 223 = "germandbls", + 224 = "agrave", 225 = "aacute", 226 = "acircumflex", 227 = "atilde", + 228 = "adieresis", 229 = "aring", 230 = "ae", 231 = "ccedilla", + 232 = "egrave", 233 = "eacute", 234 = "ecircumflex", 235 = "edieresis", + 236 = "igrave", 237 = "iacute", 238 = "icircumflex", 239 = "idieresis", + 240 = "eth", 241 = "ntilde", 242 = "ograve", 243 = "oacute", + 244 = "ocircumflex", 245 = "otilde", 246 = "odieresis", 247 = "divide", + 248 = "oslash", 249 = "ugrave", 250 = "uacute", 251 = "ucircumflex", + 252 = "udieresis", 253 = "yacute", 254 = "thorn", 255 = "ydieresis", +} + +// MacRomanEncoding (ISO 32000-2, Annex D.2), plus the fifteen positions the +// annex leaves blank but Mac OS Roman itself defines (0xAD notequal, 0xB0 +// infinity, ... 0xF0 apple). Producers on the Mac emitted those codes, and +// every other reader fills them in the same way; a blank there would drop a +// character that the file plainly meant. +@(private, rodata) +ENC_MACROMAN := [256]string{ + 32 = "space", 33 = "exclam", 34 = "quotedbl", 35 = "numbersign", + 36 = "dollar", 37 = "percent", 38 = "ampersand", 39 = "quotesingle", + 40 = "parenleft", 41 = "parenright", 42 = "asterisk", 43 = "plus", + 44 = "comma", 45 = "hyphen", 46 = "period", 47 = "slash", + 48 = "zero", 49 = "one", 50 = "two", 51 = "three", + 52 = "four", 53 = "five", 54 = "six", 55 = "seven", + 56 = "eight", 57 = "nine", 58 = "colon", 59 = "semicolon", + 60 = "less", 61 = "equal", 62 = "greater", 63 = "question", + 64 = "at", 65 = "A", 66 = "B", 67 = "C", + 68 = "D", 69 = "E", 70 = "F", 71 = "G", + 72 = "H", 73 = "I", 74 = "J", 75 = "K", + 76 = "L", 77 = "M", 78 = "N", 79 = "O", + 80 = "P", 81 = "Q", 82 = "R", 83 = "S", + 84 = "T", 85 = "U", 86 = "V", 87 = "W", + 88 = "X", 89 = "Y", 90 = "Z", 91 = "bracketleft", + 92 = "backslash", 93 = "bracketright", 94 = "asciicircum", 95 = "underscore", + 96 = "grave", 97 = "a", 98 = "b", 99 = "c", + 100 = "d", 101 = "e", 102 = "f", 103 = "g", + 104 = "h", 105 = "i", 106 = "j", 107 = "k", + 108 = "l", 109 = "m", 110 = "n", 111 = "o", + 112 = "p", 113 = "q", 114 = "r", 115 = "s", + 116 = "t", 117 = "u", 118 = "v", 119 = "w", + 120 = "x", 121 = "y", 122 = "z", 123 = "braceleft", + 124 = "bar", 125 = "braceright", 126 = "asciitilde", 128 = "Adieresis", + 129 = "Aring", 130 = "Ccedilla", 131 = "Eacute", 132 = "Ntilde", + 133 = "Odieresis", 134 = "Udieresis", 135 = "aacute", 136 = "agrave", + 137 = "acircumflex", 138 = "adieresis", 139 = "atilde", 140 = "aring", + 141 = "ccedilla", 142 = "eacute", 143 = "egrave", 144 = "ecircumflex", + 145 = "edieresis", 146 = "iacute", 147 = "igrave", 148 = "icircumflex", + 149 = "idieresis", 150 = "ntilde", 151 = "oacute", 152 = "ograve", + 153 = "ocircumflex", 154 = "odieresis", 155 = "otilde", 156 = "uacute", + 157 = "ugrave", 158 = "ucircumflex", 159 = "udieresis", 160 = "dagger", + 161 = "degree", 162 = "cent", 163 = "sterling", 164 = "section", + 165 = "bullet", 166 = "paragraph", 167 = "germandbls", 168 = "registered", + 169 = "copyright", 170 = "trademark", 171 = "acute", 172 = "dieresis", + 173 = "notequal", 174 = "AE", 175 = "Oslash", 176 = "infinity", + 177 = "plusminus", 178 = "lessequal", 179 = "greaterequal", 180 = "yen", + 181 = "mu", 182 = "partialdiff", 183 = "summation", 184 = "product", + 185 = "pi", 186 = "integral", 187 = "ordfeminine", 188 = "ordmasculine", + 189 = "Omega", 190 = "ae", 191 = "oslash", 192 = "questiondown", + 193 = "exclamdown", 194 = "logicalnot", 195 = "radical", 196 = "florin", + 197 = "approxequal", 198 = "Delta", 199 = "guillemotleft", 200 = "guillemotright", + 201 = "ellipsis", 202 = "space", 203 = "Agrave", 204 = "Atilde", + 205 = "Otilde", 206 = "OE", 207 = "oe", 208 = "endash", + 209 = "emdash", 210 = "quotedblleft", 211 = "quotedblright", 212 = "quoteleft", + 213 = "quoteright", 214 = "divide", 215 = "lozenge", 216 = "ydieresis", + 217 = "Ydieresis", 218 = "fraction", 219 = "currency", 220 = "guilsinglleft", + 221 = "guilsinglright", 222 = "fi", 223 = "fl", 224 = "daggerdbl", + 225 = "periodcentered", 226 = "quotesinglbase", 227 = "quotedblbase", 228 = "perthousand", + 229 = "Acircumflex", 230 = "Ecircumflex", 231 = "Aacute", 232 = "Edieresis", + 233 = "Egrave", 234 = "Iacute", 235 = "Icircumflex", 236 = "Idieresis", + 237 = "Igrave", 238 = "Oacute", 239 = "Ocircumflex", 240 = "apple", + 241 = "Ograve", 242 = "Uacute", 243 = "Ucircumflex", 244 = "Ugrave", + 245 = "dotlessi", 246 = "circumflex", 247 = "tilde", 248 = "macron", + 249 = "breve", 250 = "dotaccent", 251 = "ring", 252 = "cedilla", + 253 = "hungarumlaut", 254 = "ogonek", 255 = "caron", +} + +// MacExpertEncoding (ISO 32000-2, Annex D.4). None of the standard 14 fonts +// contains these glyphs; it is here for embedded expert fonts. +@(private, rodata) +ENC_MACEXPERT := [256]string{ + 32 = "space", 33 = "exclamsmall", 34 = "Hungarumlautsmall", 35 = "centoldstyle", + 36 = "dollaroldstyle", 37 = "dollarsuperior", 38 = "ampersandsmall", 39 = "Acutesmall", + 40 = "parenleftsuperior", 41 = "parenrightsuperior", 42 = "twodotenleader", 43 = "onedotenleader", + 44 = "comma", 45 = "hyphen", 46 = "period", 47 = "fraction", + 48 = "zerooldstyle", 49 = "oneoldstyle", 50 = "twooldstyle", 51 = "threeoldstyle", + 52 = "fouroldstyle", 53 = "fiveoldstyle", 54 = "sixoldstyle", 55 = "sevenoldstyle", + 56 = "eightoldstyle", 57 = "nineoldstyle", 58 = "colon", 59 = "semicolon", + 61 = "threequartersemdash", 63 = "questionsmall", 68 = "Ethsmall", 71 = "onequarter", + 72 = "onehalf", 73 = "threequarters", 74 = "oneeighth", 75 = "threeeighths", + 76 = "fiveeighths", 77 = "seveneighths", 78 = "onethird", 79 = "twothirds", + 86 = "ff", 87 = "fi", 88 = "fl", 89 = "ffi", + 90 = "ffl", 91 = "parenleftinferior", 93 = "parenrightinferior", 94 = "Circumflexsmall", + 95 = "hypheninferior", 96 = "Gravesmall", 97 = "Asmall", 98 = "Bsmall", + 99 = "Csmall", 100 = "Dsmall", 101 = "Esmall", 102 = "Fsmall", + 103 = "Gsmall", 104 = "Hsmall", 105 = "Ismall", 106 = "Jsmall", + 107 = "Ksmall", 108 = "Lsmall", 109 = "Msmall", 110 = "Nsmall", + 111 = "Osmall", 112 = "Psmall", 113 = "Qsmall", 114 = "Rsmall", + 115 = "Ssmall", 116 = "Tsmall", 117 = "Usmall", 118 = "Vsmall", + 119 = "Wsmall", 120 = "Xsmall", 121 = "Ysmall", 122 = "Zsmall", + 123 = "colonmonetary", 124 = "onefitted", 125 = "rupiah", 126 = "Tildesmall", + 129 = "asuperior", 130 = "centsuperior", 135 = "Aacutesmall", 136 = "Agravesmall", + 137 = "Acircumflexsmall", 138 = "Adieresissmall", 139 = "Atildesmall", 140 = "Aringsmall", + 141 = "Ccedillasmall", 142 = "Eacutesmall", 143 = "Egravesmall", 144 = "Ecircumflexsmall", + 145 = "Edieresissmall", 146 = "Iacutesmall", 147 = "Igravesmall", 148 = "Icircumflexsmall", + 149 = "Idieresissmall", 150 = "Ntildesmall", 151 = "Oacutesmall", 152 = "Ogravesmall", + 153 = "Ocircumflexsmall", 154 = "Odieresissmall", 155 = "Otildesmall", 156 = "Uacutesmall", + 157 = "Ugravesmall", 158 = "Ucircumflexsmall", 159 = "Udieresissmall", 161 = "eightsuperior", + 162 = "fourinferior", 163 = "threeinferior", 164 = "sixinferior", 165 = "eightinferior", + 166 = "seveninferior", 167 = "Scaronsmall", 169 = "centinferior", 170 = "twoinferior", + 172 = "Dieresissmall", 174 = "Caronsmall", 175 = "osuperior", 176 = "fiveinferior", + 178 = "commainferior", 179 = "periodinferior", 180 = "Yacutesmall", 182 = "dollarinferior", + 185 = "Thornsmall", 187 = "nineinferior", 188 = "zeroinferior", 189 = "Zcaronsmall", + 190 = "AEsmall", 191 = "Oslashsmall", 192 = "questiondownsmall", 193 = "oneinferior", + 194 = "Lslashsmall", 201 = "Cedillasmall", 207 = "OEsmall", 208 = "figuredash", + 209 = "hyphensuperior", 214 = "exclamdownsmall", 216 = "Ydieresissmall", 218 = "onesuperior", + 219 = "twosuperior", 220 = "threesuperior", 221 = "foursuperior", 222 = "fivesuperior", + 223 = "sixsuperior", 224 = "sevensuperior", 225 = "ninesuperior", 226 = "zerosuperior", + 228 = "esuperior", 229 = "rsuperior", 230 = "tsuperior", 233 = "isuperior", + 234 = "ssuperior", 235 = "dsuperior", 241 = "lsuperior", 242 = "Ogoneksmall", + 243 = "Brevesmall", 244 = "Macronsmall", 245 = "bsuperior", 246 = "nsuperior", + 247 = "msuperior", 248 = "commasuperior", 249 = "periodsuperior", 250 = "Dotaccentsmall", + 251 = "Ringsmall", +} + +// Symbol's built-in encoding, taken from the C column of Symbol.afm. +@(private, rodata) +ENC_SYMBOL := [256]string{ + 32 = "space", 33 = "exclam", 34 = "universal", 35 = "numbersign", + 36 = "existential", 37 = "percent", 38 = "ampersand", 39 = "suchthat", + 40 = "parenleft", 41 = "parenright", 42 = "asteriskmath", 43 = "plus", + 44 = "comma", 45 = "minus", 46 = "period", 47 = "slash", + 48 = "zero", 49 = "one", 50 = "two", 51 = "three", + 52 = "four", 53 = "five", 54 = "six", 55 = "seven", + 56 = "eight", 57 = "nine", 58 = "colon", 59 = "semicolon", + 60 = "less", 61 = "equal", 62 = "greater", 63 = "question", + 64 = "congruent", 65 = "Alpha", 66 = "Beta", 67 = "Chi", + 68 = "Delta", 69 = "Epsilon", 70 = "Phi", 71 = "Gamma", + 72 = "Eta", 73 = "Iota", 74 = "theta1", 75 = "Kappa", + 76 = "Lambda", 77 = "Mu", 78 = "Nu", 79 = "Omicron", + 80 = "Pi", 81 = "Theta", 82 = "Rho", 83 = "Sigma", + 84 = "Tau", 85 = "Upsilon", 86 = "sigma1", 87 = "Omega", + 88 = "Xi", 89 = "Psi", 90 = "Zeta", 91 = "bracketleft", + 92 = "therefore", 93 = "bracketright", 94 = "perpendicular", 95 = "underscore", + 96 = "radicalex", 97 = "alpha", 98 = "beta", 99 = "chi", + 100 = "delta", 101 = "epsilon", 102 = "phi", 103 = "gamma", + 104 = "eta", 105 = "iota", 106 = "phi1", 107 = "kappa", + 108 = "lambda", 109 = "mu", 110 = "nu", 111 = "omicron", + 112 = "pi", 113 = "theta", 114 = "rho", 115 = "sigma", + 116 = "tau", 117 = "upsilon", 118 = "omega1", 119 = "omega", + 120 = "xi", 121 = "psi", 122 = "zeta", 123 = "braceleft", + 124 = "bar", 125 = "braceright", 126 = "similar", 160 = "Euro", + 161 = "Upsilon1", 162 = "minute", 163 = "lessequal", 164 = "fraction", + 165 = "infinity", 166 = "florin", 167 = "club", 168 = "diamond", + 169 = "heart", 170 = "spade", 171 = "arrowboth", 172 = "arrowleft", + 173 = "arrowup", 174 = "arrowright", 175 = "arrowdown", 176 = "degree", + 177 = "plusminus", 178 = "second", 179 = "greaterequal", 180 = "multiply", + 181 = "proportional", 182 = "partialdiff", 183 = "bullet", 184 = "divide", + 185 = "notequal", 186 = "equivalence", 187 = "approxequal", 188 = "ellipsis", + 189 = "arrowvertex", 190 = "arrowhorizex", 191 = "carriagereturn", 192 = "aleph", + 193 = "Ifraktur", 194 = "Rfraktur", 195 = "weierstrass", 196 = "circlemultiply", + 197 = "circleplus", 198 = "emptyset", 199 = "intersection", 200 = "union", + 201 = "propersuperset", 202 = "reflexsuperset", 203 = "notsubset", 204 = "propersubset", + 205 = "reflexsubset", 206 = "element", 207 = "notelement", 208 = "angle", + 209 = "gradient", 210 = "registerserif", 211 = "copyrightserif", 212 = "trademarkserif", + 213 = "product", 214 = "radical", 215 = "dotmath", 216 = "logicalnot", + 217 = "logicaland", 218 = "logicalor", 219 = "arrowdblboth", 220 = "arrowdblleft", + 221 = "arrowdblup", 222 = "arrowdblright", 223 = "arrowdbldown", 224 = "lozenge", + 225 = "angleleft", 226 = "registersans", 227 = "copyrightsans", 228 = "trademarksans", + 229 = "summation", 230 = "parenlefttp", 231 = "parenleftex", 232 = "parenleftbt", + 233 = "bracketlefttp", 234 = "bracketleftex", 235 = "bracketleftbt", 236 = "bracelefttp", + 237 = "braceleftmid", 238 = "braceleftbt", 239 = "braceex", 241 = "angleright", + 242 = "integral", 243 = "integraltp", 244 = "integralex", 245 = "integralbt", + 246 = "parenrighttp", 247 = "parenrightex", 248 = "parenrightbt", 249 = "bracketrighttp", + 250 = "bracketrightex", 251 = "bracketrightbt", 252 = "bracerighttp", 253 = "bracerightmid", + 254 = "bracerightbt", +} + +// ZapfDingbats' built-in encoding, from the C column of ZapfDingbats.afm. +@(private, rodata) +ENC_ZAPF := [256]string{ + 32 = "space", 33 = "a1", 34 = "a2", 35 = "a202", + 36 = "a3", 37 = "a4", 38 = "a5", 39 = "a119", + 40 = "a118", 41 = "a117", 42 = "a11", 43 = "a12", + 44 = "a13", 45 = "a14", 46 = "a15", 47 = "a16", + 48 = "a105", 49 = "a17", 50 = "a18", 51 = "a19", + 52 = "a20", 53 = "a21", 54 = "a22", 55 = "a23", + 56 = "a24", 57 = "a25", 58 = "a26", 59 = "a27", + 60 = "a28", 61 = "a6", 62 = "a7", 63 = "a8", + 64 = "a9", 65 = "a10", 66 = "a29", 67 = "a30", + 68 = "a31", 69 = "a32", 70 = "a33", 71 = "a34", + 72 = "a35", 73 = "a36", 74 = "a37", 75 = "a38", + 76 = "a39", 77 = "a40", 78 = "a41", 79 = "a42", + 80 = "a43", 81 = "a44", 82 = "a45", 83 = "a46", + 84 = "a47", 85 = "a48", 86 = "a49", 87 = "a50", + 88 = "a51", 89 = "a52", 90 = "a53", 91 = "a54", + 92 = "a55", 93 = "a56", 94 = "a57", 95 = "a58", + 96 = "a59", 97 = "a60", 98 = "a61", 99 = "a62", + 100 = "a63", 101 = "a64", 102 = "a65", 103 = "a66", + 104 = "a67", 105 = "a68", 106 = "a69", 107 = "a70", + 108 = "a71", 109 = "a72", 110 = "a73", 111 = "a74", + 112 = "a203", 113 = "a75", 114 = "a204", 115 = "a76", + 116 = "a77", 117 = "a78", 118 = "a79", 119 = "a81", + 120 = "a82", 121 = "a83", 122 = "a84", 123 = "a97", + 124 = "a98", 125 = "a99", 126 = "a100", 128 = "a89", + 129 = "a90", 130 = "a93", 131 = "a94", 132 = "a91", + 133 = "a92", 134 = "a205", 135 = "a85", 136 = "a206", + 137 = "a86", 138 = "a87", 139 = "a88", 140 = "a95", + 141 = "a96", 161 = "a101", 162 = "a102", 163 = "a103", + 164 = "a104", 165 = "a106", 166 = "a107", 167 = "a108", + 168 = "a112", 169 = "a111", 170 = "a110", 171 = "a109", + 172 = "a120", 173 = "a121", 174 = "a122", 175 = "a123", + 176 = "a124", 177 = "a125", 178 = "a126", 179 = "a127", + 180 = "a128", 181 = "a129", 182 = "a130", 183 = "a131", + 184 = "a132", 185 = "a133", 186 = "a134", 187 = "a135", + 188 = "a136", 189 = "a137", 190 = "a138", 191 = "a139", + 192 = "a140", 193 = "a141", 194 = "a142", 195 = "a143", + 196 = "a144", 197 = "a145", 198 = "a146", 199 = "a147", + 200 = "a148", 201 = "a149", 202 = "a150", 203 = "a151", + 204 = "a152", 205 = "a153", 206 = "a154", 207 = "a155", + 208 = "a156", 209 = "a157", 210 = "a158", 211 = "a159", + 212 = "a160", 213 = "a161", 214 = "a163", 215 = "a164", + 216 = "a196", 217 = "a165", 218 = "a192", 219 = "a166", + 220 = "a167", 221 = "a168", 222 = "a169", 223 = "a170", + 224 = "a171", 225 = "a172", 226 = "a173", 227 = "a162", + 228 = "a174", 229 = "a175", 230 = "a176", 231 = "a177", + 232 = "a178", 233 = "a179", 234 = "a193", 235 = "a180", + 236 = "a199", 237 = "a181", 238 = "a200", 239 = "a182", + 241 = "a201", 242 = "a183", 243 = "a184", 244 = "a197", + 245 = "a185", 246 = "a194", 247 = "a198", 248 = "a186", + 249 = "a195", 250 = "a187", 251 = "a188", 252 = "a189", + 253 = "a190", 254 = "a191", +} + +// The Adobe Glyph List, restricted to the ranges a PDF actually names: +// Latin and its extensions, combining marks, Greek, Cyrillic, punctuation, +// currency, arrows, mathematics, geometric shapes and dingbats, the private +// use area AGL assigns to the expert glyphs, and the f-ligatures. Every name +// used by the standard 14 fonts or by any built-in encoding is present +// whatever its code point. Names standing for a sequence of code points keep +// the first, since a glyph maps to one rune here. +// +// The 201 ZapfDingbats names (a1..a191) come from Adobe's separate +// zapfdingbats.txt. The two lists share no name, so merging them cannot lose +// anything, and it means dingbat text extracts without threading the font's +// identity down to this table. +@(private) +Agl_Entry :: struct { + name: string, + cp: rune, +} + +// Sorted by name. +@(private, rodata) +AGL_TABLE := [2435]Agl_Entry{ + {"A", 0x0041}, {"AE", 0x00C6}, {"AEacute", 0x01FC}, {"AEmacron", 0x01E2}, + {"AEsmall", 0xF7E6}, {"Aacute", 0x00C1}, {"Aacutesmall", 0xF7E1}, {"Abreve", 0x0102}, + {"Abreveacute", 0x1EAE}, {"Abrevecyrillic", 0x04D0}, {"Abrevedotbelow", 0x1EB6}, {"Abrevegrave", 0x1EB0}, + {"Abrevehookabove", 0x1EB2}, {"Abrevetilde", 0x1EB4}, {"Acaron", 0x01CD}, {"Acircle", 0x24B6}, + {"Acircumflex", 0x00C2}, {"Acircumflexacute", 0x1EA4}, {"Acircumflexdotbelow", 0x1EAC}, {"Acircumflexgrave", 0x1EA6}, + {"Acircumflexhookabove", 0x1EA8}, {"Acircumflexsmall", 0xF7E2}, {"Acircumflextilde", 0x1EAA}, {"Acute", 0xF6C9}, + {"Acutesmall", 0xF7B4}, {"Acyrillic", 0x0410}, {"Adblgrave", 0x0200}, {"Adieresis", 0x00C4}, + {"Adieresiscyrillic", 0x04D2}, {"Adieresismacron", 0x01DE}, {"Adieresissmall", 0xF7E4}, {"Adotbelow", 0x1EA0}, + {"Adotmacron", 0x01E0}, {"Agrave", 0x00C0}, {"Agravesmall", 0xF7E0}, {"Ahookabove", 0x1EA2}, + {"Aiecyrillic", 0x04D4}, {"Ainvertedbreve", 0x0202}, {"Alpha", 0x0391}, {"Alphatonos", 0x0386}, + {"Amacron", 0x0100}, {"Aogonek", 0x0104}, {"Aring", 0x00C5}, {"Aringacute", 0x01FA}, + {"Aringbelow", 0x1E00}, {"Aringsmall", 0xF7E5}, {"Asmall", 0xF761}, {"Atilde", 0x00C3}, + {"Atildesmall", 0xF7E3}, {"B", 0x0042}, {"Bcircle", 0x24B7}, {"Bdotaccent", 0x1E02}, + {"Bdotbelow", 0x1E04}, {"Becyrillic", 0x0411}, {"Beta", 0x0392}, {"Bhook", 0x0181}, + {"Blinebelow", 0x1E06}, {"Brevesmall", 0xF6F4}, {"Bsmall", 0xF762}, {"Btopbar", 0x0182}, + {"C", 0x0043}, {"Cacute", 0x0106}, {"Caron", 0xF6CA}, {"Caronsmall", 0xF6F5}, + {"Ccaron", 0x010C}, {"Ccedilla", 0x00C7}, {"Ccedillaacute", 0x1E08}, {"Ccedillasmall", 0xF7E7}, + {"Ccircle", 0x24B8}, {"Ccircumflex", 0x0108}, {"Cdot", 0x010A}, {"Cdotaccent", 0x010A}, + {"Cedillasmall", 0xF7B8}, {"Cheabkhasiancyrillic", 0x04BC}, {"Checyrillic", 0x0427}, {"Chedescenderabkhasiancyrillic", 0x04BE}, + {"Chedescendercyrillic", 0x04B6}, {"Chedieresiscyrillic", 0x04F4}, {"Chekhakassiancyrillic", 0x04CB}, {"Cheverticalstrokecyrillic", 0x04B8}, + {"Chi", 0x03A7}, {"Chook", 0x0187}, {"Circumflexsmall", 0xF6F6}, {"Csmall", 0xF763}, + {"D", 0x0044}, {"DZ", 0x01F1}, {"DZcaron", 0x01C4}, {"Dafrican", 0x0189}, + {"Dcaron", 0x010E}, {"Dcedilla", 0x1E10}, {"Dcircle", 0x24B9}, {"Dcircumflexbelow", 0x1E12}, + {"Dcroat", 0x0110}, {"Ddotaccent", 0x1E0A}, {"Ddotbelow", 0x1E0C}, {"Decyrillic", 0x0414}, + {"Deicoptic", 0x03EE}, {"Delta", 0x2206}, {"Deltagreek", 0x0394}, {"Dhook", 0x018A}, + {"Dieresis", 0xF6CB}, {"DieresisAcute", 0xF6CC}, {"DieresisGrave", 0xF6CD}, {"Dieresissmall", 0xF7A8}, + {"Digammagreek", 0x03DC}, {"Djecyrillic", 0x0402}, {"Dlinebelow", 0x1E0E}, {"Dotaccentsmall", 0xF6F7}, + {"Dslash", 0x0110}, {"Dsmall", 0xF764}, {"Dtopbar", 0x018B}, {"Dz", 0x01F2}, + {"Dzcaron", 0x01C5}, {"Dzeabkhasiancyrillic", 0x04E0}, {"Dzecyrillic", 0x0405}, {"Dzhecyrillic", 0x040F}, + {"E", 0x0045}, {"Eacute", 0x00C9}, {"Eacutesmall", 0xF7E9}, {"Ebreve", 0x0114}, + {"Ecaron", 0x011A}, {"Ecedillabreve", 0x1E1C}, {"Ecircle", 0x24BA}, {"Ecircumflex", 0x00CA}, + {"Ecircumflexacute", 0x1EBE}, {"Ecircumflexbelow", 0x1E18}, {"Ecircumflexdotbelow", 0x1EC6}, {"Ecircumflexgrave", 0x1EC0}, + {"Ecircumflexhookabove", 0x1EC2}, {"Ecircumflexsmall", 0xF7EA}, {"Ecircumflextilde", 0x1EC4}, {"Ecyrillic", 0x0404}, + {"Edblgrave", 0x0204}, {"Edieresis", 0x00CB}, {"Edieresissmall", 0xF7EB}, {"Edot", 0x0116}, + {"Edotaccent", 0x0116}, {"Edotbelow", 0x1EB8}, {"Efcyrillic", 0x0424}, {"Egrave", 0x00C8}, + {"Egravesmall", 0xF7E8}, {"Ehookabove", 0x1EBA}, {"Eightroman", 0x2167}, {"Einvertedbreve", 0x0206}, + {"Eiotifiedcyrillic", 0x0464}, {"Elcyrillic", 0x041B}, {"Elevenroman", 0x216A}, {"Emacron", 0x0112}, + {"Emacronacute", 0x1E16}, {"Emacrongrave", 0x1E14}, {"Emcyrillic", 0x041C}, {"Encyrillic", 0x041D}, + {"Endescendercyrillic", 0x04A2}, {"Eng", 0x014A}, {"Enghecyrillic", 0x04A4}, {"Enhookcyrillic", 0x04C7}, + {"Eogonek", 0x0118}, {"Eopen", 0x0190}, {"Epsilon", 0x0395}, {"Epsilontonos", 0x0388}, + {"Ercyrillic", 0x0420}, {"Ereversed", 0x018E}, {"Ereversedcyrillic", 0x042D}, {"Escyrillic", 0x0421}, + {"Esdescendercyrillic", 0x04AA}, {"Esh", 0x01A9}, {"Esmall", 0xF765}, {"Eta", 0x0397}, + {"Etatonos", 0x0389}, {"Eth", 0x00D0}, {"Ethsmall", 0xF7F0}, {"Etilde", 0x1EBC}, + {"Etildebelow", 0x1E1A}, {"Euro", 0x20AC}, {"Ezh", 0x01B7}, {"Ezhcaron", 0x01EE}, + {"Ezhreversed", 0x01B8}, {"F", 0x0046}, {"Fcircle", 0x24BB}, {"Fdotaccent", 0x1E1E}, + {"Feicoptic", 0x03E4}, {"Fhook", 0x0191}, {"Fitacyrillic", 0x0472}, {"Fiveroman", 0x2164}, + {"Fourroman", 0x2163}, {"Fsmall", 0xF766}, {"G", 0x0047}, {"Gacute", 0x01F4}, + {"Gamma", 0x0393}, {"Gammaafrican", 0x0194}, {"Gangiacoptic", 0x03EA}, {"Gbreve", 0x011E}, + {"Gcaron", 0x01E6}, {"Gcedilla", 0x0122}, {"Gcircle", 0x24BC}, {"Gcircumflex", 0x011C}, + {"Gcommaaccent", 0x0122}, {"Gdot", 0x0120}, {"Gdotaccent", 0x0120}, {"Gecyrillic", 0x0413}, + {"Ghemiddlehookcyrillic", 0x0494}, {"Ghestrokecyrillic", 0x0492}, {"Gheupturncyrillic", 0x0490}, {"Ghook", 0x0193}, + {"Gjecyrillic", 0x0403}, {"Gmacron", 0x1E20}, {"Grave", 0xF6CE}, {"Gravesmall", 0xF760}, + {"Gsmall", 0xF767}, {"Gsmallhook", 0x029B}, {"Gstroke", 0x01E4}, {"H", 0x0048}, + {"H18533", 0x25CF}, {"H18543", 0x25AA}, {"H18551", 0x25AB}, {"H22073", 0x25A1}, + {"Haabkhasiancyrillic", 0x04A8}, {"Hadescendercyrillic", 0x04B2}, {"Hardsigncyrillic", 0x042A}, {"Hbar", 0x0126}, + {"Hbrevebelow", 0x1E2A}, {"Hcedilla", 0x1E28}, {"Hcircle", 0x24BD}, {"Hcircumflex", 0x0124}, + {"Hdieresis", 0x1E26}, {"Hdotaccent", 0x1E22}, {"Hdotbelow", 0x1E24}, {"Horicoptic", 0x03E8}, + {"Hsmall", 0xF768}, {"Hungarumlaut", 0xF6CF}, {"Hungarumlautsmall", 0xF6F8}, {"I", 0x0049}, + {"IAcyrillic", 0x042F}, {"IJ", 0x0132}, {"IUcyrillic", 0x042E}, {"Iacute", 0x00CD}, + {"Iacutesmall", 0xF7ED}, {"Ibreve", 0x012C}, {"Icaron", 0x01CF}, {"Icircle", 0x24BE}, + {"Icircumflex", 0x00CE}, {"Icircumflexsmall", 0xF7EE}, {"Icyrillic", 0x0406}, {"Idblgrave", 0x0208}, + {"Idieresis", 0x00CF}, {"Idieresisacute", 0x1E2E}, {"Idieresiscyrillic", 0x04E4}, {"Idieresissmall", 0xF7EF}, + {"Idot", 0x0130}, {"Idotaccent", 0x0130}, {"Idotbelow", 0x1ECA}, {"Iebrevecyrillic", 0x04D6}, + {"Iecyrillic", 0x0415}, {"Ifraktur", 0x2111}, {"Igrave", 0x00CC}, {"Igravesmall", 0xF7EC}, + {"Ihookabove", 0x1EC8}, {"Iicyrillic", 0x0418}, {"Iinvertedbreve", 0x020A}, {"Iishortcyrillic", 0x0419}, + {"Imacron", 0x012A}, {"Imacroncyrillic", 0x04E2}, {"Iocyrillic", 0x0401}, {"Iogonek", 0x012E}, + {"Iota", 0x0399}, {"Iotaafrican", 0x0196}, {"Iotadieresis", 0x03AA}, {"Iotatonos", 0x038A}, + {"Ismall", 0xF769}, {"Istroke", 0x0197}, {"Itilde", 0x0128}, {"Itildebelow", 0x1E2C}, + {"Izhitsacyrillic", 0x0474}, {"Izhitsadblgravecyrillic", 0x0476}, {"J", 0x004A}, {"Jcircle", 0x24BF}, + {"Jcircumflex", 0x0134}, {"Jecyrillic", 0x0408}, {"Jsmall", 0xF76A}, {"K", 0x004B}, + {"Kabashkircyrillic", 0x04A0}, {"Kacute", 0x1E30}, {"Kacyrillic", 0x041A}, {"Kadescendercyrillic", 0x049A}, + {"Kahookcyrillic", 0x04C3}, {"Kappa", 0x039A}, {"Kastrokecyrillic", 0x049E}, {"Kaverticalstrokecyrillic", 0x049C}, + {"Kcaron", 0x01E8}, {"Kcedilla", 0x0136}, {"Kcircle", 0x24C0}, {"Kcommaaccent", 0x0136}, + {"Kdotbelow", 0x1E32}, {"Khacyrillic", 0x0425}, {"Kheicoptic", 0x03E6}, {"Khook", 0x0198}, + {"Kjecyrillic", 0x040C}, {"Klinebelow", 0x1E34}, {"Koppacyrillic", 0x0480}, {"Koppagreek", 0x03DE}, + {"Ksicyrillic", 0x046E}, {"Ksmall", 0xF76B}, {"L", 0x004C}, {"LJ", 0x01C7}, + {"LL", 0xF6BF}, {"Lacute", 0x0139}, {"Lambda", 0x039B}, {"Lcaron", 0x013D}, + {"Lcedilla", 0x013B}, {"Lcircle", 0x24C1}, {"Lcircumflexbelow", 0x1E3C}, {"Lcommaaccent", 0x013B}, + {"Ldot", 0x013F}, {"Ldotaccent", 0x013F}, {"Ldotbelow", 0x1E36}, {"Ldotbelowmacron", 0x1E38}, + {"Lj", 0x01C8}, {"Ljecyrillic", 0x0409}, {"Llinebelow", 0x1E3A}, {"Lslash", 0x0141}, + {"Lslashsmall", 0xF6F9}, {"Lsmall", 0xF76C}, {"M", 0x004D}, {"Macron", 0xF6D0}, + {"Macronsmall", 0xF7AF}, {"Macute", 0x1E3E}, {"Mcircle", 0x24C2}, {"Mdotaccent", 0x1E40}, + {"Mdotbelow", 0x1E42}, {"Msmall", 0xF76D}, {"Mturned", 0x019C}, {"Mu", 0x039C}, + {"N", 0x004E}, {"NJ", 0x01CA}, {"Nacute", 0x0143}, {"Ncaron", 0x0147}, + {"Ncedilla", 0x0145}, {"Ncircle", 0x24C3}, {"Ncircumflexbelow", 0x1E4A}, {"Ncommaaccent", 0x0145}, + {"Ndotaccent", 0x1E44}, {"Ndotbelow", 0x1E46}, {"Nhookleft", 0x019D}, {"Nineroman", 0x2168}, + {"Nj", 0x01CB}, {"Njecyrillic", 0x040A}, {"Nlinebelow", 0x1E48}, {"Nsmall", 0xF76E}, + {"Ntilde", 0x00D1}, {"Ntildesmall", 0xF7F1}, {"Nu", 0x039D}, {"O", 0x004F}, + {"OE", 0x0152}, {"OEsmall", 0xF6FA}, {"Oacute", 0x00D3}, {"Oacutesmall", 0xF7F3}, + {"Obarredcyrillic", 0x04E8}, {"Obarreddieresiscyrillic", 0x04EA}, {"Obreve", 0x014E}, {"Ocaron", 0x01D1}, + {"Ocenteredtilde", 0x019F}, {"Ocircle", 0x24C4}, {"Ocircumflex", 0x00D4}, {"Ocircumflexacute", 0x1ED0}, + {"Ocircumflexdotbelow", 0x1ED8}, {"Ocircumflexgrave", 0x1ED2}, {"Ocircumflexhookabove", 0x1ED4}, {"Ocircumflexsmall", 0xF7F4}, + {"Ocircumflextilde", 0x1ED6}, {"Ocyrillic", 0x041E}, {"Odblacute", 0x0150}, {"Odblgrave", 0x020C}, + {"Odieresis", 0x00D6}, {"Odieresiscyrillic", 0x04E6}, {"Odieresissmall", 0xF7F6}, {"Odotbelow", 0x1ECC}, + {"Ogoneksmall", 0xF6FB}, {"Ograve", 0x00D2}, {"Ogravesmall", 0xF7F2}, {"Ohm", 0x2126}, + {"Ohookabove", 0x1ECE}, {"Ohorn", 0x01A0}, {"Ohornacute", 0x1EDA}, {"Ohorndotbelow", 0x1EE2}, + {"Ohorngrave", 0x1EDC}, {"Ohornhookabove", 0x1EDE}, {"Ohorntilde", 0x1EE0}, {"Ohungarumlaut", 0x0150}, + {"Oi", 0x01A2}, {"Oinvertedbreve", 0x020E}, {"Omacron", 0x014C}, {"Omacronacute", 0x1E52}, + {"Omacrongrave", 0x1E50}, {"Omega", 0x2126}, {"Omegacyrillic", 0x0460}, {"Omegagreek", 0x03A9}, + {"Omegaroundcyrillic", 0x047A}, {"Omegatitlocyrillic", 0x047C}, {"Omegatonos", 0x038F}, {"Omicron", 0x039F}, + {"Omicrontonos", 0x038C}, {"Oneroman", 0x2160}, {"Oogonek", 0x01EA}, {"Oogonekmacron", 0x01EC}, + {"Oopen", 0x0186}, {"Oslash", 0x00D8}, {"Oslashacute", 0x01FE}, {"Oslashsmall", 0xF7F8}, + {"Osmall", 0xF76F}, {"Ostrokeacute", 0x01FE}, {"Otcyrillic", 0x047E}, {"Otilde", 0x00D5}, + {"Otildeacute", 0x1E4C}, {"Otildedieresis", 0x1E4E}, {"Otildesmall", 0xF7F5}, {"P", 0x0050}, + {"Pacute", 0x1E54}, {"Pcircle", 0x24C5}, {"Pdotaccent", 0x1E56}, {"Pecyrillic", 0x041F}, + {"Pemiddlehookcyrillic", 0x04A6}, {"Phi", 0x03A6}, {"Phook", 0x01A4}, {"Pi", 0x03A0}, + {"Psi", 0x03A8}, {"Psicyrillic", 0x0470}, {"Psmall", 0xF770}, {"Q", 0x0051}, + {"Qcircle", 0x24C6}, {"Qsmall", 0xF771}, {"R", 0x0052}, {"Racute", 0x0154}, + {"Rcaron", 0x0158}, {"Rcedilla", 0x0156}, {"Rcircle", 0x24C7}, {"Rcommaaccent", 0x0156}, + {"Rdblgrave", 0x0210}, {"Rdotaccent", 0x1E58}, {"Rdotbelow", 0x1E5A}, {"Rdotbelowmacron", 0x1E5C}, + {"Rfraktur", 0x211C}, {"Rho", 0x03A1}, {"Ringsmall", 0xF6FC}, {"Rinvertedbreve", 0x0212}, + {"Rlinebelow", 0x1E5E}, {"Rsmall", 0xF772}, {"Rsmallinverted", 0x0281}, {"Rsmallinvertedsuperior", 0x02B6}, + {"S", 0x0053}, {"SF010000", 0x250C}, {"SF020000", 0x2514}, {"SF030000", 0x2510}, + {"SF040000", 0x2518}, {"SF050000", 0x253C}, {"SF060000", 0x252C}, {"SF070000", 0x2534}, + {"SF080000", 0x251C}, {"SF090000", 0x2524}, {"SF100000", 0x2500}, {"SF110000", 0x2502}, + {"SF190000", 0x2561}, {"SF200000", 0x2562}, {"SF210000", 0x2556}, {"SF220000", 0x2555}, + {"SF230000", 0x2563}, {"SF240000", 0x2551}, {"SF250000", 0x2557}, {"SF260000", 0x255D}, + {"SF270000", 0x255C}, {"SF280000", 0x255B}, {"SF360000", 0x255E}, {"SF370000", 0x255F}, + {"SF380000", 0x255A}, {"SF390000", 0x2554}, {"SF400000", 0x2569}, {"SF410000", 0x2566}, + {"SF420000", 0x2560}, {"SF430000", 0x2550}, {"SF440000", 0x256C}, {"SF450000", 0x2567}, + {"SF460000", 0x2568}, {"SF470000", 0x2564}, {"SF480000", 0x2565}, {"SF490000", 0x2559}, + {"SF500000", 0x2558}, {"SF510000", 0x2552}, {"SF520000", 0x2553}, {"SF530000", 0x256B}, + {"SF540000", 0x256A}, {"Sacute", 0x015A}, {"Sacutedotaccent", 0x1E64}, {"Sampigreek", 0x03E0}, + {"Scaron", 0x0160}, {"Scarondotaccent", 0x1E66}, {"Scaronsmall", 0xF6FD}, {"Scedilla", 0x015E}, + {"Schwa", 0x018F}, {"Schwacyrillic", 0x04D8}, {"Schwadieresiscyrillic", 0x04DA}, {"Scircle", 0x24C8}, + {"Scircumflex", 0x015C}, {"Scommaaccent", 0x0218}, {"Sdotaccent", 0x1E60}, {"Sdotbelow", 0x1E62}, + {"Sdotbelowdotaccent", 0x1E68}, {"Sevenroman", 0x2166}, {"Shacyrillic", 0x0428}, {"Shchacyrillic", 0x0429}, + {"Sheicoptic", 0x03E2}, {"Shhacyrillic", 0x04BA}, {"Shimacoptic", 0x03EC}, {"Sigma", 0x03A3}, + {"Sixroman", 0x2165}, {"Softsigncyrillic", 0x042C}, {"Ssmall", 0xF773}, {"Stigmagreek", 0x03DA}, + {"T", 0x0054}, {"Tau", 0x03A4}, {"Tbar", 0x0166}, {"Tcaron", 0x0164}, + {"Tcedilla", 0x0162}, {"Tcircle", 0x24C9}, {"Tcircumflexbelow", 0x1E70}, {"Tcommaaccent", 0x0162}, + {"Tdotaccent", 0x1E6A}, {"Tdotbelow", 0x1E6C}, {"Tecyrillic", 0x0422}, {"Tedescendercyrillic", 0x04AC}, + {"Tenroman", 0x2169}, {"Tetsecyrillic", 0x04B4}, {"Theta", 0x0398}, {"Thook", 0x01AC}, + {"Thorn", 0x00DE}, {"Thornsmall", 0xF7FE}, {"Threeroman", 0x2162}, {"Tildesmall", 0xF6FE}, + {"Tlinebelow", 0x1E6E}, {"Tonefive", 0x01BC}, {"Tonesix", 0x0184}, {"Tonetwo", 0x01A7}, + {"Tretroflexhook", 0x01AE}, {"Tsecyrillic", 0x0426}, {"Tshecyrillic", 0x040B}, {"Tsmall", 0xF774}, + {"Twelveroman", 0x216B}, {"Tworoman", 0x2161}, {"U", 0x0055}, {"Uacute", 0x00DA}, + {"Uacutesmall", 0xF7FA}, {"Ubreve", 0x016C}, {"Ucaron", 0x01D3}, {"Ucircle", 0x24CA}, + {"Ucircumflex", 0x00DB}, {"Ucircumflexbelow", 0x1E76}, {"Ucircumflexsmall", 0xF7FB}, {"Ucyrillic", 0x0423}, + {"Udblacute", 0x0170}, {"Udblgrave", 0x0214}, {"Udieresis", 0x00DC}, {"Udieresisacute", 0x01D7}, + {"Udieresisbelow", 0x1E72}, {"Udieresiscaron", 0x01D9}, {"Udieresiscyrillic", 0x04F0}, {"Udieresisgrave", 0x01DB}, + {"Udieresismacron", 0x01D5}, {"Udieresissmall", 0xF7FC}, {"Udotbelow", 0x1EE4}, {"Ugrave", 0x00D9}, + {"Ugravesmall", 0xF7F9}, {"Uhookabove", 0x1EE6}, {"Uhorn", 0x01AF}, {"Uhornacute", 0x1EE8}, + {"Uhorndotbelow", 0x1EF0}, {"Uhorngrave", 0x1EEA}, {"Uhornhookabove", 0x1EEC}, {"Uhorntilde", 0x1EEE}, + {"Uhungarumlaut", 0x0170}, {"Uhungarumlautcyrillic", 0x04F2}, {"Uinvertedbreve", 0x0216}, {"Ukcyrillic", 0x0478}, + {"Umacron", 0x016A}, {"Umacroncyrillic", 0x04EE}, {"Umacrondieresis", 0x1E7A}, {"Uogonek", 0x0172}, + {"Upsilon", 0x03A5}, {"Upsilon1", 0x03D2}, {"Upsilonacutehooksymbolgreek", 0x03D3}, {"Upsilonafrican", 0x01B1}, + {"Upsilondieresis", 0x03AB}, {"Upsilondieresishooksymbolgreek", 0x03D4}, {"Upsilonhooksymbol", 0x03D2}, {"Upsilontonos", 0x038E}, + {"Uring", 0x016E}, {"Ushortcyrillic", 0x040E}, {"Usmall", 0xF775}, {"Ustraightcyrillic", 0x04AE}, + {"Ustraightstrokecyrillic", 0x04B0}, {"Utilde", 0x0168}, {"Utildeacute", 0x1E78}, {"Utildebelow", 0x1E74}, + {"V", 0x0056}, {"Vcircle", 0x24CB}, {"Vdotbelow", 0x1E7E}, {"Vecyrillic", 0x0412}, + {"Vhook", 0x01B2}, {"Vsmall", 0xF776}, {"Vtilde", 0x1E7C}, {"W", 0x0057}, + {"Wacute", 0x1E82}, {"Wcircle", 0x24CC}, {"Wcircumflex", 0x0174}, {"Wdieresis", 0x1E84}, + {"Wdotaccent", 0x1E86}, {"Wdotbelow", 0x1E88}, {"Wgrave", 0x1E80}, {"Wsmall", 0xF777}, + {"X", 0x0058}, {"Xcircle", 0x24CD}, {"Xdieresis", 0x1E8C}, {"Xdotaccent", 0x1E8A}, + {"Xi", 0x039E}, {"Xsmall", 0xF778}, {"Y", 0x0059}, {"Yacute", 0x00DD}, + {"Yacutesmall", 0xF7FD}, {"Yatcyrillic", 0x0462}, {"Ycircle", 0x24CE}, {"Ycircumflex", 0x0176}, + {"Ydieresis", 0x0178}, {"Ydieresissmall", 0xF7FF}, {"Ydotaccent", 0x1E8E}, {"Ydotbelow", 0x1EF4}, + {"Yericyrillic", 0x042B}, {"Yerudieresiscyrillic", 0x04F8}, {"Ygrave", 0x1EF2}, {"Yhook", 0x01B3}, + {"Yhookabove", 0x1EF6}, {"Yicyrillic", 0x0407}, {"Ysmall", 0xF779}, {"Ytilde", 0x1EF8}, + {"Yusbigcyrillic", 0x046A}, {"Yusbigiotifiedcyrillic", 0x046C}, {"Yuslittlecyrillic", 0x0466}, {"Yuslittleiotifiedcyrillic", 0x0468}, + {"Z", 0x005A}, {"Zacute", 0x0179}, {"Zcaron", 0x017D}, {"Zcaronsmall", 0xF6FF}, + {"Zcircle", 0x24CF}, {"Zcircumflex", 0x1E90}, {"Zdot", 0x017B}, {"Zdotaccent", 0x017B}, + {"Zdotbelow", 0x1E92}, {"Zecyrillic", 0x0417}, {"Zedescendercyrillic", 0x0498}, {"Zedieresiscyrillic", 0x04DE}, + {"Zeta", 0x0396}, {"Zhebrevecyrillic", 0x04C1}, {"Zhecyrillic", 0x0416}, {"Zhedescendercyrillic", 0x0496}, + {"Zhedieresiscyrillic", 0x04DC}, {"Zlinebelow", 0x1E94}, {"Zsmall", 0xF77A}, {"Zstroke", 0x01B5}, + {"a", 0x0061}, {"a1", 0x2701}, {"a10", 0x2721}, {"a100", 0x275E}, + {"a101", 0x2761}, {"a102", 0x2762}, {"a103", 0x2763}, {"a104", 0x2764}, + {"a105", 0x2710}, {"a106", 0x2765}, {"a107", 0x2766}, {"a108", 0x2767}, + {"a109", 0x2660}, {"a11", 0x261B}, {"a110", 0x2665}, {"a111", 0x2666}, + {"a112", 0x2663}, {"a117", 0x2709}, {"a118", 0x2708}, {"a119", 0x2707}, + {"a12", 0x261E}, {"a120", 0x2460}, {"a121", 0x2461}, {"a122", 0x2462}, + {"a123", 0x2463}, {"a124", 0x2464}, {"a125", 0x2465}, {"a126", 0x2466}, + {"a127", 0x2467}, {"a128", 0x2468}, {"a129", 0x2469}, {"a13", 0x270C}, + {"a130", 0x2776}, {"a131", 0x2777}, {"a132", 0x2778}, {"a133", 0x2779}, + {"a134", 0x277A}, {"a135", 0x277B}, {"a136", 0x277C}, {"a137", 0x277D}, + {"a138", 0x277E}, {"a139", 0x277F}, {"a14", 0x270D}, {"a140", 0x2780}, + {"a141", 0x2781}, {"a142", 0x2782}, {"a143", 0x2783}, {"a144", 0x2784}, + {"a145", 0x2785}, {"a146", 0x2786}, {"a147", 0x2787}, {"a148", 0x2788}, + {"a149", 0x2789}, {"a15", 0x270E}, {"a150", 0x278A}, {"a151", 0x278B}, + {"a152", 0x278C}, {"a153", 0x278D}, {"a154", 0x278E}, {"a155", 0x278F}, + {"a156", 0x2790}, {"a157", 0x2791}, {"a158", 0x2792}, {"a159", 0x2793}, + {"a16", 0x270F}, {"a160", 0x2794}, {"a161", 0x2192}, {"a162", 0x27A3}, + {"a163", 0x2194}, {"a164", 0x2195}, {"a165", 0x2799}, {"a166", 0x279B}, + {"a167", 0x279C}, {"a168", 0x279D}, {"a169", 0x279E}, {"a17", 0x2711}, + {"a170", 0x279F}, {"a171", 0x27A0}, {"a172", 0x27A1}, {"a173", 0x27A2}, + {"a174", 0x27A4}, {"a175", 0x27A5}, {"a176", 0x27A6}, {"a177", 0x27A7}, + {"a178", 0x27A8}, {"a179", 0x27A9}, {"a18", 0x2712}, {"a180", 0x27AB}, + {"a181", 0x27AD}, {"a182", 0x27AF}, {"a183", 0x27B2}, {"a184", 0x27B3}, + {"a185", 0x27B5}, {"a186", 0x27B8}, {"a187", 0x27BA}, {"a188", 0x27BB}, + {"a189", 0x27BC}, {"a19", 0x2713}, {"a190", 0x27BD}, {"a191", 0x27BE}, + {"a192", 0x279A}, {"a193", 0x27AA}, {"a194", 0x27B6}, {"a195", 0x27B9}, + {"a196", 0x2798}, {"a197", 0x27B4}, {"a198", 0x27B7}, {"a199", 0x27AC}, + {"a2", 0x2702}, {"a20", 0x2714}, {"a200", 0x27AE}, {"a201", 0x27B1}, + {"a202", 0x2703}, {"a203", 0x2750}, {"a204", 0x2752}, {"a205", 0x276E}, + {"a206", 0x2770}, {"a21", 0x2715}, {"a22", 0x2716}, {"a23", 0x2717}, + {"a24", 0x2718}, {"a25", 0x2719}, {"a26", 0x271A}, {"a27", 0x271B}, + {"a28", 0x271C}, {"a29", 0x2722}, {"a3", 0x2704}, {"a30", 0x2723}, + {"a31", 0x2724}, {"a32", 0x2725}, {"a33", 0x2726}, {"a34", 0x2727}, + {"a35", 0x2605}, {"a36", 0x2729}, {"a37", 0x272A}, {"a38", 0x272B}, + {"a39", 0x272C}, {"a4", 0x260E}, {"a40", 0x272D}, {"a41", 0x272E}, + {"a42", 0x272F}, {"a43", 0x2730}, {"a44", 0x2731}, {"a45", 0x2732}, + {"a46", 0x2733}, {"a47", 0x2734}, {"a48", 0x2735}, {"a49", 0x2736}, + {"a5", 0x2706}, {"a50", 0x2737}, {"a51", 0x2738}, {"a52", 0x2739}, + {"a53", 0x273A}, {"a54", 0x273B}, {"a55", 0x273C}, {"a56", 0x273D}, + {"a57", 0x273E}, {"a58", 0x273F}, {"a59", 0x2740}, {"a6", 0x271D}, + {"a60", 0x2741}, {"a61", 0x2742}, {"a62", 0x2743}, {"a63", 0x2744}, + {"a64", 0x2745}, {"a65", 0x2746}, {"a66", 0x2747}, {"a67", 0x2748}, + {"a68", 0x2749}, {"a69", 0x274A}, {"a7", 0x271E}, {"a70", 0x274B}, + {"a71", 0x25CF}, {"a72", 0x274D}, {"a73", 0x25A0}, {"a74", 0x274F}, + {"a75", 0x2751}, {"a76", 0x25B2}, {"a77", 0x25BC}, {"a78", 0x25C6}, + {"a79", 0x2756}, {"a8", 0x271F}, {"a81", 0x25D7}, {"a82", 0x2758}, + {"a83", 0x2759}, {"a84", 0x275A}, {"a85", 0x276F}, {"a86", 0x2771}, + {"a87", 0x2772}, {"a88", 0x2773}, {"a89", 0x2768}, {"a9", 0x2720}, + {"a90", 0x2769}, {"a91", 0x276C}, {"a92", 0x276D}, {"a93", 0x276A}, + {"a94", 0x276B}, {"a95", 0x2774}, {"a96", 0x2775}, {"a97", 0x275B}, + {"a98", 0x275C}, {"a99", 0x275D}, {"aacute", 0x00E1}, {"abreve", 0x0103}, + {"abreveacute", 0x1EAF}, {"abrevecyrillic", 0x04D1}, {"abrevedotbelow", 0x1EB7}, {"abrevegrave", 0x1EB1}, + {"abrevehookabove", 0x1EB3}, {"abrevetilde", 0x1EB5}, {"acaron", 0x01CE}, {"acircle", 0x24D0}, + {"acircumflex", 0x00E2}, {"acircumflexacute", 0x1EA5}, {"acircumflexdotbelow", 0x1EAD}, {"acircumflexgrave", 0x1EA7}, + {"acircumflexhookabove", 0x1EA9}, {"acircumflextilde", 0x1EAB}, {"acute", 0x00B4}, {"acutebelowcmb", 0x0317}, + {"acutecmb", 0x0301}, {"acutecomb", 0x0301}, {"acutelowmod", 0x02CF}, {"acutetonecmb", 0x0341}, + {"acyrillic", 0x0430}, {"adblgrave", 0x0201}, {"adieresis", 0x00E4}, {"adieresiscyrillic", 0x04D3}, + {"adieresismacron", 0x01DF}, {"adotbelow", 0x1EA1}, {"adotmacron", 0x01E1}, {"ae", 0x00E6}, + {"aeacute", 0x01FD}, {"aemacron", 0x01E3}, {"afii00208", 0x2015}, {"afii08941", 0x20A4}, + {"afii10017", 0x0410}, {"afii10018", 0x0411}, {"afii10019", 0x0412}, {"afii10020", 0x0413}, + {"afii10021", 0x0414}, {"afii10022", 0x0415}, {"afii10023", 0x0401}, {"afii10024", 0x0416}, + {"afii10025", 0x0417}, {"afii10026", 0x0418}, {"afii10027", 0x0419}, {"afii10028", 0x041A}, + {"afii10029", 0x041B}, {"afii10030", 0x041C}, {"afii10031", 0x041D}, {"afii10032", 0x041E}, + {"afii10033", 0x041F}, {"afii10034", 0x0420}, {"afii10035", 0x0421}, {"afii10036", 0x0422}, + {"afii10037", 0x0423}, {"afii10038", 0x0424}, {"afii10039", 0x0425}, {"afii10040", 0x0426}, + {"afii10041", 0x0427}, {"afii10042", 0x0428}, {"afii10043", 0x0429}, {"afii10044", 0x042A}, + {"afii10045", 0x042B}, {"afii10046", 0x042C}, {"afii10047", 0x042D}, {"afii10048", 0x042E}, + {"afii10049", 0x042F}, {"afii10050", 0x0490}, {"afii10051", 0x0402}, {"afii10052", 0x0403}, + {"afii10053", 0x0404}, {"afii10054", 0x0405}, {"afii10055", 0x0406}, {"afii10056", 0x0407}, + {"afii10057", 0x0408}, {"afii10058", 0x0409}, {"afii10059", 0x040A}, {"afii10060", 0x040B}, + {"afii10061", 0x040C}, {"afii10062", 0x040E}, {"afii10063", 0xF6C4}, {"afii10064", 0xF6C5}, + {"afii10065", 0x0430}, {"afii10066", 0x0431}, {"afii10067", 0x0432}, {"afii10068", 0x0433}, + {"afii10069", 0x0434}, {"afii10070", 0x0435}, {"afii10071", 0x0451}, {"afii10072", 0x0436}, + {"afii10073", 0x0437}, {"afii10074", 0x0438}, {"afii10075", 0x0439}, {"afii10076", 0x043A}, + {"afii10077", 0x043B}, {"afii10078", 0x043C}, {"afii10079", 0x043D}, {"afii10080", 0x043E}, + {"afii10081", 0x043F}, {"afii10082", 0x0440}, {"afii10083", 0x0441}, {"afii10084", 0x0442}, + {"afii10085", 0x0443}, {"afii10086", 0x0444}, {"afii10087", 0x0445}, {"afii10088", 0x0446}, + {"afii10089", 0x0447}, {"afii10090", 0x0448}, {"afii10091", 0x0449}, {"afii10092", 0x044A}, + {"afii10093", 0x044B}, {"afii10094", 0x044C}, {"afii10095", 0x044D}, {"afii10096", 0x044E}, + {"afii10097", 0x044F}, {"afii10098", 0x0491}, {"afii10099", 0x0452}, {"afii10100", 0x0453}, + {"afii10101", 0x0454}, {"afii10102", 0x0455}, {"afii10103", 0x0456}, {"afii10104", 0x0457}, + {"afii10105", 0x0458}, {"afii10106", 0x0459}, {"afii10107", 0x045A}, {"afii10108", 0x045B}, + {"afii10109", 0x045C}, {"afii10110", 0x045E}, {"afii10145", 0x040F}, {"afii10146", 0x0462}, + {"afii10147", 0x0472}, {"afii10148", 0x0474}, {"afii10192", 0xF6C6}, {"afii10193", 0x045F}, + {"afii10194", 0x0463}, {"afii10195", 0x0473}, {"afii10196", 0x0475}, {"afii10831", 0xF6C7}, + {"afii10832", 0xF6C8}, {"afii10846", 0x04D9}, {"afii299", 0x200E}, {"afii300", 0x200F}, + {"afii301", 0x200D}, {"afii57636", 0x20AA}, {"afii57705", 0xFB1F}, {"afii57929", 0x02BC}, + {"afii61248", 0x2105}, {"afii61289", 0x2113}, {"afii61352", 0x2116}, {"afii61573", 0x202C}, + {"afii61574", 0x202D}, {"afii61575", 0x202E}, {"afii61664", 0x200C}, {"afii64937", 0x02BD}, + {"agrave", 0x00E0}, {"ahookabove", 0x1EA3}, {"aiecyrillic", 0x04D5}, {"ainvertedbreve", 0x0203}, + {"aleph", 0x2135}, {"allequal", 0x224C}, {"alpha", 0x03B1}, {"alphatonos", 0x03AC}, + {"amacron", 0x0101}, {"ampersand", 0x0026}, {"ampersandsmall", 0xF726}, {"angle", 0x2220}, + {"angleleft", 0x2329}, {"angleright", 0x232A}, {"angstrom", 0x212B}, {"anoteleia", 0x0387}, + {"aogonek", 0x0105}, {"aparen", 0x249C}, {"apostrophemod", 0x02BC}, {"apple", 0xF8FF}, + {"approaches", 0x2250}, {"approxequal", 0x2248}, {"approxequalorimage", 0x2252}, {"approximatelyequal", 0x2245}, + {"arc", 0x2312}, {"arighthalfring", 0x1E9A}, {"aring", 0x00E5}, {"aringacute", 0x01FB}, + {"aringbelow", 0x1E01}, {"arrowboth", 0x2194}, {"arrowdashdown", 0x21E3}, {"arrowdashleft", 0x21E0}, + {"arrowdashright", 0x21E2}, {"arrowdashup", 0x21E1}, {"arrowdblboth", 0x21D4}, {"arrowdbldown", 0x21D3}, + {"arrowdblleft", 0x21D0}, {"arrowdblright", 0x21D2}, {"arrowdblup", 0x21D1}, {"arrowdown", 0x2193}, + {"arrowdownleft", 0x2199}, {"arrowdownright", 0x2198}, {"arrowdownwhite", 0x21E9}, {"arrowheaddownmod", 0x02C5}, + {"arrowheadleftmod", 0x02C2}, {"arrowheadrightmod", 0x02C3}, {"arrowheadupmod", 0x02C4}, {"arrowhorizex", 0xF8E7}, + {"arrowleft", 0x2190}, {"arrowleftdbl", 0x21D0}, {"arrowleftdblstroke", 0x21CD}, {"arrowleftoverright", 0x21C6}, + {"arrowleftwhite", 0x21E6}, {"arrowright", 0x2192}, {"arrowrightdblstroke", 0x21CF}, {"arrowrightheavy", 0x279E}, + {"arrowrightoverleft", 0x21C4}, {"arrowrightwhite", 0x21E8}, {"arrowtableft", 0x21E4}, {"arrowtabright", 0x21E5}, + {"arrowup", 0x2191}, {"arrowupdn", 0x2195}, {"arrowupdnbse", 0x21A8}, {"arrowupdownbase", 0x21A8}, + {"arrowupleft", 0x2196}, {"arrowupleftofdown", 0x21C5}, {"arrowupright", 0x2197}, {"arrowupwhite", 0x21E7}, + {"arrowvertex", 0xF8E6}, {"asciicircum", 0x005E}, {"asciitilde", 0x007E}, {"ascript", 0x0251}, + {"ascriptturned", 0x0252}, {"asterisk", 0x002A}, {"asteriskmath", 0x2217}, {"asterism", 0x2042}, + {"asuperior", 0xF6E9}, {"asymptoticallyequal", 0x2243}, {"at", 0x0040}, {"atilde", 0x00E3}, + {"aturned", 0x0250}, {"b", 0x0062}, {"backslash", 0x005C}, {"bar", 0x007C}, + {"bcircle", 0x24D1}, {"bdotaccent", 0x1E03}, {"bdotbelow", 0x1E05}, {"beamedsixteenthnotes", 0x266C}, + {"because", 0x2235}, {"becyrillic", 0x0431}, {"beta", 0x03B2}, {"betasymbolgreek", 0x03D0}, + {"bhook", 0x0253}, {"bilabialclick", 0x0298}, {"blackcircle", 0x25CF}, {"blackdiamond", 0x25C6}, + {"blackdownpointingtriangle", 0x25BC}, {"blackleftpointingpointer", 0x25C4}, {"blackleftpointingtriangle", 0x25C0}, {"blacklowerlefttriangle", 0x25E3}, + {"blacklowerrighttriangle", 0x25E2}, {"blackrectangle", 0x25AC}, {"blackrightpointingpointer", 0x25BA}, {"blackrightpointingtriangle", 0x25B6}, + {"blacksmallsquare", 0x25AA}, {"blacksmilingface", 0x263B}, {"blacksquare", 0x25A0}, {"blackstar", 0x2605}, + {"blackupperlefttriangle", 0x25E4}, {"blackupperrighttriangle", 0x25E5}, {"blackuppointingsmalltriangle", 0x25B4}, {"blackuppointingtriangle", 0x25B2}, + {"blank", 0x2423}, {"blinebelow", 0x1E07}, {"block", 0x2588}, {"bparen", 0x249D}, + {"braceex", 0xF8F4}, {"braceleft", 0x007B}, {"braceleftbt", 0xF8F3}, {"braceleftmid", 0xF8F2}, + {"bracelefttp", 0xF8F1}, {"braceright", 0x007D}, {"bracerightbt", 0xF8FE}, {"bracerightmid", 0xF8FD}, + {"bracerighttp", 0xF8FC}, {"bracketleft", 0x005B}, {"bracketleftbt", 0xF8F0}, {"bracketleftex", 0xF8EF}, + {"bracketlefttp", 0xF8EE}, {"bracketright", 0x005D}, {"bracketrightbt", 0xF8FB}, {"bracketrightex", 0xF8FA}, + {"bracketrighttp", 0xF8F9}, {"breve", 0x02D8}, {"brevebelowcmb", 0x032E}, {"brevecmb", 0x0306}, + {"breveinvertedbelowcmb", 0x032F}, {"breveinvertedcmb", 0x0311}, {"breveinverteddoublecmb", 0x0361}, {"bridgebelowcmb", 0x032A}, + {"bridgeinvertedbelowcmb", 0x033A}, {"brokenbar", 0x00A6}, {"bstroke", 0x0180}, {"bsuperior", 0xF6EA}, + {"btopbar", 0x0183}, {"bullet", 0x2022}, {"bulletinverse", 0x25D8}, {"bulletoperator", 0x2219}, + {"bullseye", 0x25CE}, {"c", 0x0063}, {"cacute", 0x0107}, {"candrabinducmb", 0x0310}, + {"capslock", 0x21EA}, {"careof", 0x2105}, {"caron", 0x02C7}, {"caronbelowcmb", 0x032C}, + {"caroncmb", 0x030C}, {"carriagereturn", 0x21B5}, {"ccaron", 0x010D}, {"ccedilla", 0x00E7}, + {"ccedillaacute", 0x1E09}, {"ccircle", 0x24D2}, {"ccircumflex", 0x0109}, {"ccurl", 0x0255}, + {"cdot", 0x010B}, {"cdotaccent", 0x010B}, {"cedilla", 0x00B8}, {"cedillacmb", 0x0327}, + {"cent", 0x00A2}, {"centigrade", 0x2103}, {"centinferior", 0xF6DF}, {"centoldstyle", 0xF7A2}, + {"centsuperior", 0xF6E0}, {"cheabkhasiancyrillic", 0x04BD}, {"checkmark", 0x2713}, {"checyrillic", 0x0447}, + {"chedescenderabkhasiancyrillic", 0x04BF}, {"chedescendercyrillic", 0x04B7}, {"chedieresiscyrillic", 0x04F5}, {"chekhakassiancyrillic", 0x04CC}, + {"cheverticalstrokecyrillic", 0x04B9}, {"chi", 0x03C7}, {"chook", 0x0188}, {"circle", 0x25CB}, + {"circlemultiply", 0x2297}, {"circleot", 0x2299}, {"circleplus", 0x2295}, {"circlewithlefthalfblack", 0x25D0}, + {"circlewithrighthalfblack", 0x25D1}, {"circumflex", 0x02C6}, {"circumflexbelowcmb", 0x032D}, {"circumflexcmb", 0x0302}, + {"clear", 0x2327}, {"clickalveolar", 0x01C2}, {"clickdental", 0x01C0}, {"clicklateral", 0x01C1}, + {"clickretroflex", 0x01C3}, {"club", 0x2663}, {"clubsuitblack", 0x2663}, {"clubsuitwhite", 0x2667}, + {"colon", 0x003A}, {"colonmonetary", 0x20A1}, {"colonsign", 0x20A1}, {"colontriangularhalfmod", 0x02D1}, + {"colontriangularmod", 0x02D0}, {"comma", 0x002C}, {"commaabovecmb", 0x0313}, {"commaaboverightcmb", 0x0315}, + {"commaaccent", 0xF6C3}, {"commainferior", 0xF6E1}, {"commareversedabovecmb", 0x0314}, {"commareversedmod", 0x02BD}, + {"commasuperior", 0xF6E2}, {"commaturnedabovecmb", 0x0312}, {"commaturnedmod", 0x02BB}, {"compass", 0x263C}, + {"congruent", 0x2245}, {"contourintegral", 0x222E}, {"control", 0x2303}, {"controlACK", 0x0006}, + {"controlBEL", 0x0007}, {"controlBS", 0x0008}, {"controlCAN", 0x0018}, {"controlCR", 0x000D}, + {"controlDC1", 0x0011}, {"controlDC2", 0x0012}, {"controlDC3", 0x0013}, {"controlDC4", 0x0014}, + {"controlDEL", 0x007F}, {"controlDLE", 0x0010}, {"controlEM", 0x0019}, {"controlENQ", 0x0005}, + {"controlEOT", 0x0004}, {"controlESC", 0x001B}, {"controlETB", 0x0017}, {"controlETX", 0x0003}, + {"controlFF", 0x000C}, {"controlFS", 0x001C}, {"controlGS", 0x001D}, {"controlHT", 0x0009}, + {"controlLF", 0x000A}, {"controlNAK", 0x0015}, {"controlRS", 0x001E}, {"controlSI", 0x000F}, + {"controlSO", 0x000E}, {"controlSOT", 0x0002}, {"controlSTX", 0x0001}, {"controlSUB", 0x001A}, + {"controlSYN", 0x0016}, {"controlUS", 0x001F}, {"controlVT", 0x000B}, {"copyright", 0x00A9}, + {"copyrightsans", 0xF8E9}, {"copyrightserif", 0xF6D9}, {"cparen", 0x249E}, {"cruzeiro", 0x20A2}, + {"cstretched", 0x0297}, {"curlyand", 0x22CF}, {"curlyor", 0x22CE}, {"currency", 0x00A4}, + {"cyrBreve", 0xF6D1}, {"cyrFlex", 0xF6D2}, {"cyrbreve", 0xF6D4}, {"cyrflex", 0xF6D5}, + {"d", 0x0064}, {"dagger", 0x2020}, {"daggerdbl", 0x2021}, {"dasiapneumatacyrilliccmb", 0x0485}, + {"dblGrave", 0xF6D3}, {"dblarchinvertedbelowcmb", 0x032B}, {"dblarrowleft", 0x21D4}, {"dblarrowright", 0x21D2}, + {"dblgrave", 0xF6D6}, {"dblgravecmb", 0x030F}, {"dblintegral", 0x222C}, {"dbllowline", 0x2017}, + {"dbllowlinecmb", 0x0333}, {"dbloverlinecmb", 0x033F}, {"dblprimemod", 0x02BA}, {"dblverticalbar", 0x2016}, + {"dblverticallineabovecmb", 0x030E}, {"dcaron", 0x010F}, {"dcedilla", 0x1E11}, {"dcircle", 0x24D3}, + {"dcircumflexbelow", 0x1E13}, {"dcroat", 0x0111}, {"ddotaccent", 0x1E0B}, {"ddotbelow", 0x1E0D}, + {"decyrillic", 0x0434}, {"degree", 0x00B0}, {"deicoptic", 0x03EF}, {"deleteleft", 0x232B}, + {"deleteright", 0x2326}, {"delta", 0x03B4}, {"deltaturned", 0x018D}, {"dezh", 0x02A4}, + {"dhook", 0x0257}, {"dialytikatonos", 0x0385}, {"dialytikatonoscmb", 0x0344}, {"diamond", 0x2666}, + {"diamondsuitwhite", 0x2662}, {"dieresis", 0x00A8}, {"dieresisacute", 0xF6D7}, {"dieresisbelowcmb", 0x0324}, + {"dieresiscmb", 0x0308}, {"dieresisgrave", 0xF6D8}, {"dieresistonos", 0x0385}, {"divide", 0x00F7}, + {"divides", 0x2223}, {"divisionslash", 0x2215}, {"djecyrillic", 0x0452}, {"dkshade", 0x2593}, + {"dlinebelow", 0x1E0F}, {"dmacron", 0x0111}, {"dnblock", 0x2584}, {"dollar", 0x0024}, + {"dollarinferior", 0xF6E3}, {"dollaroldstyle", 0xF724}, {"dollarsuperior", 0xF6E4}, {"dong", 0x20AB}, + {"dotaccent", 0x02D9}, {"dotaccentcmb", 0x0307}, {"dotbelowcmb", 0x0323}, {"dotbelowcomb", 0x0323}, + {"dotlessi", 0x0131}, {"dotlessj", 0xF6BE}, {"dotlessjstrokehook", 0x0284}, {"dotmath", 0x22C5}, + {"dottedcircle", 0x25CC}, {"doubleyodpatah", 0xFB1F}, {"doubleyodpatahhebrew", 0xFB1F}, {"downtackbelowcmb", 0x031E}, + {"downtackmod", 0x02D5}, {"dparen", 0x249F}, {"dsuperior", 0xF6EB}, {"dtail", 0x0256}, + {"dtopbar", 0x018C}, {"dz", 0x01F3}, {"dzaltone", 0x02A3}, {"dzcaron", 0x01C6}, + {"dzcurl", 0x02A5}, {"dzeabkhasiancyrillic", 0x04E1}, {"dzecyrillic", 0x0455}, {"dzhecyrillic", 0x045F}, + {"e", 0x0065}, {"eacute", 0x00E9}, {"earth", 0x2641}, {"ebreve", 0x0115}, + {"ecaron", 0x011B}, {"ecedillabreve", 0x1E1D}, {"ecircle", 0x24D4}, {"ecircumflex", 0x00EA}, + {"ecircumflexacute", 0x1EBF}, {"ecircumflexbelow", 0x1E19}, {"ecircumflexdotbelow", 0x1EC7}, {"ecircumflexgrave", 0x1EC1}, + {"ecircumflexhookabove", 0x1EC3}, {"ecircumflextilde", 0x1EC5}, {"ecyrillic", 0x0454}, {"edblgrave", 0x0205}, + {"edieresis", 0x00EB}, {"edot", 0x0117}, {"edotaccent", 0x0117}, {"edotbelow", 0x1EB9}, + {"efcyrillic", 0x0444}, {"egrave", 0x00E8}, {"ehookabove", 0x1EBB}, {"eight", 0x0038}, + {"eightcircle", 0x2467}, {"eightcircleinversesansserif", 0x2791}, {"eighteencircle", 0x2471}, {"eighteenparen", 0x2485}, + {"eighteenperiod", 0x2499}, {"eighthnotebeamed", 0x266B}, {"eightinferior", 0x2088}, {"eightoldstyle", 0xF738}, + {"eightparen", 0x247B}, {"eightperiod", 0x248F}, {"eightroman", 0x2177}, {"eightsuperior", 0x2078}, + {"einvertedbreve", 0x0207}, {"eiotifiedcyrillic", 0x0465}, {"elcyrillic", 0x043B}, {"element", 0x2208}, + {"elevencircle", 0x246A}, {"elevenparen", 0x247E}, {"elevenperiod", 0x2492}, {"elevenroman", 0x217A}, + {"ellipsis", 0x2026}, {"ellipsisvertical", 0x22EE}, {"emacron", 0x0113}, {"emacronacute", 0x1E17}, + {"emacrongrave", 0x1E15}, {"emcyrillic", 0x043C}, {"emdash", 0x2014}, {"emptyset", 0x2205}, + {"encyrillic", 0x043D}, {"endash", 0x2013}, {"endescendercyrillic", 0x04A3}, {"eng", 0x014B}, + {"enghecyrillic", 0x04A5}, {"enhookcyrillic", 0x04C8}, {"enspace", 0x2002}, {"eogonek", 0x0119}, + {"eopen", 0x025B}, {"eopenclosed", 0x029A}, {"eopenreversed", 0x025C}, {"eopenreversedclosed", 0x025E}, + {"eopenreversedhook", 0x025D}, {"eparen", 0x24A0}, {"epsilon", 0x03B5}, {"epsilontonos", 0x03AD}, + {"equal", 0x003D}, {"equalsuperior", 0x207C}, {"equivalence", 0x2261}, {"ercyrillic", 0x0440}, + {"ereversed", 0x0258}, {"ereversedcyrillic", 0x044D}, {"escyrillic", 0x0441}, {"esdescendercyrillic", 0x04AB}, + {"esh", 0x0283}, {"eshcurl", 0x0286}, {"eshreversedloop", 0x01AA}, {"eshsquatreversed", 0x0285}, + {"estimated", 0x212E}, {"esuperior", 0xF6EC}, {"eta", 0x03B7}, {"etatonos", 0x03AE}, + {"eth", 0x00F0}, {"etilde", 0x1EBD}, {"etildebelow", 0x1E1B}, {"eturned", 0x01DD}, + {"euro", 0x20AC}, {"exclam", 0x0021}, {"exclamdbl", 0x203C}, {"exclamdown", 0x00A1}, + {"exclamdownsmall", 0xF7A1}, {"exclamsmall", 0xF721}, {"existential", 0x2203}, {"ezh", 0x0292}, + {"ezhcaron", 0x01EF}, {"ezhcurl", 0x0293}, {"ezhreversed", 0x01B9}, {"ezhtail", 0x01BA}, + {"f", 0x0066}, {"fahrenheit", 0x2109}, {"fcircle", 0x24D5}, {"fdotaccent", 0x1E1F}, + {"feicoptic", 0x03E5}, {"female", 0x2640}, {"ff", 0xFB00}, {"ffi", 0xFB03}, + {"ffl", 0xFB04}, {"fi", 0xFB01}, {"fifteencircle", 0x246E}, {"fifteenparen", 0x2482}, + {"fifteenperiod", 0x2496}, {"figuredash", 0x2012}, {"filledbox", 0x25A0}, {"filledrect", 0x25AC}, + {"firsttonechinese", 0x02C9}, {"fisheye", 0x25C9}, {"fitacyrillic", 0x0473}, {"five", 0x0035}, + {"fivecircle", 0x2464}, {"fivecircleinversesansserif", 0x278E}, {"fiveeighths", 0x215D}, {"fiveinferior", 0x2085}, + {"fiveoldstyle", 0xF735}, {"fiveparen", 0x2478}, {"fiveperiod", 0x248C}, {"fiveroman", 0x2174}, + {"fivesuperior", 0x2075}, {"fl", 0xFB02}, {"florin", 0x0192}, {"forall", 0x2200}, + {"four", 0x0034}, {"fourcircle", 0x2463}, {"fourcircleinversesansserif", 0x278D}, {"fourinferior", 0x2084}, + {"fouroldstyle", 0xF734}, {"fourparen", 0x2477}, {"fourperiod", 0x248B}, {"fourroman", 0x2173}, + {"foursuperior", 0x2074}, {"fourteencircle", 0x246D}, {"fourteenparen", 0x2481}, {"fourteenperiod", 0x2495}, + {"fourthtonechinese", 0x02CB}, {"fparen", 0x24A1}, {"fraction", 0x2044}, {"franc", 0x20A3}, + {"g", 0x0067}, {"gacute", 0x01F5}, {"gamma", 0x03B3}, {"gammalatinsmall", 0x0263}, + {"gammasuperior", 0x02E0}, {"gangiacoptic", 0x03EB}, {"gbreve", 0x011F}, {"gcaron", 0x01E7}, + {"gcedilla", 0x0123}, {"gcircle", 0x24D6}, {"gcircumflex", 0x011D}, {"gcommaaccent", 0x0123}, + {"gdot", 0x0121}, {"gdotaccent", 0x0121}, {"gecyrillic", 0x0433}, {"geometricallyequal", 0x2251}, + {"germandbls", 0x00DF}, {"ghemiddlehookcyrillic", 0x0495}, {"ghestrokecyrillic", 0x0493}, {"gheupturncyrillic", 0x0491}, + {"ghook", 0x0260}, {"gjecyrillic", 0x0453}, {"glottalinvertedstroke", 0x01BE}, {"glottalstop", 0x0294}, + {"glottalstopinverted", 0x0296}, {"glottalstopmod", 0x02C0}, {"glottalstopreversed", 0x0295}, {"glottalstopreversedmod", 0x02C1}, + {"glottalstopreversedsuperior", 0x02E4}, {"glottalstopstroke", 0x02A1}, {"glottalstopstrokereversed", 0x02A2}, {"gmacron", 0x1E21}, + {"gparen", 0x24A2}, {"gradient", 0x2207}, {"grave", 0x0060}, {"gravebelowcmb", 0x0316}, + {"gravecmb", 0x0300}, {"gravecomb", 0x0300}, {"gravelowmod", 0x02CE}, {"gravetonecmb", 0x0340}, + {"greater", 0x003E}, {"greaterequal", 0x2265}, {"greaterequalorless", 0x22DB}, {"greaterorequivalent", 0x2273}, + {"greaterorless", 0x2277}, {"greateroverequal", 0x2267}, {"gscript", 0x0261}, {"gstroke", 0x01E5}, + {"guillemotleft", 0x00AB}, {"guillemotright", 0x00BB}, {"guilsinglleft", 0x2039}, {"guilsinglright", 0x203A}, + {"h", 0x0068}, {"haabkhasiancyrillic", 0x04A9}, {"hadescendercyrillic", 0x04B3}, {"hardsigncyrillic", 0x044A}, + {"harpoonleftbarbup", 0x21BC}, {"harpoonrightbarbup", 0x21C0}, {"hbar", 0x0127}, {"hbrevebelow", 0x1E2B}, + {"hcedilla", 0x1E29}, {"hcircle", 0x24D7}, {"hcircumflex", 0x0125}, {"hdieresis", 0x1E27}, + {"hdotaccent", 0x1E23}, {"hdotbelow", 0x1E25}, {"heart", 0x2665}, {"heartsuitblack", 0x2665}, + {"heartsuitwhite", 0x2661}, {"henghook", 0x0267}, {"hhook", 0x0266}, {"hhooksuperior", 0x02B1}, + {"hlinebelow", 0x1E96}, {"hookabovecomb", 0x0309}, {"hookcmb", 0x0309}, {"hookpalatalizedbelowcmb", 0x0321}, + {"hookretroflexbelowcmb", 0x0322}, {"horicoptic", 0x03E9}, {"horizontalbar", 0x2015}, {"horncmb", 0x031B}, + {"hotsprings", 0x2668}, {"house", 0x2302}, {"hparen", 0x24A3}, {"hsuperior", 0x02B0}, + {"hturned", 0x0265}, {"hungarumlaut", 0x02DD}, {"hungarumlautcmb", 0x030B}, {"hv", 0x0195}, + {"hyphen", 0x002D}, {"hypheninferior", 0xF6E5}, {"hyphensuperior", 0xF6E6}, {"hyphentwo", 0x2010}, + {"i", 0x0069}, {"iacute", 0x00ED}, {"iacyrillic", 0x044F}, {"ibreve", 0x012D}, + {"icaron", 0x01D0}, {"icircle", 0x24D8}, {"icircumflex", 0x00EE}, {"icyrillic", 0x0456}, + {"idblgrave", 0x0209}, {"idieresis", 0x00EF}, {"idieresisacute", 0x1E2F}, {"idieresiscyrillic", 0x04E5}, + {"idotbelow", 0x1ECB}, {"iebrevecyrillic", 0x04D7}, {"iecyrillic", 0x0435}, {"igrave", 0x00EC}, + {"ihookabove", 0x1EC9}, {"iicyrillic", 0x0438}, {"iinvertedbreve", 0x020B}, {"iishortcyrillic", 0x0439}, + {"ij", 0x0133}, {"ilde", 0x02DC}, {"imacron", 0x012B}, {"imacroncyrillic", 0x04E3}, + {"imageorapproximatelyequal", 0x2253}, {"increment", 0x2206}, {"infinity", 0x221E}, {"integral", 0x222B}, + {"integralbottom", 0x2321}, {"integralbt", 0x2321}, {"integralex", 0xF8F5}, {"integraltop", 0x2320}, + {"integraltp", 0x2320}, {"intersection", 0x2229}, {"invbullet", 0x25D8}, {"invcircle", 0x25D9}, + {"invsmileface", 0x263B}, {"iocyrillic", 0x0451}, {"iogonek", 0x012F}, {"iota", 0x03B9}, + {"iotadieresis", 0x03CA}, {"iotadieresistonos", 0x0390}, {"iotalatin", 0x0269}, {"iotatonos", 0x03AF}, + {"iparen", 0x24A4}, {"istroke", 0x0268}, {"isuperior", 0xF6ED}, {"itilde", 0x0129}, + {"itildebelow", 0x1E2D}, {"iucyrillic", 0x044E}, {"izhitsacyrillic", 0x0475}, {"izhitsadblgravecyrillic", 0x0477}, + {"j", 0x006A}, {"jcaron", 0x01F0}, {"jcircle", 0x24D9}, {"jcircumflex", 0x0135}, + {"jcrossedtail", 0x029D}, {"jdotlessstroke", 0x025F}, {"jecyrillic", 0x0458}, {"jparen", 0x24A5}, + {"jsuperior", 0x02B2}, {"k", 0x006B}, {"kabashkircyrillic", 0x04A1}, {"kacute", 0x1E31}, + {"kacyrillic", 0x043A}, {"kadescendercyrillic", 0x049B}, {"kahookcyrillic", 0x04C4}, {"kappa", 0x03BA}, + {"kappasymbolgreek", 0x03F0}, {"kastrokecyrillic", 0x049F}, {"kaverticalstrokecyrillic", 0x049D}, {"kcaron", 0x01E9}, + {"kcedilla", 0x0137}, {"kcircle", 0x24DA}, {"kcommaaccent", 0x0137}, {"kdotbelow", 0x1E33}, + {"kgreenlandic", 0x0138}, {"khacyrillic", 0x0445}, {"kheicoptic", 0x03E7}, {"khook", 0x0199}, + {"kjecyrillic", 0x045C}, {"klinebelow", 0x1E35}, {"koppacyrillic", 0x0481}, {"koroniscmb", 0x0343}, + {"kparen", 0x24A6}, {"ksicyrillic", 0x046F}, {"kturned", 0x029E}, {"l", 0x006C}, + {"lacute", 0x013A}, {"lambda", 0x03BB}, {"lambdastroke", 0x019B}, {"largecircle", 0x25EF}, + {"lbar", 0x019A}, {"lbelt", 0x026C}, {"lcaron", 0x013E}, {"lcedilla", 0x013C}, + {"lcircle", 0x24DB}, {"lcircumflexbelow", 0x1E3D}, {"lcommaaccent", 0x013C}, {"ldot", 0x0140}, + {"ldotaccent", 0x0140}, {"ldotbelow", 0x1E37}, {"ldotbelowmacron", 0x1E39}, {"leftangleabovecmb", 0x031A}, + {"lefttackbelowcmb", 0x0318}, {"less", 0x003C}, {"lessequal", 0x2264}, {"lessequalorgreater", 0x22DA}, + {"lessorequivalent", 0x2272}, {"lessorgreater", 0x2276}, {"lessoverequal", 0x2266}, {"lezh", 0x026E}, + {"lfblock", 0x258C}, {"lhookretroflex", 0x026D}, {"lira", 0x20A4}, {"lj", 0x01C9}, + {"ljecyrillic", 0x0459}, {"ll", 0xF6C0}, {"llinebelow", 0x1E3B}, {"lmiddletilde", 0x026B}, + {"logicaland", 0x2227}, {"logicalnot", 0x00AC}, {"logicalnotreversed", 0x2310}, {"logicalor", 0x2228}, + {"longs", 0x017F}, {"lowlinecmb", 0x0332}, {"lozenge", 0x25CA}, {"lparen", 0x24A7}, + {"lslash", 0x0142}, {"lsquare", 0x2113}, {"lsuperior", 0xF6EE}, {"ltshade", 0x2591}, + {"m", 0x006D}, {"macron", 0x00AF}, {"macronbelowcmb", 0x0331}, {"macroncmb", 0x0304}, + {"macronlowmod", 0x02CD}, {"macute", 0x1E3F}, {"maichattawalowleftthai", 0xF895}, {"maichattawalowrightthai", 0xF894}, + {"maichattawaupperleftthai", 0xF893}, {"maieklowleftthai", 0xF88C}, {"maieklowrightthai", 0xF88B}, {"maiekupperleftthai", 0xF88A}, + {"maihanakatleftthai", 0xF884}, {"maitaikhuleftthai", 0xF889}, {"maitholowleftthai", 0xF88F}, {"maitholowrightthai", 0xF88E}, + {"maithoupperleftthai", 0xF88D}, {"maitrilowleftthai", 0xF892}, {"maitrilowrightthai", 0xF891}, {"maitriupperleftthai", 0xF890}, + {"male", 0x2642}, {"mars", 0x2642}, {"mcircle", 0x24DC}, {"mdotaccent", 0x1E41}, + {"mdotbelow", 0x1E43}, {"mhook", 0x0271}, {"middot", 0x00B7}, {"minus", 0x2212}, + {"minusbelowcmb", 0x0320}, {"minuscircle", 0x2296}, {"minusmod", 0x02D7}, {"minusplus", 0x2213}, + {"minute", 0x2032}, {"mlonglegturned", 0x0270}, {"mparen", 0x24A8}, {"msuperior", 0xF6EF}, + {"mturned", 0x026F}, {"mu", 0x00B5}, {"mu1", 0x00B5}, {"muchgreater", 0x226B}, + {"muchless", 0x226A}, {"mugreek", 0x03BC}, {"multiply", 0x00D7}, {"musicalnote", 0x266A}, + {"musicalnotedbl", 0x266B}, {"musicflatsign", 0x266D}, {"musicsharpsign", 0x266F}, {"n", 0x006E}, + {"nabla", 0x2207}, {"nacute", 0x0144}, {"napostrophe", 0x0149}, {"nbspace", 0x00A0}, + {"ncaron", 0x0148}, {"ncedilla", 0x0146}, {"ncircle", 0x24DD}, {"ncircumflexbelow", 0x1E4B}, + {"ncommaaccent", 0x0146}, {"ndotaccent", 0x1E45}, {"ndotbelow", 0x1E47}, {"newsheqelsign", 0x20AA}, + {"nhookleft", 0x0272}, {"nhookretroflex", 0x0273}, {"nikhahitleftthai", 0xF899}, {"nine", 0x0039}, + {"ninecircle", 0x2468}, {"ninecircleinversesansserif", 0x2792}, {"nineinferior", 0x2089}, {"nineoldstyle", 0xF739}, + {"nineparen", 0x247C}, {"nineperiod", 0x2490}, {"nineroman", 0x2178}, {"ninesuperior", 0x2079}, + {"nineteencircle", 0x2472}, {"nineteenparen", 0x2486}, {"nineteenperiod", 0x249A}, {"nj", 0x01CC}, + {"njecyrillic", 0x045A}, {"nlegrightlong", 0x019E}, {"nlinebelow", 0x1E49}, {"nonbreakingspace", 0x00A0}, + {"notcontains", 0x220C}, {"notelement", 0x2209}, {"notelementof", 0x2209}, {"notequal", 0x2260}, + {"notgreater", 0x226F}, {"notgreaternorequal", 0x2271}, {"notgreaternorless", 0x2279}, {"notidentical", 0x2262}, + {"notless", 0x226E}, {"notlessnorequal", 0x2270}, {"notparallel", 0x2226}, {"notprecedes", 0x2280}, + {"notsubset", 0x2284}, {"notsucceeds", 0x2281}, {"notsuperset", 0x2285}, {"nparen", 0x24A9}, + {"nsuperior", 0x207F}, {"ntilde", 0x00F1}, {"nu", 0x03BD}, {"numbersign", 0x0023}, + {"numeralsigngreek", 0x0374}, {"numeralsignlowergreek", 0x0375}, {"numero", 0x2116}, {"o", 0x006F}, + {"oacute", 0x00F3}, {"obarred", 0x0275}, {"obarredcyrillic", 0x04E9}, {"obarreddieresiscyrillic", 0x04EB}, + {"obreve", 0x014F}, {"ocaron", 0x01D2}, {"ocircle", 0x24DE}, {"ocircumflex", 0x00F4}, + {"ocircumflexacute", 0x1ED1}, {"ocircumflexdotbelow", 0x1ED9}, {"ocircumflexgrave", 0x1ED3}, {"ocircumflexhookabove", 0x1ED5}, + {"ocircumflextilde", 0x1ED7}, {"ocyrillic", 0x043E}, {"odblacute", 0x0151}, {"odblgrave", 0x020D}, + {"odieresis", 0x00F6}, {"odieresiscyrillic", 0x04E7}, {"odotbelow", 0x1ECD}, {"oe", 0x0153}, + {"ogonek", 0x02DB}, {"ogonekcmb", 0x0328}, {"ograve", 0x00F2}, {"ohookabove", 0x1ECF}, + {"ohorn", 0x01A1}, {"ohornacute", 0x1EDB}, {"ohorndotbelow", 0x1EE3}, {"ohorngrave", 0x1EDD}, + {"ohornhookabove", 0x1EDF}, {"ohorntilde", 0x1EE1}, {"ohungarumlaut", 0x0151}, {"oi", 0x01A3}, + {"oinvertedbreve", 0x020F}, {"omacron", 0x014D}, {"omacronacute", 0x1E53}, {"omacrongrave", 0x1E51}, + {"omega", 0x03C9}, {"omega1", 0x03D6}, {"omegacyrillic", 0x0461}, {"omegalatinclosed", 0x0277}, + {"omegaroundcyrillic", 0x047B}, {"omegatitlocyrillic", 0x047D}, {"omegatonos", 0x03CE}, {"omicron", 0x03BF}, + {"omicrontonos", 0x03CC}, {"one", 0x0031}, {"onecircle", 0x2460}, {"onecircleinversesansserif", 0x278A}, + {"onedotenleader", 0x2024}, {"oneeighth", 0x215B}, {"onefitted", 0xF6DC}, {"onehalf", 0x00BD}, + {"oneinferior", 0x2081}, {"oneoldstyle", 0xF731}, {"oneparen", 0x2474}, {"oneperiod", 0x2488}, + {"onequarter", 0x00BC}, {"oneroman", 0x2170}, {"onesuperior", 0x00B9}, {"onethird", 0x2153}, + {"oogonek", 0x01EB}, {"oogonekmacron", 0x01ED}, {"oopen", 0x0254}, {"oparen", 0x24AA}, + {"openbullet", 0x25E6}, {"option", 0x2325}, {"ordfeminine", 0x00AA}, {"ordmasculine", 0x00BA}, + {"orthogonal", 0x221F}, {"oslash", 0x00F8}, {"oslashacute", 0x01FF}, {"ostrokeacute", 0x01FF}, + {"osuperior", 0xF6F0}, {"otcyrillic", 0x047F}, {"otilde", 0x00F5}, {"otildeacute", 0x1E4D}, + {"otildedieresis", 0x1E4F}, {"overline", 0x203E}, {"overlinecmb", 0x0305}, {"overscore", 0x00AF}, + {"p", 0x0070}, {"pacute", 0x1E55}, {"pagedown", 0x21DF}, {"pageup", 0x21DE}, + {"palatalizationcyrilliccmb", 0x0484}, {"palochkacyrillic", 0x04C0}, {"paragraph", 0x00B6}, {"parallel", 0x2225}, + {"parenleft", 0x0028}, {"parenleftbt", 0xF8ED}, {"parenleftex", 0xF8EC}, {"parenleftinferior", 0x208D}, + {"parenleftsuperior", 0x207D}, {"parenlefttp", 0xF8EB}, {"parenright", 0x0029}, {"parenrightbt", 0xF8F8}, + {"parenrightex", 0xF8F7}, {"parenrightinferior", 0x208E}, {"parenrightsuperior", 0x207E}, {"parenrighttp", 0xF8F6}, + {"partialdiff", 0x2202}, {"pcircle", 0x24DF}, {"pdotaccent", 0x1E57}, {"pecyrillic", 0x043F}, + {"pemiddlehookcyrillic", 0x04A7}, {"percent", 0x0025}, {"period", 0x002E}, {"periodcentered", 0x00B7}, + {"periodinferior", 0xF6E7}, {"periodsuperior", 0xF6E8}, {"perispomenigreekcmb", 0x0342}, {"perpendicular", 0x22A5}, + {"perthousand", 0x2030}, {"peseta", 0x20A7}, {"phi", 0x03C6}, {"phi1", 0x03D5}, + {"philatin", 0x0278}, {"phisymbolgreek", 0x03D5}, {"phook", 0x01A5}, {"pi", 0x03C0}, + {"pisymbolgreek", 0x03D6}, {"plus", 0x002B}, {"plusbelowcmb", 0x031F}, {"pluscircle", 0x2295}, + {"plusminus", 0x00B1}, {"plusmod", 0x02D6}, {"plussuperior", 0x207A}, {"pointingindexdownwhite", 0x261F}, + {"pointingindexleftwhite", 0x261C}, {"pointingindexrightwhite", 0x261E}, {"pointingindexupwhite", 0x261D}, {"pparen", 0x24AB}, + {"precedes", 0x227A}, {"prescription", 0x211E}, {"primemod", 0x02B9}, {"primereversed", 0x2035}, + {"product", 0x220F}, {"projective", 0x2305}, {"propellor", 0x2318}, {"propersubset", 0x2282}, + {"propersuperset", 0x2283}, {"proportion", 0x2237}, {"proportional", 0x221D}, {"psi", 0x03C8}, + {"psicyrillic", 0x0471}, {"psilipneumatacyrilliccmb", 0x0486}, {"q", 0x0071}, {"qcircle", 0x24E0}, + {"qhook", 0x02A0}, {"qparen", 0x24AC}, {"quarternote", 0x2669}, {"question", 0x003F}, + {"questiondown", 0x00BF}, {"questiondownsmall", 0xF7BF}, {"questiongreek", 0x037E}, {"questionsmall", 0xF73F}, + {"quotedbl", 0x0022}, {"quotedblbase", 0x201E}, {"quotedblleft", 0x201C}, {"quotedblright", 0x201D}, + {"quoteleft", 0x2018}, {"quoteleftreversed", 0x201B}, {"quotereversed", 0x201B}, {"quoteright", 0x2019}, + {"quoterightn", 0x0149}, {"quotesinglbase", 0x201A}, {"quotesingle", 0x0027}, {"r", 0x0072}, + {"racute", 0x0155}, {"radical", 0x221A}, {"radicalex", 0xF8E5}, {"ramshorn", 0x0264}, + {"ratio", 0x2236}, {"rcaron", 0x0159}, {"rcedilla", 0x0157}, {"rcircle", 0x24E1}, + {"rcommaaccent", 0x0157}, {"rdblgrave", 0x0211}, {"rdotaccent", 0x1E59}, {"rdotbelow", 0x1E5B}, + {"rdotbelowmacron", 0x1E5D}, {"referencemark", 0x203B}, {"reflexsubset", 0x2286}, {"reflexsuperset", 0x2287}, + {"registered", 0x00AE}, {"registersans", 0xF8E8}, {"registerserif", 0xF6DA}, {"reversedtilde", 0x223D}, + {"revlogicalnot", 0x2310}, {"rfishhook", 0x027E}, {"rfishhookreversed", 0x027F}, {"rho", 0x03C1}, + {"rhook", 0x027D}, {"rhookturned", 0x027B}, {"rhookturnedsuperior", 0x02B5}, {"rhosymbolgreek", 0x03F1}, + {"rhotichookmod", 0x02DE}, {"rightangle", 0x221F}, {"righttackbelowcmb", 0x0319}, {"righttriangle", 0x22BF}, + {"ring", 0x02DA}, {"ringbelowcmb", 0x0325}, {"ringcmb", 0x030A}, {"ringhalfleft", 0x02BF}, + {"ringhalfleftbelowcmb", 0x031C}, {"ringhalfleftcentered", 0x02D3}, {"ringhalfright", 0x02BE}, {"ringhalfrightbelowcmb", 0x0339}, + {"ringhalfrightcentered", 0x02D2}, {"rinvertedbreve", 0x0213}, {"rlinebelow", 0x1E5F}, {"rlongleg", 0x027C}, + {"rlonglegturned", 0x027A}, {"rparen", 0x24AD}, {"rsuperior", 0xF6F1}, {"rtblock", 0x2590}, + {"rturned", 0x0279}, {"rturnedsuperior", 0x02B4}, {"rupiah", 0xF6DD}, {"s", 0x0073}, + {"sacute", 0x015B}, {"sacutedotaccent", 0x1E65}, {"saraiileftthai", 0xF886}, {"saraileftthai", 0xF885}, + {"saraueeleftthai", 0xF888}, {"saraueleftthai", 0xF887}, {"scaron", 0x0161}, {"scarondotaccent", 0x1E67}, + {"scedilla", 0x015F}, {"schwa", 0x0259}, {"schwacyrillic", 0x04D9}, {"schwadieresiscyrillic", 0x04DB}, + {"schwahook", 0x025A}, {"scircle", 0x24E2}, {"scircumflex", 0x015D}, {"scommaaccent", 0x0219}, + {"sdotaccent", 0x1E61}, {"sdotbelow", 0x1E63}, {"sdotbelowdotaccent", 0x1E69}, {"seagullbelowcmb", 0x033C}, + {"second", 0x2033}, {"secondtonechinese", 0x02CA}, {"section", 0x00A7}, {"semicolon", 0x003B}, + {"seven", 0x0037}, {"sevencircle", 0x2466}, {"sevencircleinversesansserif", 0x2790}, {"seveneighths", 0x215E}, + {"seveninferior", 0x2087}, {"sevenoldstyle", 0xF737}, {"sevenparen", 0x247A}, {"sevenperiod", 0x248E}, + {"sevenroman", 0x2176}, {"sevensuperior", 0x2077}, {"seventeencircle", 0x2470}, {"seventeenparen", 0x2484}, + {"seventeenperiod", 0x2498}, {"sfthyphen", 0x00AD}, {"shacyrillic", 0x0448}, {"shade", 0x2592}, + {"shadedark", 0x2593}, {"shadelight", 0x2591}, {"shademedium", 0x2592}, {"shchacyrillic", 0x0449}, + {"sheicoptic", 0x03E3}, {"sheqel", 0x20AA}, {"sheqelhebrew", 0x20AA}, {"shhacyrillic", 0x04BB}, + {"shimacoptic", 0x03ED}, {"shook", 0x0282}, {"sigma", 0x03C3}, {"sigma1", 0x03C2}, + {"sigmafinal", 0x03C2}, {"sigmalunatesymbolgreek", 0x03F2}, {"similar", 0x223C}, {"six", 0x0036}, + {"sixcircle", 0x2465}, {"sixcircleinversesansserif", 0x278F}, {"sixinferior", 0x2086}, {"sixoldstyle", 0xF736}, + {"sixparen", 0x2479}, {"sixperiod", 0x248D}, {"sixroman", 0x2175}, {"sixsuperior", 0x2076}, + {"sixteencircle", 0x246F}, {"sixteenparen", 0x2483}, {"sixteenperiod", 0x2497}, {"slash", 0x002F}, + {"slong", 0x017F}, {"slongdotaccent", 0x1E9B}, {"smileface", 0x263A}, {"softhyphen", 0x00AD}, + {"softsigncyrillic", 0x044C}, {"soliduslongoverlaycmb", 0x0338}, {"solidusshortoverlaycmb", 0x0337}, {"space", 0x0020}, + {"spacehackarabic", 0x0020}, {"spade", 0x2660}, {"spadesuitblack", 0x2660}, {"spadesuitwhite", 0x2664}, + {"sparen", 0x24AE}, {"squarebelowcmb", 0x033B}, {"squarediagonalcrosshatchfill", 0x25A9}, {"squarehorizontalfill", 0x25A4}, + {"squareorthogonalcrosshatchfill", 0x25A6}, {"squareupperlefttolowerrightfill", 0x25A7}, {"squareupperrighttolowerleftfill", 0x25A8}, {"squareverticalfill", 0x25A5}, + {"squarewhitewithsmallblack", 0x25A3}, {"ssuperior", 0xF6F2}, {"sterling", 0x00A3}, {"strokelongoverlaycmb", 0x0336}, + {"strokeshortoverlaycmb", 0x0335}, {"subset", 0x2282}, {"subsetnotequal", 0x228A}, {"subsetorequal", 0x2286}, + {"succeeds", 0x227B}, {"suchthat", 0x220B}, {"summation", 0x2211}, {"sun", 0x263C}, + {"superset", 0x2283}, {"supersetnotequal", 0x228B}, {"supersetorequal", 0x2287}, {"t", 0x0074}, + {"tackdown", 0x22A4}, {"tackleft", 0x22A3}, {"tau", 0x03C4}, {"tbar", 0x0167}, + {"tcaron", 0x0165}, {"tccurl", 0x02A8}, {"tcedilla", 0x0163}, {"tcircle", 0x24E3}, + {"tcircumflexbelow", 0x1E71}, {"tcommaaccent", 0x0163}, {"tdieresis", 0x1E97}, {"tdotaccent", 0x1E6B}, + {"tdotbelow", 0x1E6D}, {"tecyrillic", 0x0442}, {"tedescendercyrillic", 0x04AD}, {"telephone", 0x2121}, + {"telephoneblack", 0x260E}, {"tencircle", 0x2469}, {"tenparen", 0x247D}, {"tenperiod", 0x2491}, + {"tenroman", 0x2179}, {"tesh", 0x02A7}, {"tetsecyrillic", 0x04B5}, {"thanthakhatlowleftthai", 0xF898}, + {"thanthakhatlowrightthai", 0xF897}, {"thanthakhatupperleftthai", 0xF896}, {"thereexists", 0x2203}, {"therefore", 0x2234}, + {"theta", 0x03B8}, {"theta1", 0x03D1}, {"thetasymbolgreek", 0x03D1}, {"thirteencircle", 0x246C}, + {"thirteenparen", 0x2480}, {"thirteenperiod", 0x2494}, {"thook", 0x01AD}, {"thorn", 0x00FE}, + {"thousandcyrillic", 0x0482}, {"three", 0x0033}, {"threecircle", 0x2462}, {"threecircleinversesansserif", 0x278C}, + {"threeeighths", 0x215C}, {"threeinferior", 0x2083}, {"threeoldstyle", 0xF733}, {"threeparen", 0x2476}, + {"threeperiod", 0x248A}, {"threequarters", 0x00BE}, {"threequartersemdash", 0xF6DE}, {"threeroman", 0x2172}, + {"threesuperior", 0x00B3}, {"tilde", 0x02DC}, {"tildebelowcmb", 0x0330}, {"tildecmb", 0x0303}, + {"tildecomb", 0x0303}, {"tildedoublecmb", 0x0360}, {"tildeoperator", 0x223C}, {"tildeoverlaycmb", 0x0334}, + {"tildeverticalcmb", 0x033E}, {"timescircle", 0x2297}, {"titlocyrilliccmb", 0x0483}, {"tlinebelow", 0x1E6F}, + {"tonebarextrahighmod", 0x02E5}, {"tonebarextralowmod", 0x02E9}, {"tonebarhighmod", 0x02E6}, {"tonebarlowmod", 0x02E8}, + {"tonebarmidmod", 0x02E7}, {"tonefive", 0x01BD}, {"tonesix", 0x0185}, {"tonetwo", 0x01A8}, + {"tonos", 0x0384}, {"tpalatalhook", 0x01AB}, {"tparen", 0x24AF}, {"trademark", 0x2122}, + {"trademarksans", 0xF8EA}, {"trademarkserif", 0xF6DB}, {"tretroflexhook", 0x0288}, {"triagdn", 0x25BC}, + {"triaglf", 0x25C4}, {"triagrt", 0x25BA}, {"triagup", 0x25B2}, {"ts", 0x02A6}, + {"tsecyrillic", 0x0446}, {"tshecyrillic", 0x045B}, {"tsuperior", 0xF6F3}, {"tturned", 0x0287}, + {"twelvecircle", 0x246B}, {"twelveparen", 0x247F}, {"twelveperiod", 0x2493}, {"twelveroman", 0x217B}, + {"twentycircle", 0x2473}, {"twentyparen", 0x2487}, {"twentyperiod", 0x249B}, {"two", 0x0032}, + {"twocircle", 0x2461}, {"twocircleinversesansserif", 0x278B}, {"twodotenleader", 0x2025}, {"twodotleader", 0x2025}, + {"twoinferior", 0x2082}, {"twooldstyle", 0xF732}, {"twoparen", 0x2475}, {"twoperiod", 0x2489}, + {"tworoman", 0x2171}, {"twostroke", 0x01BB}, {"twosuperior", 0x00B2}, {"twothirds", 0x2154}, + {"u", 0x0075}, {"uacute", 0x00FA}, {"ubar", 0x0289}, {"ubreve", 0x016D}, + {"ucaron", 0x01D4}, {"ucircle", 0x24E4}, {"ucircumflex", 0x00FB}, {"ucircumflexbelow", 0x1E77}, + {"ucyrillic", 0x0443}, {"udblacute", 0x0171}, {"udblgrave", 0x0215}, {"udieresis", 0x00FC}, + {"udieresisacute", 0x01D8}, {"udieresisbelow", 0x1E73}, {"udieresiscaron", 0x01DA}, {"udieresiscyrillic", 0x04F1}, + {"udieresisgrave", 0x01DC}, {"udieresismacron", 0x01D6}, {"udotbelow", 0x1EE5}, {"ugrave", 0x00F9}, + {"uhookabove", 0x1EE7}, {"uhorn", 0x01B0}, {"uhornacute", 0x1EE9}, {"uhorndotbelow", 0x1EF1}, + {"uhorngrave", 0x1EEB}, {"uhornhookabove", 0x1EED}, {"uhorntilde", 0x1EEF}, {"uhungarumlaut", 0x0171}, + {"uhungarumlautcyrillic", 0x04F3}, {"uinvertedbreve", 0x0217}, {"ukcyrillic", 0x0479}, {"umacron", 0x016B}, + {"umacroncyrillic", 0x04EF}, {"umacrondieresis", 0x1E7B}, {"underscore", 0x005F}, {"underscoredbl", 0x2017}, + {"union", 0x222A}, {"universal", 0x2200}, {"uogonek", 0x0173}, {"uparen", 0x24B0}, + {"upblock", 0x2580}, {"upsilon", 0x03C5}, {"upsilondieresis", 0x03CB}, {"upsilondieresistonos", 0x03B0}, + {"upsilonlatin", 0x028A}, {"upsilontonos", 0x03CD}, {"uptackbelowcmb", 0x031D}, {"uptackmod", 0x02D4}, + {"uring", 0x016F}, {"ushortcyrillic", 0x045E}, {"ustraightcyrillic", 0x04AF}, {"ustraightstrokecyrillic", 0x04B1}, + {"utilde", 0x0169}, {"utildeacute", 0x1E79}, {"utildebelow", 0x1E75}, {"v", 0x0076}, + {"vcircle", 0x24E5}, {"vdotbelow", 0x1E7F}, {"vecyrillic", 0x0432}, {"venus", 0x2640}, + {"verticalbar", 0x007C}, {"verticallineabovecmb", 0x030D}, {"verticallinebelowcmb", 0x0329}, {"verticallinelowmod", 0x02CC}, + {"verticallinemod", 0x02C8}, {"vhook", 0x028B}, {"vparen", 0x24B1}, {"vtilde", 0x1E7D}, + {"vturned", 0x028C}, {"w", 0x0077}, {"wacute", 0x1E83}, {"wcircle", 0x24E6}, + {"wcircumflex", 0x0175}, {"wdieresis", 0x1E85}, {"wdotaccent", 0x1E87}, {"wdotbelow", 0x1E89}, + {"weierstrass", 0x2118}, {"wgrave", 0x1E81}, {"whitebullet", 0x25E6}, {"whitecircle", 0x25CB}, + {"whitecircleinverse", 0x25D9}, {"whitediamond", 0x25C7}, {"whitediamondcontainingblacksmalldiamond", 0x25C8}, {"whitedownpointingsmalltriangle", 0x25BF}, + {"whitedownpointingtriangle", 0x25BD}, {"whiteleftpointingsmalltriangle", 0x25C3}, {"whiteleftpointingtriangle", 0x25C1}, {"whiterightpointingsmalltriangle", 0x25B9}, + {"whiterightpointingtriangle", 0x25B7}, {"whitesmallsquare", 0x25AB}, {"whitesmilingface", 0x263A}, {"whitesquare", 0x25A1}, + {"whitestar", 0x2606}, {"whitetelephone", 0x260F}, {"whiteuppointingsmalltriangle", 0x25B5}, {"whiteuppointingtriangle", 0x25B3}, + {"won", 0x20A9}, {"wparen", 0x24B2}, {"wring", 0x1E98}, {"wsuperior", 0x02B7}, + {"wturned", 0x028D}, {"wynn", 0x01BF}, {"x", 0x0078}, {"xabovecmb", 0x033D}, + {"xcircle", 0x24E7}, {"xdieresis", 0x1E8D}, {"xdotaccent", 0x1E8B}, {"xi", 0x03BE}, + {"xparen", 0x24B3}, {"xsuperior", 0x02E3}, {"y", 0x0079}, {"yacute", 0x00FD}, + {"yatcyrillic", 0x0463}, {"ycircle", 0x24E8}, {"ycircumflex", 0x0177}, {"ydieresis", 0x00FF}, + {"ydotaccent", 0x1E8F}, {"ydotbelow", 0x1EF5}, {"yen", 0x00A5}, {"yericyrillic", 0x044B}, + {"yerudieresiscyrillic", 0x04F9}, {"ygrave", 0x1EF3}, {"yhook", 0x01B4}, {"yhookabove", 0x1EF7}, + {"yicyrillic", 0x0457}, {"yinyang", 0x262F}, {"yodyodpatahhebrew", 0xFB1F}, {"yotgreek", 0x03F3}, + {"yparen", 0x24B4}, {"ypogegrammeni", 0x037A}, {"ypogegrammenigreekcmb", 0x0345}, {"yr", 0x01A6}, + {"yring", 0x1E99}, {"ysuperior", 0x02B8}, {"ytilde", 0x1EF9}, {"yturned", 0x028E}, + {"yusbigcyrillic", 0x046B}, {"yusbigiotifiedcyrillic", 0x046D}, {"yuslittlecyrillic", 0x0467}, {"yuslittleiotifiedcyrillic", 0x0469}, + {"z", 0x007A}, {"zacute", 0x017A}, {"zcaron", 0x017E}, {"zcircle", 0x24E9}, + {"zcircumflex", 0x1E91}, {"zcurl", 0x0291}, {"zdot", 0x017C}, {"zdotaccent", 0x017C}, + {"zdotbelow", 0x1E93}, {"zecyrillic", 0x0437}, {"zedescendercyrillic", 0x0499}, {"zedieresiscyrillic", 0x04DF}, + {"zero", 0x0030}, {"zeroinferior", 0x2080}, {"zerooldstyle", 0xF730}, {"zerosuperior", 0x2070}, + {"zerowidthnonjoiner", 0x200C}, {"zerowidthspace", 0x200B}, {"zeta", 0x03B6}, {"zhebrevecyrillic", 0x04C2}, + {"zhecyrillic", 0x0436}, {"zhedescendercyrillic", 0x0497}, {"zhedieresiscyrillic", 0x04DD}, {"zlinebelow", 0x1E95}, + {"zparen", 0x24B5}, {"zretroflexhook", 0x0290}, {"zstroke", 0x01B6}, +} + +// One preferred name per code point, as an index into AGL_TABLE, sorted by +// code point. Where several names share a rune the AGLFN name wins, then one +// used by a built-in encoding, then the shortest. +@(private, rodata) +AGL_BY_RUNE := [2179]u16{ + 1266, 1265, 1255, 1252, 1251, 1239, 1240, 1241, 1259, 1260, 1270, 1256, 1243, 1264, 1263, 1249, + 1244, 1245, 1246, 1247, 1261, 1268, 1254, 1242, 1250, 1267, 1253, 1257, 1258, 1262, 1269, 2135, + 1453, 1996, 1835, 1335, 1945, 1029, 2006, 1928, 1934, 1093, 1961, 1225, 1600, 1946, 2127, 2420, + 1885, 2255, 2201, 1496, 1483, 2115, 2084, 1387, 1799, 1220, 2083, 1709, 1432, 1552, 1991, 1098, + 0, 49, 60, 84, 116, 177, 186, 211, 231, 274, 279, 302, 322, 332, 351, 415, + 427, 430, 448, 516, 546, 600, 607, 616, 622, 644, 1145, 1102, 1149, 1089, 2302, 1546, + 664, 1101, 1169, 1284, 1364, 1464, 1512, 1564, 1604, 1656, 1665, 1691, 1736, 1783, 1839, 1920, + 1986, 2007, 2059, 2163, 2268, 2323, 2337, 2370, 2378, 2408, 1137, 1103, 1141, 1090, 1248, 1787, + 1455, 1188, 2150, 1279, 2386, 1161, 2082, 1321, 1271, 1906, 1560, 1725, 2097, 2024, 1737, 1309, + 1964, 2266, 2212, 882, 1773, 1926, 1947, 1186, 1898, 1907, 1561, 1896, 1891, 2209, 1992, 33, + 5, 16, 47, 27, 42, 1, 65, 139, 117, 123, 133, 254, 235, 240, 244, 169, + 348, 377, 354, 362, 411, 372, 1778, 405, 567, 547, 552, 558, 623, 532, 1528, 1020, + 866, 876, 1099, 890, 1046, 895, 1179, 1385, 1365, 1371, 1380, 1619, 1605, 1610, 1613, 1448, + 1833, 1862, 1840, 1847, 1914, 1856, 1327, 1909, 2287, 2269, 2274, 2279, 2379, 2199, 2383, 40, + 1028, 7, 867, 41, 1036, 61, 1170, 69, 1182, 71, 1185, 64, 1178, 88, 1301, 92, + 1305, 147, 1410, 119, 1367, 136, 1382, 156, 1423, 120, 1368, 195, 1522, 191, 1518, 198, + 1525, 193, 1520, 223, 1574, 219, 1570, 270, 1651, 260, 1626, 237, 1607, 263, 1642, 249, + 1344, 233, 1624, 276, 1659, 289, 1676, 1680, 305, 1692, 308, 1699, 307, 1698, 312, 1703, + 319, 1732, 334, 1785, 336, 1789, 335, 1788, 1786, 153, 1419, 390, 1873, 358, 1844, 387, + 1870, 352, 1859, 431, 2008, 433, 2014, 432, 2013, 489, 2060, 500, 2074, 495, 2068, 492, + 2066, 520, 2170, 519, 2168, 518, 2167, 597, 2320, 580, 2299, 549, 2271, 592, 2316, 576, + 2295, 583, 2306, 610, 2340, 627, 2382, 628, 645, 2409, 651, 2415, 646, 2410, 1728, 1162, + 55, 59, 1164, 538, 2230, 404, 81, 1202, 87, 99, 110, 1356, 1314, 161, 496, 157, + 181, 1494, 203, 189, 1599, 265, 269, 295, 1683, 1696, 1694, 330, 342, 1813, 360, 381, + 1864, 388, 1871, 422, 1958, 2399, 539, 2231, 165, 1442, 2233, 531, 2198, 540, 570, 2289, + 587, 604, 635, 2390, 663, 2434, 174, 176, 1462, 1463, 2265, 537, 2229, 1534, 2369, 1214, + 1215, 1213, 1216, 86, 112, 1359, 303, 316, 1719, 333, 344, 1811, 14, 874, 238, 1608, + 359, 1845, 550, 2272, 564, 2285, 559, 2280, 561, 2282, 563, 2284, 1451, 29, 892, 32, + 894, 3, 897, 210, 1559, 192, 1519, 288, 1675, 402, 1900, 403, 1901, 175, 1460, 1657, + 85, 111, 1357, 187, 1513, 43, 1047, 2, 896, 406, 1910, 26, 889, 37, 1023, 132, + 1379, 143, 1400, 243, 1612, 258, 1622, 371, 1855, 389, 1872, 436, 2017, 443, 2049, 557, + 2278, 578, 2297, 501, 2075, 1100, 1091, 1092, 1112, 1902, 1183, 1355, 1316, 1436, 2069, 2072, + 1424, 1426, 1428, 1427, 1661, 1532, 1558, 1515, 2011, 1596, 1582, 1581, 1649, 1646, 1723, 1697, + 1717, 1715, 1772, 1769, 1761, 1796, 1797, 1841, 1879, 1956, 2056, 2052, 2033, 2051, 2032, 2029, + 2030, 446, 2109, 1440, 1346, 1443, 1441, 2247, 2238, 2270, 2312, 2333, 2336, 2368, 2403, 2433, + 2413, 1459, 1461, 1535, 1538, 1536, 1276, 1113, 1425, 209, 1660, 1690, 1988, 1541, 1542, 1358, + 1315, 1360, 2243, 2185, 2169, 1595, 1583, 1664, 2057, 2034, 447, 2367, 2401, 1974, 1298, 1234, + 1011, 1019, 2046, 2043, 1537, 1539, 1064, 1065, 1066, 1063, 1209, 1174, 2332, 1480, 2081, 1508, + 2331, 1740, 1550, 886, 1224, 1223, 2048, 2045, 2315, 1352, 1965, 1766, 1153, 1340, 2040, 1860, + 2213, 1597, 2036, 1516, 2377, 1540, 2224, 2226, 2228, 2227, 2225, 1549, 885, 1211, 2216, 1739, + 1918, 1155, 1341, 1324, 1585, 2042, 1598, 1176, 2329, 1300, 1293, 1171, 1157, 1233, 1226, 1230, + 1227, 1547, 883, 1708, 2038, 1707, 1591, 2044, 2314, 1351, 1962, 1764, 1587, 1588, 1343, 1323, + 2041, 1187, 1861, 2330, 1159, 1289, 1175, 1210, 1154, 1156, 2214, 1738, 1729, 1296, 2219, 2152, + 2151, 2134, 2133, 2047, 1160, 2141, 2079, 2371, 2220, 1297, 1551, 887, 1950, 1687, 1318, 2398, + 2217, 1158, 1836, 1837, 2397, 1994, 2232, 1326, 39, 1035, 159, 168, 267, 400, 591, 398, + 1645, 38, 54, 188, 98, 158, 656, 167, 530, 264, 285, 306, 331, 350, 620, 399, + 423, 441, 511, 517, 584, 421, 80, 424, 395, 266, 588, 1027, 1431, 1447, 1647, 2311, + 1026, 1110, 1514, 1313, 1430, 2426, 1446, 2192, 1643, 1671, 1693, 1777, 1834, 2375, 1883, 1959, + 2031, 2111, 2110, 2166, 2309, 1954, 1201, 1983, 1876, 1644, 2310, 1884, 2313, 1882, 1111, 2193, + 585, 586, 589, 1955, 1877, 515, 104, 299, 491, 508, 2104, 180, 1468, 294, 1682, 227, + 1589, 190, 1517, 510, 2108, 96, 1310, 1672, 2035, 2113, 2395, 906, 934, 935, 131, 937, + 242, 939, 940, 941, 942, 943, 944, 945, 994, 25, 901, 902, 903, 904, 905, 907, + 908, 909, 910, 911, 912, 913, 914, 369, 916, 917, 918, 919, 555, 921, 922, 923, + 924, 925, 926, 927, 928, 929, 930, 931, 932, 888, 949, 950, 951, 952, 953, 955, + 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, + 972, 973, 974, 975, 976, 977, 978, 979, 980, 954, 982, 983, 984, 985, 986, 987, + 988, 989, 990, 991, 992, 993, 999, 394, 1878, 995, 1000, 144, 1401, 642, 2406, 643, + 2407, 640, 2404, 641, 2405, 300, 1689, 425, 1984, 996, 1001, 997, 1002, 273, 1655, 579, + 2298, 396, 1880, 397, 1881, 410, 1913, 298, 1686, 2200, 2222, 1924, 1287, 1985, 933, 981, + 201, 1530, 200, 1529, 659, 2429, 654, 2418, 283, 1669, 287, 1674, 286, 1673, 280, 1666, + 152, 1418, 154, 1420, 420, 1944, 216, 1565, 164, 1439, 527, 2178, 595, 2318, 596, 2319, + 217, 1566, 529, 2186, 76, 1197, 79, 1200, 509, 2107, 73, 1193, 75, 1196, 1925, 657, + 2427, 284, 1670, 155, 1421, 78, 1199, 9, 869, 28, 891, 36, 1022, 251, 1617, 497, + 1005, 498, 2071, 660, 2430, 655, 2419, 113, 1361, 261, 1627, 246, 1615, 373, 1857, 356, + 1842, 357, 1843, 581, 2300, 562, 2283, 577, 2296, 77, 1198, 633, 2388, 44, 1048, 51, + 1105, 52, 1106, 56, 1133, 66, 1180, 93, 1306, 94, 1307, 106, 1332, 89, 1302, 91, + 1304, 149, 1412, 148, 1411, 125, 1373, 172, 1450, 121, 1369, 179, 1467, 205, 1543, 225, + 1576, 226, 1577, 224, 1575, 221, 1572, 220, 1571, 271, 1652, 245, 1614, 281, 1667, 292, + 1679, 297, 1685, 314, 1705, 315, 1706, 318, 1722, 310, 1701, 325, 1741, 327, 1759, 328, + 1760, 340, 1793, 341, 1794, 346, 1814, 338, 1791, 412, 1915, 413, 1916, 392, 1875, 391, + 1874, 416, 1921, 418, 1942, 437, 2018, 438, 2019, 439, 2020, 444, 2050, 502, 2076, 503, + 2077, 490, 2061, 493, 2067, 504, 2078, 524, 2175, 525, 2176, 536, 2223, 522, 2172, 560, + 2281, 599, 2322, 553, 2275, 598, 2321, 582, 2301, 606, 2335, 602, 2325, 614, 2345, 608, + 2338, 611, 2341, 612, 2342, 613, 2343, 619, 2374, 618, 2373, 630, 2384, 649, 2412, 652, + 2416, 661, 2431, 1584, 2174, 2366, 2400, 1045, 2129, 31, 893, 35, 1021, 17, 877, 19, + 879, 20, 880, 22, 881, 18, 878, 8, 868, 11, 871, 12, 872, 13, 873, 10, + 870, 137, 1383, 141, 1386, 171, 1449, 124, 1372, 127, 1375, 128, 1376, 130, 1377, 126, + 1374, 256, 1620, 250, 1616, 375, 1858, 380, 1863, 363, 1848, 365, 1850, 366, 1851, 368, + 1852, 364, 1849, 382, 1865, 384, 1867, 385, 1868, 386, 1869, 383, 1866, 566, 2286, 569, + 2288, 571, 2290, 573, 2292, 574, 2293, 575, 2294, 572, 2291, 634, 2389, 631, 2385, 636, + 2391, 639, 2402, 1422, 2425, 1018, 1008, 1006, 1007, 1603, 1477, 1417, 1414, 898, 1299, 2303, + 2000, 2003, 2005, 2002, 1998, 1999, 1997, 1285, 1286, 1165, 1888, 2258, 1408, 1015, 1016, 1017, + 1952, 1768, 2080, 1975, 1562, 1563, 2021, 1454, 1917, 1095, 1510, 2423, 1504, 1492, 2123, 2093, + 1399, 1807, 1966, 1433, 1932, 1938, 1832, 2421, 1892, 2260, 2205, 1499, 1487, 2118, 2088, 1394, + 1802, 1931, 1937, 1221, 1275, 1511, 1718, 1953, 2364, 2105, 1339, 173, 1189, 1173, 1465, 253, + 1733, 1838, 2344, 440, 1973, 2179, 2235, 393, 1034, 1444, 1024, 1899, 2267, 1889, 2204, 1486, + 2087, 401, 545, 534, 184, 183, 512, 505, 142, 343, 528, 146, 544, 1897, 2264, 2211, + 1503, 1491, 2122, 2092, 1398, 1806, 2184, 1407, 2251, 1068, 1080, 1073, 1059, 1049, 1081, 1084, + 1086, 1061, 1060, 1082, 1177, 1568, 1569, 1076, 1085, 1071, 1070, 1074, 1056, 1058, 1057, 1055, + 1054, 1923, 1922, 1051, 1053, 1052, 1050, 1078, 1079, 1072, 1087, 1077, 1062, 1172, 2305, 1940, + 1458, 1415, 97, 1545, 1403, 1817, 2157, 1816, 1976, 2158, 1763, 1767, 1329, 1094, 1167, 2009, + 1982, 1630, 1908, 1031, 1328, 1927, 1826, 1724, 1727, 1637, 2304, 1631, 1294, 1237, 2191, 1108, + 2012, 1981, 2114, 2027, 1097, 1236, 1041, 1025, 1040, 1527, 1042, 1628, 1819, 1434, 1823, 1710, + 1553, 1714, 1557, 1776, 1775, 1824, 1820, 1825, 1821, 1712, 1555, 1713, 1556, 1822, 1972, 2156, + 1827, 1829, 1979, 1980, 1828, 1830, 2022, 2023, 2154, 2161, 1206, 1765, 1204, 1205, 2165, 2164, + 1951, 2039, 1347, 1278, 1277, 1711, 1554, 1409, 1593, 1238, 1977, 2028, 1044, 1978, 1636, 1633, + 1905, 1312, 1212, 1032, 1033, 1311, 1132, 685, 686, 687, 688, 689, 690, 691, 692, 693, + 694, 1404, 2248, 2195, 1505, 1474, 2124, 2094, 1390, 1808, 2252, 1894, 2262, 2207, 1501, 1489, + 2120, 2090, 1396, 1804, 2182, 1405, 2249, 2196, 1506, 1475, 2125, 2095, 1391, 1809, 2253, 1895, + 2263, 2208, 1502, 1490, 2121, 2091, 1397, 1805, 2183, 1406, 2250, 2197, 1507, 1476, 2126, 2096, + 1392, 1810, 2254, 1037, 1135, 1274, 1353, 1429, 1509, 1544, 1594, 1648, 1663, 1688, 1731, 1770, + 1831, 1903, 1971, 1989, 2053, 2140, 2234, 2307, 2334, 2365, 2376, 2396, 2432, 15, 50, 68, + 90, 122, 178, 194, 222, 239, 275, 290, 309, 326, 337, 361, 417, 428, 434, 499, + 521, 551, 601, 609, 617, 626, 648, 875, 1104, 1181, 1303, 1370, 1466, 1521, 1573, 1609, + 1658, 1677, 1700, 1758, 1790, 1846, 1941, 1987, 2015, 2073, 2171, 2273, 2324, 2339, 2372, 2381, + 2411, 458, 459, 449, 451, 450, 452, 456, 457, 454, 455, 453, 477, 465, 485, 486, + 473, 463, 462, 466, 484, 483, 472, 469, 468, 467, 470, 471, 476, 460, 461, 464, + 481, 482, 475, 479, 480, 474, 488, 487, 478, 2308, 1334, 1134, 1716, 2055, 1735, 2099, + 1331, 1478, 215, 2148, 2143, 2147, 2144, 2145, 2146, 2142, 213, 214, 1479, 2242, 2363, 1130, + 2362, 1123, 2356, 2355, 2241, 2239, 2352, 2351, 1118, 2354, 2353, 2240, 843, 2349, 2350, 1481, + 1730, 1203, 1348, 1168, 212, 1207, 1208, 846, 1638, 1639, 1120, 1119, 1128, 1129, 1904, 1695, + 796, 2360, 801, 2361, 677, 1968, 1970, 684, 1967, 2393, 2130, 1640, 2159, 1469, 1366, 1756, + 2137, 1580, 1320, 1217, 2139, 1578, 1319, 1219, 1592, 1990, 1779, 1780, 1107, 1781, 1782, 665, + 772, 776, 790, 812, 683, 682, 681, 695, 706, 717, 728, 672, 739, 750, 761, 773, + 781, 782, 783, 784, 785, 786, 787, 788, 823, 834, 845, 855, 666, 789, 791, 792, + 793, 794, 795, 797, 798, 799, 800, 802, 803, 804, 805, 806, 807, 808, 809, 810, + 811, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 824, 825, 826, 827, 828, + 829, 830, 831, 832, 833, 835, 837, 839, 777, 840, 778, 844, 847, 848, 849, 863, + 864, 865, 667, 668, 669, 670, 671, 673, 674, 675, 854, 856, 859, 860, 857, 858, + 779, 850, 780, 851, 852, 853, 861, 862, 696, 697, 698, 699, 700, 701, 702, 703, + 704, 705, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 718, 719, 720, 721, + 722, 723, 724, 725, 726, 727, 729, 768, 734, 764, 735, 736, 737, 738, 740, 741, + 742, 743, 731, 744, 745, 746, 747, 748, 749, 765, 751, 771, 752, 774, 753, 775, + 754, 755, 769, 756, 766, 770, 757, 767, 758, 759, 760, 762, 763, 1345, 304, 1721, + 1228, 946, 947, 998, 1003, 1004, 23, 62, 100, 101, 102, 206, 229, 323, 1280, 1281, + 1288, 1282, 1283, 1292, 1322, 1325, 1273, 2026, 2237, 1890, 2058, 2210, 1190, 1192, 1229, 1232, + 1336, 1338, 1601, 1602, 1948, 1949, 1096, 1163, 1354, 1445, 1650, 1734, 1771, 1912, 2054, 2149, + 2246, 57, 63, 82, 107, 230, 320, 353, 376, 442, 494, 535, 647, 1457, 1337, 1030, + 2422, 1893, 2261, 2206, 1500, 1488, 2119, 2089, 1395, 1803, 1995, 207, 46, 58, 83, 109, + 166, 185, 208, 228, 268, 278, 301, 321, 329, 347, 408, 426, 429, 445, 514, 543, + 594, 605, 615, 621, 638, 662, 1456, 1191, 103, 324, 24, 72, 1993, 34, 6, 21, + 48, 30, 45, 4, 67, 140, 118, 129, 134, 255, 236, 241, 247, 170, 349, 378, + 355, 367, 414, 374, 407, 568, 548, 554, 565, 624, 533, 629, 1748, 2063, 2062, 2065, + 2064, 1749, 1747, 1746, 1745, 1752, 1751, 1750, 1755, 1754, 1753, 1744, 1743, 1742, 2189, 2188, + 2187, 1798, 2010, 1088, 1067, 2025, 1272, 2236, 1933, 1930, 1929, 1148, 1147, 1146, 1140, 1139, + 1138, 1136, 1634, 1939, 1936, 1935, 1152, 1151, 1150, 1144, 1143, 1142, 1039, 1470, 1473, 1493, + 1471, 1472, 1010, +} + +// --------------------------------------------------------------------------- +// Name matching +// --------------------------------------------------------------------------- + +// std_strip_subset removes the "ABCDEF+" tag a subsetter puts in front of a +// /BaseFont name (ISO 32000-2, 9.9.2). The format says six uppercase letters, +// but producers emit four, and lowercase, often enough that the check here is +// "letters then a plus, within the first eight bytes" instead. A real font name +// with a '+' that early is not a thing, so nothing legitimate is eaten. +@(private) +std_strip_subset :: proc(name: string) -> string { + limit := min(len(name), 8) + for i in 0 ..< limit { + c := name[i] + if c == '+' { + return i > 0 ? name[i + 1:] : name + } + if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + break + } + } + return name +} + +// std_normalize lowercases `name` into `buf` and drops everything that is not a +// letter or a digit, so that "Arial-BoldMT", "Arial,Bold" and "ArialBold" all +// compare equal. Returns the prefix of buf that was filled; a name longer than +// buf is truncated, which only loses a style suffix on a name no producer +// writes. +@(private) +std_normalize :: proc(name: string, buf: []u8) -> string { + n := 0 + for i in 0 ..< len(name) { + if n >= len(buf) { + break + } + c := name[i] + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') { + buf[n] = c + n += 1 + } + } + return string(buf[:n]) +} + +// std_contains is `strings.contains` without the import; it runs over a name +// that came out of a file, so it is written to be obviously bounded. +@(private) +std_contains :: proc(hay, needle: string) -> bool { + if len(needle) == 0 { + return true + } + if len(needle) > len(hay) { + return false + } + for i in 0 ..= len(hay) - len(needle) { + if hay[i:i + len(needle)] == needle { + return true + } + } + return false +} + +// std_font_lookup matches a /BaseFont name against the standard 14 and returns +// the index of the metrics to use. +// +// Matching is deliberately loose in one direction and strict in the other: any +// spelling of a standard font resolves (subset tag stripped, case and +// punctuation ignored, the Monotype and URW aliases folded in, style read out of +// the name), but a font that is not one of the fourteen returns false rather +// than being silently substituted. Choosing a substitute for an arbitrary +// missing font is a decision about descriptor flags, and it belongs in +// font.odin, not in a metrics table. +@(private) +std_font_lookup :: proc(base_font: string) -> (index: int, ok: bool) { + buf: [128]u8 + n := std_normalize(std_strip_subset(base_font), buf[:]) + if len(n) == 0 { + return 0, false + } + + // The two symbolic fonts have no styles: "Symbol,Bold" is still Symbol. + if std_contains(n, "zapfdingbat") || std_contains(n, "dingbats") { + return STD_ZAPF_DINGBATS, true + } + if std_contains(n, "symbol") { + return STD_SYMBOL, true + } + + family := -1 + switch { + case std_contains(n, "courier") || std_contains(n, "nimbusmon") || std_contains(n, "monospac"): + family = STD_COURIER + case std_contains(n, "helvetica") || std_contains(n, "arial") || std_contains(n, "nimbussan"): + family = STD_HELVETICA + case std_contains(n, "times") || std_contains(n, "nimbusrom"): + family = STD_TIMES_ROMAN + } + if family < 0 { + return 0, false + } + + style := 0 + // "semibold" and "extrabold" both contain "bold"; "black" and "heavy" are + // the other names producers give the bold face of these families. + if std_contains(n, "bold") || std_contains(n, "black") || std_contains(n, "heavy") { + style += 1 + } + if std_contains(n, "italic") || std_contains(n, "oblique") { + style += 2 + } + return family + style, true +} + +// std_font_name is the canonical PostScript name of a standard font, or "" for +// an index that is not one. +@(private) +std_font_name :: proc(index: int) -> string { + if index < 0 || index >= STD_FONT_COUNT { + return "" + } + return STD_FONT_NAMES[index] +} + +// std_font_is_symbolic reports whether the font uses a built-in, font-specific +// encoding rather than a Latin text encoding -- true only for Symbol and +// ZapfDingbats, whose descriptors set the Symbolic flag (ISO 32000-2, 9.8.2). +@(private) +std_font_is_symbolic :: proc(index: int) -> bool { + return index == STD_SYMBOL || index == STD_ZAPF_DINGBATS +} + +// std_font_builtin_encoding is the encoding a standard font uses when the font +// dictionary gives none. The twelve text fonts are AdobeStandardEncoding, which +// is what their AFMs declare; Symbol and ZapfDingbats each have their own. +// Returns nil for an index that is not a standard font. +@(private) +std_font_builtin_encoding :: proc(index: int) -> ^[256]string { + switch { + case index == STD_SYMBOL: + return &ENC_SYMBOL + case index == STD_ZAPF_DINGBATS: + return &ENC_ZAPF + case index >= 0 && index < STD_FONT_COUNT: + return &ENC_STANDARD + } + return nil +} + +// --------------------------------------------------------------------------- +// Widths +// --------------------------------------------------------------------------- + +// std_glyph_search is a binary search over a sorted glyph-name table. Odin +// compares strings byte-wise, which is the order the tables were generated in. +@(private) +std_glyph_search :: proc(names: []string, name: string) -> (int, bool) { + lo, hi := 0, len(names) + for lo < hi { + mid := lo + (hi - lo) / 2 + if names[mid] < name { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(names) && names[lo] == name { + return lo, true + } + return 0, false +} + +// std_font_width is the advance of one glyph of a standard font, in glyph space +// (1/1000 of the text space unit; all fourteen are 1000 units per em). +// +// ok is false when the font has no such glyph, which is the caller's cue to fall +// back to the descriptor's /MissingWidth. Courier is the exception: it is +// fixed-pitch, so 600 is the answer for any name at all, including one the font +// cannot draw -- a viewer still advances the pen by one cell. +@(private) +std_font_width :: proc(index: int, glyph_name: string) -> (width: f64, ok: bool) { + if len(glyph_name) == 0 { + return 0, false + } + switch { + case index >= STD_COURIER && index <= STD_COURIER_BOLD_OBLIQUE: + return 600, true + case index >= STD_HELVETICA && index <= STD_TIMES_BOLD_ITALIC: + i := std_glyph_search(STD_TEXT_GLYPHS[:], glyph_name) or_return + return f64(STD_TEXT_WIDTHS[index - STD_HELVETICA][i]), true + case index == STD_SYMBOL: + i := std_glyph_search(STD_SYMBOL_GLYPHS[:], glyph_name) or_return + return f64(STD_SYMBOL_WIDTHS[i]), true + case index == STD_ZAPF_DINGBATS: + i := std_glyph_search(STD_ZAPF_GLYPHS[:], glyph_name) or_return + return f64(STD_ZAPF_WIDTHS[i]), true + } + return 0, false +} + +// --------------------------------------------------------------------------- +// The built-in encodings +// --------------------------------------------------------------------------- +// +// Each is 256 glyph names, "" where the encoding leaves the code undefined. The +// tables are read-only and live for the life of the program, so handing back a +// pointer costs nothing and lets a font apply /Differences over a copy. + +@(private) +encoding_standard :: proc() -> ^[256]string { return &ENC_STANDARD } +@(private) +encoding_winansi :: proc() -> ^[256]string { return &ENC_WINANSI } +@(private) +encoding_macroman :: proc() -> ^[256]string { return &ENC_MACROMAN } +@(private) +encoding_macexpert :: proc() -> ^[256]string { return &ENC_MACEXPERT } +@(private) +encoding_symbol :: proc() -> ^[256]string { return &ENC_SYMBOL } +@(private) +encoding_zapf :: proc() -> ^[256]string { return &ENC_ZAPF } + +// encoding_macroman_code is ENC_MACROMAN read the other way round: the code a +// glyph name sits at in MacRomanEncoding, if it sits at one. +// +// ISO 32000-2, 9.6.5.4 needs exactly this to reach a non-symbolic TrueType +// font's (1,0) cmap, which is keyed by Mac Roman codes and not by the codes +// the PDF's own /Encoding uses. Without it a font whose only cmap is (1,0) +// resolves nothing above ASCII. +// +// A linear scan rather than a second sorted table: it runs only for a code the +// other routes could not resolve, and a reverse table would be one more thing +// to keep in step with the forward one for no measurable gain. "space" is the +// one name the encoding repeats (0x20 and 0xCA); the lower code is the one +// meant, and scanning upwards is what returns it. +@(private) +encoding_macroman_code :: proc(name: string) -> (u8, bool) { + if name == "" { + return 0, false + } + for nm, i in ENC_MACROMAN { + if nm == name { + return u8(i), true + } + } + return 0, false +} + +// encoding_by_name resolves an /Encoding or /BaseEncoding name. Returns nil for +// anything else, which the caller must read as "keep the font's built-in +// encoding" -- an unknown name is not a reason to reset a font to Standard. +// The bare forms are accepted because they appear in the wild. +@(private) +encoding_by_name :: proc(name: string) -> ^[256]string { + switch name { + case "WinAnsiEncoding", "WinAnsi": + return &ENC_WINANSI + case "MacRomanEncoding", "MacRoman": + return &ENC_MACROMAN + case "MacExpertEncoding", "MacExpert": + return &ENC_MACEXPERT + case "StandardEncoding", "Standard": + return &ENC_STANDARD + case "SymbolEncoding", "Symbol": + return &ENC_SYMBOL + case "ZapfDingbatsEncoding", "ZapfDingbats": + return &ENC_ZAPF + } + return nil +} + +// --------------------------------------------------------------------------- +// Glyph names and Unicode +// --------------------------------------------------------------------------- + +// agl_search is a binary search over AGL_TABLE, which is sorted by name. +@(private) +agl_search :: proc(name: string) -> (rune, bool) { + lo, hi := 0, len(AGL_TABLE) + for lo < hi { + mid := lo + (hi - lo) / 2 + if AGL_TABLE[mid].name < name { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(AGL_TABLE) && AGL_TABLE[lo].name == name { + return AGL_TABLE[lo].cp, true + } + return -1, false +} + +// agl_hex_prefix reads exactly `n` hex digits from the front of s. The AGL says +// uppercase, but "uni00e9" is common enough in real fonts that rejecting it +// would lose characters for no gain. +@(private) +agl_hex_prefix :: proc(s: string, n: int) -> (cp: rune, ok: bool) { + if len(s) < n { + return -1, false + } + v := 0 + for i in 0 ..< n { + // hex_value is object.odin's, and is the package's one hex digit + // decoder: a second copy here is a second place to get it wrong. + d, valid := hex_value(s[i]) + if !valid { + return -1, false + } + v = v * 16 + int(d) + } + return rune(v), true +} + +// agl_valid_rune rejects what cannot be a character: surrogate halves (which a +// glyph name is explicitly not allowed to encode) and anything past the end of +// the code space. +@(private) +agl_valid_rune :: proc(r: rune) -> bool { + return r >= 0 && r <= 0x10FFFF && !(r >= 0xD800 && r <= 0xDFFF) +} + +// agl_is_index_name reports whether a name is one of the "this is glyph number +// N" spellings -- gNN, cidNN, indexNN, glyphNN -- that subsetters and broken +// producers emit. They carry no Unicode at all, and reading the digits as a +// character would put arbitrary text into a document that has none. +// +// No AGL name has this shape, so today the fallthrough below would answer -1 +// anyway; the rule is written out so that it survives a change to the table, and +// the test asserts the table still has no such name. +@(private) +agl_is_index_name :: proc(name: string) -> bool { + prefixes := [?]string{"g", "cid", "index", "glyph"} + for p in prefixes { + if len(name) > len(p) && name[:len(p)] == p { + all_digits := true + for i in len(p) ..< len(name) { + if name[i] < '0' || name[i] > '9' { + all_digits = false + break + } + } + if all_digits { + return true + } + } + } + return false +} + +// glyph_name_to_rune maps a glyph name to Unicode by the Adobe Glyph List +// algorithm ("Unicode and Glyph Names", Adobe technical note), returning -1 when +// the name carries no character. +// +// The algorithm, in order: +// +// 1. drop everything from the first '.', so "a.sc" is "a" and ".notdef" is +// nothing at all; +// 2. split on '_' and take the first component -- "f_i" is a ligature glyph, +// and one rune has to stand for it; +// 3. look the component up in the AGL; +// 4. "uniXXXX" with a multiple of four hex digits, first code point wins; +// 5. "uXXXX" through "uXXXXXX"; +// 6. otherwise nothing. +// +// The AGL is consulted before the uni/u forms because a legitimate name can look +// like one: "union" starts with "uni" and would otherwise be searched for hex. +@(private) +glyph_name_to_rune :: proc(name: string) -> rune { + s := name + for i in 0 ..< len(s) { + if s[i] == '.' { + s = s[:i] + break + } + } + for i in 0 ..< len(s) { + if s[i] == '_' { + s = s[:i] + break + } + } + if len(s) == 0 { + return -1 + } + + if r, ok := agl_search(s); ok { + return r + } + if agl_is_index_name(s) { + return -1 + } + + if len(s) > 3 && s[:3] == "uni" { + // Four hex digits per code point, and every group has to be whole and in + // range for the name to mean anything -- "uni41" and "uni0041D800" are + // both nothing. Only the first code point survives as a rune. + rest := s[3:] + first := rune(-1) + for i := 0; i < len(rest); i += 4 { + r, ok := agl_hex_prefix(rest[i:], 4) + if !ok || !agl_valid_rune(r) { + return -1 + } + if i == 0 { + first = r + } + } + return first + } + if len(s) >= 5 && len(s) <= 7 && s[0] == 'u' { + if r, ok := agl_hex_prefix(s[1:], len(s) - 1); ok && agl_valid_rune(r) { + return r + } + } + return -1 +} + +// rune_to_glyph_name is the other direction: the name a font is most likely to +// use for a character, or "" when the AGL has none. Where several names share a +// code point the one Adobe's AGLFN list prefers wins, so the ASCII range comes +// back as the plain names ("A", "space", "period") and round-trips through +// glyph_name_to_rune. +@(private) +rune_to_glyph_name :: proc(r: rune) -> string { + lo, hi := 0, len(AGL_BY_RUNE) + for lo < hi { + mid := lo + (hi - lo) / 2 + if AGL_TABLE[AGL_BY_RUNE[mid]].cp < r { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(AGL_BY_RUNE) { + e := AGL_TABLE[AGL_BY_RUNE[lo]] + if e.cp == r { + return e.name + } + } + return "" +} diff --git a/font_standard_test.odin b/font_standard_test.odin new file mode 100644 index 0000000..c6d200f --- /dev/null +++ b/font_standard_test.odin @@ -0,0 +1,845 @@ +package pdf + +import "core:testing" + +// Every width asserted here was read out of the Adobe Core 14 AFM file named in +// the comment above it, not remembered. A width that is wrong by one unit is +// invisible on the first glyph of a line and unmissable by the last, so these +// are spot checks against the source of truth rather than against this file's +// own tables. + +@(test) +test_std_font_widths_match_afm :: proc(t: ^testing.T) { + cases := [?]struct { + font: int, + glyph: string, + width: f64, + }{ + // Courier.afm + {STD_COURIER, "space", 600}, {STD_COURIER, "A", 600}, {STD_COURIER, "W", 600}, {STD_COURIER, "a", 600}, + {STD_COURIER, "m", 600}, {STD_COURIER, "one", 600}, {STD_COURIER, "comma", 600}, {STD_COURIER, "period", 600}, + {STD_COURIER, "quoteright", 600}, {STD_COURIER, "quotesingle", 600}, {STD_COURIER, "fraction", 600}, {STD_COURIER, "germandbls", 600}, + {STD_COURIER, "Euro", 600}, {STD_COURIER, "ydieresis", 600}, + // Courier-Bold.afm + {STD_COURIER_BOLD, "space", 600}, {STD_COURIER_BOLD, "A", 600}, {STD_COURIER_BOLD, "W", 600}, {STD_COURIER_BOLD, "a", 600}, + {STD_COURIER_BOLD, "m", 600}, {STD_COURIER_BOLD, "one", 600}, {STD_COURIER_BOLD, "comma", 600}, {STD_COURIER_BOLD, "period", 600}, + {STD_COURIER_BOLD, "quoteright", 600}, {STD_COURIER_BOLD, "quotesingle", 600}, {STD_COURIER_BOLD, "fraction", 600}, {STD_COURIER_BOLD, "germandbls", 600}, + {STD_COURIER_BOLD, "Euro", 600}, {STD_COURIER_BOLD, "ydieresis", 600}, + // Courier-Oblique.afm + {STD_COURIER_OBLIQUE, "space", 600}, {STD_COURIER_OBLIQUE, "A", 600}, {STD_COURIER_OBLIQUE, "W", 600}, {STD_COURIER_OBLIQUE, "a", 600}, + {STD_COURIER_OBLIQUE, "m", 600}, {STD_COURIER_OBLIQUE, "one", 600}, {STD_COURIER_OBLIQUE, "comma", 600}, {STD_COURIER_OBLIQUE, "period", 600}, + {STD_COURIER_OBLIQUE, "quoteright", 600}, {STD_COURIER_OBLIQUE, "quotesingle", 600}, {STD_COURIER_OBLIQUE, "fraction", 600}, {STD_COURIER_OBLIQUE, "germandbls", 600}, + {STD_COURIER_OBLIQUE, "Euro", 600}, {STD_COURIER_OBLIQUE, "ydieresis", 600}, + // Courier-BoldOblique.afm + {STD_COURIER_BOLD_OBLIQUE, "space", 600}, {STD_COURIER_BOLD_OBLIQUE, "A", 600}, {STD_COURIER_BOLD_OBLIQUE, "W", 600}, {STD_COURIER_BOLD_OBLIQUE, "a", 600}, + {STD_COURIER_BOLD_OBLIQUE, "m", 600}, {STD_COURIER_BOLD_OBLIQUE, "one", 600}, {STD_COURIER_BOLD_OBLIQUE, "comma", 600}, {STD_COURIER_BOLD_OBLIQUE, "period", 600}, + {STD_COURIER_BOLD_OBLIQUE, "quoteright", 600}, {STD_COURIER_BOLD_OBLIQUE, "quotesingle", 600}, {STD_COURIER_BOLD_OBLIQUE, "fraction", 600}, {STD_COURIER_BOLD_OBLIQUE, "germandbls", 600}, + {STD_COURIER_BOLD_OBLIQUE, "Euro", 600}, {STD_COURIER_BOLD_OBLIQUE, "ydieresis", 600}, + // Helvetica.afm + {STD_HELVETICA, "space", 278}, {STD_HELVETICA, "A", 667}, {STD_HELVETICA, "W", 944}, {STD_HELVETICA, "a", 556}, + {STD_HELVETICA, "m", 833}, {STD_HELVETICA, "one", 556}, {STD_HELVETICA, "comma", 278}, {STD_HELVETICA, "period", 278}, + {STD_HELVETICA, "quoteright", 222}, {STD_HELVETICA, "quotesingle", 191}, {STD_HELVETICA, "fraction", 167}, {STD_HELVETICA, "germandbls", 611}, + {STD_HELVETICA, "Euro", 556}, {STD_HELVETICA, "ydieresis", 500}, + // Helvetica-Bold.afm + {STD_HELVETICA_BOLD, "space", 278}, {STD_HELVETICA_BOLD, "A", 722}, {STD_HELVETICA_BOLD, "W", 944}, {STD_HELVETICA_BOLD, "a", 556}, + {STD_HELVETICA_BOLD, "m", 889}, {STD_HELVETICA_BOLD, "one", 556}, {STD_HELVETICA_BOLD, "comma", 278}, {STD_HELVETICA_BOLD, "period", 278}, + {STD_HELVETICA_BOLD, "quoteright", 278}, {STD_HELVETICA_BOLD, "quotesingle", 238}, {STD_HELVETICA_BOLD, "fraction", 167}, {STD_HELVETICA_BOLD, "germandbls", 611}, + {STD_HELVETICA_BOLD, "Euro", 556}, {STD_HELVETICA_BOLD, "ydieresis", 556}, + // Helvetica-Oblique.afm + {STD_HELVETICA_OBLIQUE, "space", 278}, {STD_HELVETICA_OBLIQUE, "A", 667}, {STD_HELVETICA_OBLIQUE, "W", 944}, {STD_HELVETICA_OBLIQUE, "a", 556}, + {STD_HELVETICA_OBLIQUE, "m", 833}, {STD_HELVETICA_OBLIQUE, "one", 556}, {STD_HELVETICA_OBLIQUE, "comma", 278}, {STD_HELVETICA_OBLIQUE, "period", 278}, + {STD_HELVETICA_OBLIQUE, "quoteright", 222}, {STD_HELVETICA_OBLIQUE, "quotesingle", 191}, {STD_HELVETICA_OBLIQUE, "fraction", 167}, {STD_HELVETICA_OBLIQUE, "germandbls", 611}, + {STD_HELVETICA_OBLIQUE, "Euro", 556}, {STD_HELVETICA_OBLIQUE, "ydieresis", 500}, + // Helvetica-BoldOblique.afm + {STD_HELVETICA_BOLD_OBLIQUE, "space", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "A", 722}, {STD_HELVETICA_BOLD_OBLIQUE, "W", 944}, {STD_HELVETICA_BOLD_OBLIQUE, "a", 556}, + {STD_HELVETICA_BOLD_OBLIQUE, "m", 889}, {STD_HELVETICA_BOLD_OBLIQUE, "one", 556}, {STD_HELVETICA_BOLD_OBLIQUE, "comma", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "period", 278}, + {STD_HELVETICA_BOLD_OBLIQUE, "quoteright", 278}, {STD_HELVETICA_BOLD_OBLIQUE, "quotesingle", 238}, {STD_HELVETICA_BOLD_OBLIQUE, "fraction", 167}, {STD_HELVETICA_BOLD_OBLIQUE, "germandbls", 611}, + {STD_HELVETICA_BOLD_OBLIQUE, "Euro", 556}, {STD_HELVETICA_BOLD_OBLIQUE, "ydieresis", 556}, + // Times-Roman.afm + {STD_TIMES_ROMAN, "space", 250}, {STD_TIMES_ROMAN, "A", 722}, {STD_TIMES_ROMAN, "W", 944}, {STD_TIMES_ROMAN, "a", 444}, + {STD_TIMES_ROMAN, "m", 778}, {STD_TIMES_ROMAN, "one", 500}, {STD_TIMES_ROMAN, "comma", 250}, {STD_TIMES_ROMAN, "period", 250}, + {STD_TIMES_ROMAN, "quoteright", 333}, {STD_TIMES_ROMAN, "quotesingle", 180}, {STD_TIMES_ROMAN, "fraction", 167}, {STD_TIMES_ROMAN, "germandbls", 500}, + {STD_TIMES_ROMAN, "Euro", 500}, {STD_TIMES_ROMAN, "ydieresis", 500}, + // Times-Bold.afm + {STD_TIMES_BOLD, "space", 250}, {STD_TIMES_BOLD, "A", 722}, {STD_TIMES_BOLD, "W", 1000}, {STD_TIMES_BOLD, "a", 500}, + {STD_TIMES_BOLD, "m", 833}, {STD_TIMES_BOLD, "one", 500}, {STD_TIMES_BOLD, "comma", 250}, {STD_TIMES_BOLD, "period", 250}, + {STD_TIMES_BOLD, "quoteright", 333}, {STD_TIMES_BOLD, "quotesingle", 278}, {STD_TIMES_BOLD, "fraction", 167}, {STD_TIMES_BOLD, "germandbls", 556}, + {STD_TIMES_BOLD, "Euro", 500}, {STD_TIMES_BOLD, "ydieresis", 500}, + // Times-Italic.afm + {STD_TIMES_ITALIC, "space", 250}, {STD_TIMES_ITALIC, "A", 611}, {STD_TIMES_ITALIC, "W", 833}, {STD_TIMES_ITALIC, "a", 500}, + {STD_TIMES_ITALIC, "m", 722}, {STD_TIMES_ITALIC, "one", 500}, {STD_TIMES_ITALIC, "comma", 250}, {STD_TIMES_ITALIC, "period", 250}, + {STD_TIMES_ITALIC, "quoteright", 333}, {STD_TIMES_ITALIC, "quotesingle", 214}, {STD_TIMES_ITALIC, "fraction", 167}, {STD_TIMES_ITALIC, "germandbls", 500}, + {STD_TIMES_ITALIC, "Euro", 500}, {STD_TIMES_ITALIC, "ydieresis", 444}, + // Times-BoldItalic.afm + {STD_TIMES_BOLD_ITALIC, "space", 250}, {STD_TIMES_BOLD_ITALIC, "A", 667}, {STD_TIMES_BOLD_ITALIC, "W", 889}, {STD_TIMES_BOLD_ITALIC, "a", 500}, + {STD_TIMES_BOLD_ITALIC, "m", 778}, {STD_TIMES_BOLD_ITALIC, "one", 500}, {STD_TIMES_BOLD_ITALIC, "comma", 250}, {STD_TIMES_BOLD_ITALIC, "period", 250}, + {STD_TIMES_BOLD_ITALIC, "quoteright", 333}, {STD_TIMES_BOLD_ITALIC, "quotesingle", 278}, {STD_TIMES_BOLD_ITALIC, "fraction", 167}, {STD_TIMES_BOLD_ITALIC, "germandbls", 500}, + {STD_TIMES_BOLD_ITALIC, "Euro", 500}, {STD_TIMES_BOLD_ITALIC, "ydieresis", 444}, + // Symbol.afm + {STD_SYMBOL, "space", 250}, {STD_SYMBOL, "exclam", 333}, {STD_SYMBOL, "universal", 713}, {STD_SYMBOL, "numbersign", 500}, + {STD_SYMBOL, "alpha", 631}, {STD_SYMBOL, "beta", 549}, {STD_SYMBOL, "gamma", 411}, {STD_SYMBOL, "Alpha", 722}, + {STD_SYMBOL, "Omega", 768}, {STD_SYMBOL, "summation", 713}, {STD_SYMBOL, "integral", 274}, {STD_SYMBOL, "radical", 549}, + {STD_SYMBOL, "infinity", 713}, {STD_SYMBOL, "copyrightserif", 790}, {STD_SYMBOL, "apple", 790}, + // ZapfDingbats.afm + {STD_ZAPF_DINGBATS, "space", 278}, {STD_ZAPF_DINGBATS, "a1", 974}, {STD_ZAPF_DINGBATS, "a5", 789}, {STD_ZAPF_DINGBATS, "a10", 692}, + {STD_ZAPF_DINGBATS, "a60", 789}, {STD_ZAPF_DINGBATS, "a89", 390}, {STD_ZAPF_DINGBATS, "a100", 668}, {STD_ZAPF_DINGBATS, "a101", 732}, + {STD_ZAPF_DINGBATS, "a109", 626}, {STD_ZAPF_DINGBATS, "a191", 918}, + } + for c in cases { + w, ok := std_font_width(c.font, c.glyph) + testing.expectf(t, ok, "%s has no width for %q", std_font_name(c.font), c.glyph) + testing.expectf(t, w == c.width, "%s %q: width %v, want %v", + std_font_name(c.font), c.glyph, w, c.width) + } +} + +// Courier is fixed pitch, so the width question has one answer for every name, +// including names the font cannot draw: a viewer still moves the pen one cell. +@(test) +test_std_font_width_courier_is_fixed_pitch :: proc(t: ^testing.T) { + for f in ([?]int{STD_COURIER, STD_COURIER_BOLD, STD_COURIER_OBLIQUE, STD_COURIER_BOLD_OBLIQUE}) { + for name in ([?]string{"A", "m", "space", "Aacute", "a1", "uni4E00", "notaglyph"}) { + w, ok := std_font_width(f, name) + testing.expectf(t, ok && w == 600, "%s %q: %v %v", std_font_name(f), name, w, ok) + } + w, ok := std_font_width(f, "") + testing.expect(t, !ok && w == 0, "an empty glyph name has no width even in Courier") + } +} + +// A name the font does not have must report ok = false rather than 0, because +// the caller has to fall back to /MissingWidth and cannot tell those apart if +// the width alone is returned. +@(test) +test_std_font_width_absent_and_out_of_range :: proc(t: ^testing.T) { + for f in ([?]int{STD_HELVETICA, STD_TIMES_BOLD_ITALIC, STD_SYMBOL, STD_ZAPF_DINGBATS}) { + for name in ([?]string{"", "notaglyph", "Asmall", "zzzzzz", "\x00"}) { + w, ok := std_font_width(f, name) + testing.expectf(t, !ok && w == 0, "%s %q: %v %v", std_font_name(f), name, w, ok) + } + } + // Symbol and ZapfDingbats share no glyph names with the text fonts. + _, ok := std_font_width(STD_HELVETICA, "alpha") + testing.expect(t, !ok, "Helvetica has no alpha") + _, ok = std_font_width(STD_SYMBOL, "A") + testing.expect(t, !ok, "Symbol has no A -- code 0x41 is Alpha") + _, ok = std_font_width(STD_ZAPF_DINGBATS, "A") + testing.expect(t, !ok, "ZapfDingbats has no A") + + for f in ([?]int{-1, -1000, STD_FONT_COUNT, 99} ) { + w, iok := std_font_width(f, "A") + testing.expectf(t, !iok && w == 0, "index %d must not resolve", f) + testing.expect(t, std_font_name(f) == "", "no name for a bad index") + testing.expect(t, !std_font_is_symbolic(f), "a bad index is not symbolic") + testing.expect(t, std_font_builtin_encoding(f) == nil, "no encoding for a bad index") + } +} + +@(test) +test_std_font_lookup_canonical_names :: proc(t: ^testing.T) { + for i in 0 ..< STD_FONT_COUNT { + idx, ok := std_font_lookup(std_font_name(i)) + testing.expectf(t, ok, "%q did not resolve", std_font_name(i)) + testing.expectf(t, idx == i, "%q resolved to %d (%s), want %d", + std_font_name(i), idx, std_font_name(idx), i) + } +} + +@(test) +test_std_font_lookup_aliases :: proc(t: ^testing.T) { + cases := [?]struct { + name: string, + want: int, + }{ + // The Monotype spellings Word and Acrobat emit. + {"Arial", STD_HELVETICA}, + {"ArialMT", STD_HELVETICA}, + {"Arial-BoldMT", STD_HELVETICA_BOLD}, + {"Arial-ItalicMT", STD_HELVETICA_OBLIQUE}, + {"Arial-BoldItalicMT", STD_HELVETICA_BOLD_OBLIQUE}, + {"Arial,Bold", STD_HELVETICA_BOLD}, + {"Arial,BoldItalic", STD_HELVETICA_BOLD_OBLIQUE}, + {"Arial-Black", STD_HELVETICA_BOLD}, + {"ArialNarrow", STD_HELVETICA}, + {"Helvetica-Italic", STD_HELVETICA_OBLIQUE}, + {"Helvetica Bold Oblique", STD_HELVETICA_BOLD_OBLIQUE}, + {"NimbusSanL-Regu", STD_HELVETICA}, + {"NimbusSans-BoldItalic", STD_HELVETICA_BOLD_OBLIQUE}, + + {"TimesNewRoman", STD_TIMES_ROMAN}, + {"TimesNewRomanPSMT", STD_TIMES_ROMAN}, + {"TimesNewRomanPS-BoldMT", STD_TIMES_BOLD}, + {"TimesNewRomanPS-ItalicMT", STD_TIMES_ITALIC}, + {"TimesNewRomanPS-BoldItalicMT", STD_TIMES_BOLD_ITALIC}, + {"Times", STD_TIMES_ROMAN}, + {"Times New Roman,Bold", STD_TIMES_BOLD}, + {"NimbusRomNo9L-Medi", STD_TIMES_ROMAN}, + + {"CourierNew", STD_COURIER}, + {"CourierNewPSMT", STD_COURIER}, + {"CourierNewPS-BoldMT", STD_COURIER_BOLD}, + {"CourierNew,BoldItalic", STD_COURIER_BOLD_OBLIQUE}, + {"CourierStd-Oblique", STD_COURIER_OBLIQUE}, + {"NimbusMonL-Regu", STD_COURIER}, + + // The two symbolic fonts have no styles; a style in the name is noise. + {"Symbol", STD_SYMBOL}, + {"SymbolMT", STD_SYMBOL}, + {"Symbol,Bold", STD_SYMBOL}, + {"ZapfDingbats", STD_ZAPF_DINGBATS}, + {"Dingbats", STD_ZAPF_DINGBATS}, + {"ZapfDingbatsITC", STD_ZAPF_DINGBATS}, + } + for c in cases { + idx, ok := std_font_lookup(c.name) + testing.expectf(t, ok, "%q did not resolve", c.name) + testing.expectf(t, idx == c.want, "%q resolved to %d (%s), want %d (%s)", + c.name, idx, std_font_name(idx), c.want, std_font_name(c.want)) + } +} + +@(test) +test_std_font_lookup_subset_prefix :: proc(t: ^testing.T) { + cases := [?]struct { + name: string, + want: int, + }{ + {"ABCDEF+Helvetica", STD_HELVETICA}, + {"AAAAAA+Arial-BoldMT", STD_HELVETICA_BOLD}, + {"XYZabc+Times-Italic", STD_TIMES_ITALIC}, + {"ABCD+Courier", STD_COURIER}, + {"ABCDEF+ZapfDingbats", STD_ZAPF_DINGBATS}, + } + for c in cases { + idx, ok := std_font_lookup(c.name) + testing.expectf(t, ok && idx == c.want, "%q -> %d %v", c.name, idx, ok) + } + // The tag really is removed, not just ignored: a tag that spells a family + // must not decide the family of the font behind it. + idx, ok := std_font_lookup("TIMESX+Helvetica") + testing.expect(t, ok && idx == STD_HELVETICA, "the subset tag must not be matched against") + idx, ok = std_font_lookup("BOLDXX+Helvetica") + testing.expect(t, ok && idx == STD_HELVETICA, "a tag spelling a style must not set the style") +} + +@(test) +test_std_font_lookup_rejects_other_fonts :: proc(t: ^testing.T) { + for name in ([?]string{ + "", "+", "ABCDEF+", "Garamond", "Wingdings", "Wingdings-Regular", + "MS-Mincho", "Calibri", "Cambria-Bold", "DejaVuSans", "Verdana", + "Georgia", "TrebuchetMS", "Webdings", "\x00\x01\x02", + }) { + idx, ok := std_font_lookup(name) + testing.expectf(t, !ok && idx == 0, "%q must not resolve to a standard font", name) + } + // A name longer than the normalising buffer must be handled, not crash. + long := make([]u8, 4096) + defer delete(long) + for i in 0 ..< len(long) { + long[i] = 'x' + } + _, ok := std_font_lookup(string(long)) + testing.expect(t, !ok, "a 4 KB name of nothing is not a standard font") +} + +@(test) +test_std_font_is_symbolic :: proc(t: ^testing.T) { + for i in 0 ..< STD_FONT_COUNT { + want := i == STD_SYMBOL || i == STD_ZAPF_DINGBATS + testing.expectf(t, std_font_is_symbolic(i) == want, "%s", std_font_name(i)) + } + testing.expect(t, std_font_builtin_encoding(STD_SYMBOL) == encoding_symbol(), "") + testing.expect(t, std_font_builtin_encoding(STD_ZAPF_DINGBATS) == encoding_zapf(), "") + testing.expect(t, std_font_builtin_encoding(STD_HELVETICA) == encoding_standard(), "") + testing.expect(t, std_font_builtin_encoding(STD_TIMES_ROMAN) == encoding_standard(), "") +} + +// --------------------------------------------------------------------------- +// Encodings +// --------------------------------------------------------------------------- + +// The classic wrong-apostrophe bug: code 0x27 is a right single quote in +// StandardEncoding and a straight typewriter apostrophe in WinAnsi, and the +// straight one lives at 0xA9 in Standard while the curly one lives at 0x92 in +// WinAnsi. Reading a WinAnsi document with the Standard table turns every +// apostrophe into a curly quote and every 0x92 into nothing. +@(test) +test_encoding_quote_positions :: proc(t: ^testing.T) { + std := encoding_standard() + win := encoding_winansi() + + testing.expect_value(t, std[0x27], "quoteright") + testing.expect_value(t, win[0x27], "quotesingle") + testing.expect_value(t, std[0xA9], "quotesingle") + testing.expect_value(t, win[0x92], "quoteright") + + testing.expect_value(t, std[0x60], "quoteleft") + testing.expect_value(t, win[0x60], "grave") + testing.expect_value(t, win[0x91], "quoteleft") + + // Standard has no code at all where WinAnsi keeps the Latin-1 letters. + testing.expect_value(t, std[0xFC], "") + testing.expect_value(t, win[0xFC], "udieresis") + + testing.expect_value(t, glyph_name_to_rune(std[0x27]), rune(0x2019)) + testing.expect_value(t, glyph_name_to_rune(win[0x27]), rune(0x0027)) +} + +// 0x80..0x9F is where WinAnsi stops being Latin-1: CP1252 puts the typographic +// characters in the C1 control area, and the annex fills every code CP1252 +// leaves undefined with bullet (ISO 32000-2, Annex D.2, note 3). +@(test) +test_encoding_winansi_c1_range :: proc(t: ^testing.T) { + win := encoding_winansi() + cases := [?]struct { + code: int, + name: string, + }{ + {0x80, "Euro"}, {0x81, "bullet"}, {0x82, "quotesinglbase"}, {0x83, "florin"}, + {0x84, "quotedblbase"}, {0x85, "ellipsis"}, {0x86, "dagger"}, {0x87, "daggerdbl"}, + {0x88, "circumflex"}, {0x89, "perthousand"}, {0x8A, "Scaron"}, {0x8B, "guilsinglleft"}, + {0x8C, "OE"}, {0x8D, "bullet"}, {0x8E, "Zcaron"}, {0x8F, "bullet"}, + {0x90, "bullet"}, {0x91, "quoteleft"}, {0x92, "quoteright"}, {0x93, "quotedblleft"}, + {0x94, "quotedblright"}, {0x95, "bullet"}, {0x96, "endash"}, {0x97, "emdash"}, + {0x98, "tilde"}, {0x99, "trademark"}, {0x9A, "scaron"}, {0x9B, "guilsinglright"}, + {0x9C, "oe"}, {0x9D, "bullet"}, {0x9E, "zcaron"}, {0x9F, "Ydieresis"}, + } + for c in cases { + testing.expectf(t, win[c.code] == c.name, "WinAnsi 0x%02X: %q, want %q", + c.code, win[c.code], c.name) + } + // Latin-1 would have left all of these undefined. + std := encoding_standard() + for c in cases { + testing.expectf(t, std[c.code] == "", "StandardEncoding 0x%02X is undefined", c.code) + } +} + +// Above 0xA0 WinAnsi is Latin-1 exactly, with two codes that carry a character +// PDF draws as its plain form: a no-break space and a soft hyphen. +@(test) +test_encoding_winansi_is_latin1_above_a0 :: proc(t: ^testing.T) { + win := encoding_winansi() + for code in 0xA0 ..= 0xFF { + name := win[code] + testing.expectf(t, name != "", "WinAnsi 0x%02X is defined", code) + want := rune(code) + switch code { + case 0xA0: + want = 0x0020 // no-break space, drawn as a space + case 0xAD: + want = 0x002D // soft hyphen, drawn as a hyphen + } + testing.expectf(t, glyph_name_to_rune(name) == want, + "WinAnsi 0x%02X (%s) -> U+%04X, want U+%04X", code, name, + glyph_name_to_rune(name), want) + } + // ASCII is shared by both Latin encodings apart from the two quote codes. + std := encoding_standard() + for code in 0x20 ..= 0x7E { + if code == 0x27 || code == 0x60 { + continue + } + testing.expectf(t, std[code] == win[code], "0x%02X: %q vs %q", code, std[code], win[code]) + testing.expectf(t, glyph_name_to_rune(win[code]) == rune(code), + "WinAnsi 0x%02X (%s)", code, win[code]) + } +} + +@(test) +test_encoding_macroman_high_range :: proc(t: ^testing.T) { + mac := encoding_macroman() + cases := [?]struct { + code: int, + name: string, + }{ + {0x80, "Adieresis"}, {0x81, "Aring"}, {0x82, "Ccedilla"}, {0x83, "Eacute"}, + {0x84, "Ntilde"}, {0x85, "Odieresis"}, {0x86, "Udieresis"}, {0x87, "aacute"}, + {0x88, "agrave"}, {0x89, "acircumflex"}, {0x8A, "adieresis"}, {0x8B, "atilde"}, + {0x8C, "aring"}, {0x8D, "ccedilla"}, {0x8E, "eacute"}, {0x8F, "egrave"}, + {0x90, "ecircumflex"}, {0x91, "edieresis"}, {0x92, "iacute"}, {0x93, "igrave"}, + {0x94, "icircumflex"}, {0x95, "idieresis"}, {0x96, "ntilde"}, {0x97, "oacute"}, + {0x98, "ograve"}, {0x99, "ocircumflex"}, {0x9A, "odieresis"}, {0x9B, "otilde"}, + {0x9C, "uacute"}, {0x9D, "ugrave"}, {0x9E, "ucircumflex"}, {0x9F, "udieresis"}, + {0xA0, "dagger"}, {0xA1, "degree"}, {0xA2, "cent"}, {0xA3, "sterling"}, + {0xA4, "section"}, {0xA5, "bullet"}, {0xA6, "paragraph"}, {0xA7, "germandbls"}, + {0xA8, "registered"}, {0xA9, "copyright"}, {0xAA, "trademark"}, {0xAB, "acute"}, + {0xAC, "dieresis"}, {0xAD, "notequal"}, {0xAE, "AE"}, {0xAF, "Oslash"}, + {0xB0, "infinity"}, {0xB1, "plusminus"}, {0xB2, "lessequal"}, {0xB3, "greaterequal"}, + {0xB4, "yen"}, {0xB5, "mu"}, {0xB6, "partialdiff"}, {0xB7, "summation"}, + {0xB8, "product"}, {0xB9, "pi"}, {0xBA, "integral"}, {0xBB, "ordfeminine"}, + {0xBC, "ordmasculine"}, {0xBD, "Omega"}, {0xBE, "ae"}, {0xBF, "oslash"}, + {0xC0, "questiondown"}, {0xC1, "exclamdown"}, {0xC2, "logicalnot"}, {0xC3, "radical"}, + {0xC4, "florin"}, {0xC5, "approxequal"}, {0xC6, "Delta"}, {0xC7, "guillemotleft"}, + {0xC8, "guillemotright"}, {0xC9, "ellipsis"}, {0xCA, "space"}, {0xCB, "Agrave"}, + {0xCC, "Atilde"}, {0xCD, "Otilde"}, {0xCE, "OE"}, {0xCF, "oe"}, + {0xD0, "endash"}, {0xD1, "emdash"}, {0xD2, "quotedblleft"}, {0xD3, "quotedblright"}, + {0xD4, "quoteleft"}, {0xD5, "quoteright"}, {0xD6, "divide"}, {0xD7, "lozenge"}, + {0xD8, "ydieresis"}, {0xD9, "Ydieresis"}, {0xDA, "fraction"}, {0xDB, "currency"}, + {0xDC, "guilsinglleft"}, {0xDD, "guilsinglright"}, {0xDE, "fi"}, {0xDF, "fl"}, + {0xE0, "daggerdbl"}, {0xE1, "periodcentered"}, {0xE2, "quotesinglbase"}, {0xE3, "quotedblbase"}, + {0xE4, "perthousand"}, {0xE5, "Acircumflex"}, {0xE6, "Ecircumflex"}, {0xE7, "Aacute"}, + {0xE8, "Edieresis"}, {0xE9, "Egrave"}, {0xEA, "Iacute"}, {0xEB, "Icircumflex"}, + {0xEC, "Idieresis"}, {0xED, "Igrave"}, {0xEE, "Oacute"}, {0xEF, "Ocircumflex"}, + {0xF0, "apple"}, {0xF1, "Ograve"}, {0xF2, "Uacute"}, {0xF3, "Ucircumflex"}, + {0xF4, "Ugrave"}, {0xF5, "dotlessi"}, {0xF6, "circumflex"}, {0xF7, "tilde"}, + {0xF8, "macron"}, {0xF9, "breve"}, {0xFA, "dotaccent"}, {0xFB, "ring"}, + {0xFC, "cedilla"}, {0xFD, "hungarumlaut"}, {0xFE, "ogonek"}, {0xFF, "caron"}, + } + for c in cases { + testing.expectf(t, mac[c.code] == c.name, "MacRoman 0x%02X: %q, want %q", + c.code, mac[c.code], c.name) + } + // 0xCA is Mac OS Roman's no-break space; PDF draws it as an ordinary space. + testing.expect_value(t, mac[0xCA], "space") + // MacRoman is not Latin-1: 0x80 is A-dieresis, not a control code, and the + // accented letters are in a completely different order. + win := encoding_winansi() + differing := 0 + for code in 0x80 ..= 0xFF { + if mac[code] != win[code] { + differing += 1 + } + } + testing.expectf(t, differing > 100, "MacRoman and WinAnsi differ widely above 0x80, got %d", differing) +} + +// ISO 32000-2 Annex D.2 leaves these fifteen positions blank, but Mac OS Roman +// defines them and producers emitted them; every other reader fills them in. +@(test) +test_encoding_macroman_mac_os_extras :: proc(t: ^testing.T) { + mac := encoding_macroman() + cases := [?]struct { + code: int, + name: string, + cp: rune, + }{ + {0xAD, "notequal", 0x2260}, {0xB0, "infinity", 0x221E}, + {0xB2, "lessequal", 0x2264}, {0xB3, "greaterequal", 0x2265}, + {0xB6, "partialdiff", 0x2202}, {0xB7, "summation", 0x2211}, + {0xB8, "product", 0x220F}, {0xB9, "pi", 0x03C0}, + {0xBA, "integral", 0x222B}, {0xBD, "Omega", 0x2126}, + {0xC3, "radical", 0x221A}, {0xC5, "approxequal", 0x2248}, + {0xC6, "Delta", 0x2206}, {0xD7, "lozenge", 0x25CA}, + {0xF0, "apple", 0xF8FF}, + } + for c in cases { + testing.expectf(t, mac[c.code] == c.name, "MacRoman 0x%02X: %q", c.code, mac[c.code]) + testing.expectf(t, glyph_name_to_rune(c.name) == c.cp, + "%s -> U+%04X, want U+%04X", c.name, glyph_name_to_rune(c.name), c.cp) + } +} + +@(test) +test_encoding_standard_high_range :: proc(t: ^testing.T) { + std := encoding_standard() + cases := [?]struct { + code: int, + name: string, + }{ + {0xA1, "exclamdown"}, {0xA2, "cent"}, {0xA3, "sterling"}, {0xA4, "fraction"}, + {0xA5, "yen"}, {0xA6, "florin"}, {0xA7, "section"}, {0xA8, "currency"}, + {0xA9, "quotesingle"}, {0xAA, "quotedblleft"}, {0xAB, "guillemotleft"}, {0xAC, "guilsinglleft"}, + {0xAD, "guilsinglright"}, {0xAE, "fi"}, {0xAF, "fl"}, {0xB1, "endash"}, + {0xB2, "dagger"}, {0xB3, "daggerdbl"}, {0xB4, "periodcentered"}, {0xB6, "paragraph"}, + {0xB7, "bullet"}, {0xB8, "quotesinglbase"}, {0xB9, "quotedblbase"}, {0xBA, "quotedblright"}, + {0xBB, "guillemotright"}, {0xBC, "ellipsis"}, {0xBD, "perthousand"}, {0xBF, "questiondown"}, + {0xC1, "grave"}, {0xC2, "acute"}, {0xC3, "circumflex"}, {0xC4, "tilde"}, + {0xC5, "macron"}, {0xC6, "breve"}, {0xC7, "dotaccent"}, {0xC8, "dieresis"}, + {0xCA, "ring"}, {0xCB, "cedilla"}, {0xCD, "hungarumlaut"}, {0xCE, "ogonek"}, + {0xCF, "caron"}, {0xD0, "emdash"}, {0xE1, "AE"}, {0xE3, "ordfeminine"}, + {0xE8, "Lslash"}, {0xE9, "Oslash"}, {0xEA, "OE"}, {0xEB, "ordmasculine"}, + {0xF1, "ae"}, {0xF5, "dotlessi"}, {0xF8, "lslash"}, {0xF9, "oslash"}, + {0xFA, "oe"}, {0xFB, "germandbls"}, + } + seen: [256]bool + for c in cases { + testing.expectf(t, std[c.code] == c.name, "Standard 0x%02X: %q, want %q", + c.code, std[c.code], c.name) + seen[c.code] = true + } + // Everything above 0x7F that the table above does not name is undefined. + for code in 0x80 ..= 0xFF { + if !seen[code] { + testing.expectf(t, std[code] == "", "Standard 0x%02X should be undefined, got %q", + code, std[code]) + } + } + // StandardEncoding has no accented letters at all; that is why a document + // in French needs WinAnsi, MacRoman or a /Differences array. + for name in ([?]string{"eacute", "ccedilla", "udieresis", "Adieresis"}) { + found := false + for code in 0 ..< 256 { + if std[code] == name { + found = true + } + } + testing.expectf(t, !found, "StandardEncoding must not contain %s", name) + } +} + +@(test) +test_encoding_symbol_and_zapf :: proc(t: ^testing.T) { + sym := encoding_symbol() + testing.expect_value(t, sym[0x20], "space") + testing.expect_value(t, sym[0x41], "Alpha") + testing.expect_value(t, sym[0x61], "alpha") + testing.expect_value(t, sym[0x62], "beta") + testing.expect_value(t, sym[0xD6], "radical") + testing.expect_value(t, sym[0xE5], "summation") + testing.expect_value(t, sym[0xF2], "integral") + testing.expect_value(t, sym[0xA0], "Euro") + // Symbol's 0x41 is not "A": reading it with a Latin table gives Greek text + // spelled in Latin letters, the usual symptom of a missing built-in encoding. + testing.expect(t, sym[0x41] != encoding_standard()[0x41], "") + + zapf := encoding_zapf() + testing.expect_value(t, zapf[0x20], "space") + testing.expect_value(t, zapf[0x21], "a1") + testing.expect_value(t, zapf[0x41], "a10") + testing.expect_value(t, zapf[0x61], "a60") + testing.expect_value(t, zapf[0x7E], "a100") + testing.expect_value(t, zapf[0x80], "a89") + testing.expect_value(t, zapf[0xA1], "a101") + testing.expect_value(t, zapf[0xFE], "a191") + testing.expect_value(t, zapf[0xFF], "") + + exp := encoding_macexpert() + testing.expect_value(t, exp[0x20], "space") + testing.expect_value(t, exp[0x21], "exclamsmall") + testing.expect_value(t, exp[0x30], "zerooldstyle") + testing.expect_value(t, exp[0x61], "Asmall") + testing.expect_value(t, exp[0xF6], "nsuperior") + testing.expect_value(t, exp[0x40], "") +} + +@(test) +test_encoding_by_name :: proc(t: ^testing.T) { + testing.expect(t, encoding_by_name("WinAnsiEncoding") == encoding_winansi(), "") + testing.expect(t, encoding_by_name("MacRomanEncoding") == encoding_macroman(), "") + testing.expect(t, encoding_by_name("MacExpertEncoding") == encoding_macexpert(), "") + testing.expect(t, encoding_by_name("StandardEncoding") == encoding_standard(), "") + testing.expect(t, encoding_by_name("WinAnsi") == encoding_winansi(), "") + testing.expect(t, encoding_by_name("MacRoman") == encoding_macroman(), "") + // An unknown name is nil, which the caller must read as "leave the font's + // built-in encoding alone" rather than "reset to Standard". + for name in ([?]string{"", "Encoding", "winansiencoding", "WinAnsiEncodings", + "PDFDocEncoding", "Identity-H", "\x00"}) { + testing.expectf(t, encoding_by_name(name) == nil, "%q must not resolve", name) + } + // The four tables are genuinely different objects. + testing.expect(t, encoding_standard() != encoding_winansi(), "") + testing.expect(t, encoding_winansi() != encoding_macroman(), "") + testing.expect(t, encoding_macroman() != encoding_macexpert(), "") +} + +// --------------------------------------------------------------------------- +// Glyph names +// --------------------------------------------------------------------------- + +@(test) +test_glyph_name_to_rune_agl_algorithm :: proc(t: ^testing.T) { + cases := [?]struct { + name: string, + want: rune, + }{ + // Ordinary AGL names. + {"A", 0x0041}, + {"space", 0x0020}, + {"quoteright", 0x2019}, + {"eacute", 0x00E9}, + {"germandbls", 0x00DF}, + {"fi", 0xFB01}, + {"Alpha", 0x0391}, + {"alpha", 0x03B1}, + {"afii10017", 0x0410}, // Cyrillic capital A + {"Acyrillic", 0x0410}, + {"summation", 0x2211}, + {"a1", 0x2701}, // ZapfDingbats' own list + {"Euro", 0x20AC}, + + // uniXXXX, one code point per four digits; the first one wins. + {"uni0041", 0x0041}, + {"uni00e9", 0x00E9}, // lowercase hex is not AGL-legal but common + {"uni20AC", 0x20AC}, + {"uni00410042", 0x0041}, + + // uXXXX through uXXXXXX. + {"u0041", 0x0041}, + {"u1F600", 0x1F600}, + {"u10FFFF", 0x10FFFF}, + + // A suffix after a period is a stylistic variant of the same character. + {"a.sc", 0x0061}, + {"one.oldstyle", 0x0031}, + {"A.alt01", 0x0041}, + {"uni0041.swash", 0x0041}, + + // An underscore joins components; a ligature is named by its parts and + // only the first can be represented as one rune. + {"f_i", 0x0066}, + {"f_f_l", 0x0066}, + {"uni0066_uni0069", 0x0066}, + + // The AGL is consulted before the uni/u forms, or a real name that + // begins with those letters would be parsed as hex. + {"union", 0x222A}, + {"universal", 0x2200}, + {"uacute", 0x00FA}, + {"u", 0x0075}, + {"g", 0x0067}, + + // Nothing at all. + {"", -1}, + {".notdef", -1}, + {".null", -1}, + {"_", -1}, + {"g3", -1}, // a glyph index, not a character + {"g127", -1}, + {"cid42", -1}, + {"index7", -1}, + {"glyph12", -1}, + {"uni", -1}, + {"uni41", -1}, // not a multiple of four digits + {"uni00411", -1}, + {"uniZZZZ", -1}, + {"uniD800", -1}, // a lone surrogate is not a character + {"uniDFFF", -1}, + {"uni0041D800", -1}, // and not in a later group either + {"uni004100", -1}, // a trailing part-group is not a name + {"u110000", -1}, // past the end of the code space + {"uD800", -1}, + {"u041", -1}, // fewer than four digits + {"u00000041", -1}, // more than six + {"notaglyphname", -1}, + {"\x00\x01", -1}, + } + for c in cases { + got := glyph_name_to_rune(c.name) + testing.expectf(t, got == c.want, "%q -> U+%04X (%d), want U+%04X (%d)", + c.name, got, got, c.want, c.want) + } +} + +// The ASCII range must survive a trip through both directions, because that is +// the path every /Differences array and every text extraction takes. +@(test) +test_rune_to_glyph_name_ascii_round_trip :: proc(t: ^testing.T) { + for r in rune(0x20) ..= rune(0x7E) { + name := rune_to_glyph_name(r) + testing.expectf(t, name != "", "U+%04X has no glyph name", r) + back := glyph_name_to_rune(name) + testing.expectf(t, back == r, "U+%04X -> %q -> U+%04X", r, name, back) + } + // The names are the plain ones, not an obscure synonym. + testing.expect_value(t, rune_to_glyph_name(' '), "space") + testing.expect_value(t, rune_to_glyph_name('A'), "A") + testing.expect_value(t, rune_to_glyph_name('z'), "z") + testing.expect_value(t, rune_to_glyph_name('0'), "zero") + testing.expect_value(t, rune_to_glyph_name('.'), "period") + testing.expect_value(t, rune_to_glyph_name('-'), "hyphen") +} + +@(test) +test_rune_to_glyph_name_beyond_ascii :: proc(t: ^testing.T) { + cases := [?]struct { + r: rune, + name: string, + }{ + {0x00E9, "eacute"}, + {0x00DF, "germandbls"}, + {0x2019, "quoteright"}, + {0x2014, "emdash"}, + {0x20AC, "Euro"}, + {0x0391, "Alpha"}, + {0x03C0, "pi"}, + {0xFB01, "fi"}, + {0x2701, "a1"}, + } + for c in cases { + testing.expectf(t, rune_to_glyph_name(c.r) == c.name, "U+%04X -> %q, want %q", + c.r, rune_to_glyph_name(c.r), c.name) + testing.expectf(t, glyph_name_to_rune(c.name) == c.r, "%q -> U+%04X", + c.name, glyph_name_to_rune(c.name)) + } + // Code points the list does not name come back empty rather than as a + // fabricated "uniXXXX", which a font would not find either. + for r in ([?]rune{-1, 0, 0x0378, 0x4E00, 0x1F600, 0x10FFFF, 0x110000}) { + testing.expectf(t, rune_to_glyph_name(r) == "", "U+%04X must have no name", r) + } +} + +// --------------------------------------------------------------------------- +// Table invariants +// --------------------------------------------------------------------------- + +// Every lookup here is a binary search, so a table that is out of order fails +// silently for some of its entries and not others. Walking each table proves +// both the ordering and that the search finds everything in it. +@(test) +test_tables_are_sorted_and_complete :: proc(t: ^testing.T) { + for i in 1 ..< len(STD_TEXT_GLYPHS) { + testing.expectf(t, STD_TEXT_GLYPHS[i-1] < STD_TEXT_GLYPHS[i], + "STD_TEXT_GLYPHS out of order at %d: %q then %q", i, STD_TEXT_GLYPHS[i-1], STD_TEXT_GLYPHS[i]) + } + for i in 1 ..< len(STD_SYMBOL_GLYPHS) { + testing.expectf(t, STD_SYMBOL_GLYPHS[i-1] < STD_SYMBOL_GLYPHS[i], "STD_SYMBOL_GLYPHS at %d", i) + } + for i in 1 ..< len(STD_ZAPF_GLYPHS) { + testing.expectf(t, STD_ZAPF_GLYPHS[i-1] < STD_ZAPF_GLYPHS[i], "STD_ZAPF_GLYPHS at %d", i) + } + for i in 1 ..< len(AGL_TABLE) { + testing.expectf(t, AGL_TABLE[i-1].name < AGL_TABLE[i].name, + "AGL_TABLE out of order at %d: %q then %q", i, AGL_TABLE[i-1].name, AGL_TABLE[i].name) + } + for i in 1 ..< len(AGL_BY_RUNE) { + testing.expectf(t, AGL_TABLE[AGL_BY_RUNE[i-1]].cp < AGL_TABLE[AGL_BY_RUNE[i]].cp, + "AGL_BY_RUNE out of order at %d", i) + } + + // Every name is findable, and every proportional width is plausible: the + // narrowest glyph in any of the fourteen is 138 units and the widest 1042. + for name, i in STD_TEXT_GLYPHS { + testing.expectf(t, len(name) > 0, "empty name at %d", i) + for f in STD_HELVETICA ..= STD_TIMES_BOLD_ITALIC { + w, ok := std_font_width(f, name) + testing.expectf(t, ok, "%s has no %q", std_font_name(f), name) + testing.expectf(t, w >= 138 && w <= 1042, "%s %q: %v", std_font_name(f), name, w) + } + } + for name in STD_SYMBOL_GLYPHS { + w, ok := std_font_width(STD_SYMBOL, name) + testing.expectf(t, ok && w >= 138 && w <= 1042, "Symbol %q: %v %v", name, w, ok) + } + for name in STD_ZAPF_GLYPHS { + w, ok := std_font_width(STD_ZAPF_DINGBATS, name) + testing.expectf(t, ok && w >= 138 && w <= 1042, "ZapfDingbats %q: %v %v", name, w, ok) + } + // agl_is_index_name is only equivalent to the fallthrough while the table + // holds no name shaped like a glyph index; that is the invariant it rests on. + for e in AGL_TABLE { + for p in ([?]string{"g", "cid", "index", "glyph"}) { + if len(e.name) <= len(p) || e.name[:len(p)] != p { + continue + } + digits := true + for j in len(p) ..< len(e.name) { + if e.name[j] < '0' || e.name[j] > '9' { + digits = false + } + } + testing.expectf(t, !digits, "AGL name %q is shaped like a glyph index", e.name) + } + } + for e, i in AGL_TABLE { + testing.expectf(t, len(e.name) > 0, "empty AGL name at %d", i) + testing.expectf(t, glyph_name_to_rune(e.name) == e.cp, + "AGL %q -> U+%04X, want U+%04X", e.name, glyph_name_to_rune(e.name), e.cp) + } +} + +// A width table that is missing a glyph the encoding can name is the same bug as +// a wrong width: the caller falls back to /MissingWidth, which a standard-14 +// font dictionary usually does not carry, and the line collapses. +@(test) +test_every_encoded_glyph_has_a_width :: proc(t: ^testing.T) { + // Seven glyphs are named by Mac OS Roman but are genuinely not in Adobe's + // base 14 -- Helvetica has no infinity and no apple. A viewer draws .notdef + // for them, so no width is the honest answer; they are listed rather than + // skipped quietly so the rest of the sweep stays strict, and each one is + // checked below to still be absent. + absent := [?]string{"Omega", "apple", "approxequal", "infinity", "integral", "pi", "product"} + is_absent :: proc(list: []string, name: string) -> bool { + for x in list { + if x == name { + return true + } + } + return false + } + for enc in ([?]^[256]string{encoding_standard(), encoding_winansi(), encoding_macroman()}) { + for name, code in enc { + if name == "" || is_absent(absent[:], name) { + continue + } + for f in 0 ..< STD_SYMBOL { + _, ok := std_font_width(f, name) + testing.expectf(t, ok, "%s has no width for %q (code 0x%02X)", + std_font_name(f), name, code) + } + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } + } + for name in absent { + _, ok := std_font_width(STD_HELVETICA, name) + testing.expectf(t, !ok, "%q is in Helvetica after all; drop it from the list", name) + _, ok = std_font_width(STD_TIMES_ROMAN, name) + testing.expectf(t, !ok, "%q is in Times-Roman after all; drop it from the list", name) + testing.expectf(t, glyph_name_to_rune(name) >= 0, + "%q must still extract as text even with no width", name) + } + // The eight that MacRoman names and the fonts do carry must not be in that + // list by accident. + for name in ([?]string{"notequal", "lessequal", "greaterequal", "partialdiff", + "summation", "radical", "Delta", "lozenge"}) { + _, ok := std_font_width(STD_HELVETICA, name) + testing.expectf(t, ok, "Helvetica does have %q", name) + } + for name in encoding_symbol() { + if name == "" { + continue + } + _, ok := std_font_width(STD_SYMBOL, name) + testing.expectf(t, ok, "Symbol has no width for %q", name) + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } + for name in encoding_zapf() { + if name == "" { + continue + } + _, ok := std_font_width(STD_ZAPF_DINGBATS, name) + testing.expectf(t, ok, "ZapfDingbats has no width for %q", name) + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } + // Every glyph the fonts do have must extract as text, or a page set in one + // of them cannot be searched. + for name in STD_TEXT_GLYPHS { + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } + for name in STD_SYMBOL_GLYPHS { + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } + for name in STD_ZAPF_GLYPHS { + testing.expectf(t, glyph_name_to_rune(name) >= 0, "%q has no Unicode", name) + } +} + +// The helpers underneath the public four, checked directly: they are where a +// name that came out of a file is taken apart, so they carry the bounds. +@(test) +test_std_name_helpers :: proc(t: ^testing.T) { + testing.expect_value(t, std_strip_subset("ABCDEF+Helvetica"), "Helvetica") + testing.expect_value(t, std_strip_subset("abcdef+Helvetica"), "Helvetica") + testing.expect_value(t, std_strip_subset("AB+Helvetica"), "Helvetica") + testing.expect_value(t, std_strip_subset("Helvetica"), "Helvetica") + testing.expect_value(t, std_strip_subset(""), "") + testing.expect_value(t, std_strip_subset("+Helvetica"), "+Helvetica") + testing.expect_value(t, std_strip_subset("ABCDEF+"), "") + // The plus must be within the tag, and only letters may precede it. + testing.expect_value(t, std_strip_subset("Helvetica+Bold"), "Helvetica+Bold") + testing.expect_value(t, std_strip_subset("AB1DEF+Helvetica"), "AB1DEF+Helvetica") + + buf: [8]u8 + testing.expect_value(t, std_normalize("Arial-BoldMT", buf[:]), "arialbol") + testing.expect_value(t, std_normalize("", buf[:]), "") + big: [64]u8 + testing.expect_value(t, std_normalize("Times New Roman,Bold", big[:]), "timesnewromanbold") + testing.expect_value(t, std_normalize("\xff\x00-_.", big[:]), "") + + testing.expect(t, std_contains("helvetica", "vet"), "") + testing.expect(t, std_contains("helvetica", "helvetica"), "") + testing.expect(t, std_contains("helvetica", ""), "the empty needle is everywhere") + testing.expect(t, !std_contains("helvetica", "helveticax"), "") + testing.expect(t, !std_contains("", "a"), "") + testing.expect(t, std_contains("aaab", "ab"), "") +} diff --git a/font_test.odin b/font_test.odin new file mode 100644 index 0000000..b15e9f2 --- /dev/null +++ b/font_test.odin @@ -0,0 +1,2171 @@ +package pdf + +import "core:testing" + +// Tests for font.odin: the PDF font dictionary, its encodings, its widths and +// its CID machinery. +// +// The Document fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` -- +// live in function_test.odin. +// +// The font programs here are assembled from byte literals rather than taken +// from real files, because what is under test is the *dictionary's* effect on +// them: which cmap subtable a symbolic flag selects, which name a +// /Differences entry substitutes, which glyph a CID reaches. The programs +// themselves are tested in font_truetype_test.odin and font_cff_test.odin, so +// the ones built here are the smallest that can answer a lookup -- often a +// cmap and a glyph count and nothing else. +// +// Everything is allocated from the Document's arena, so a failing assertion +// cannot leak: `close` reclaims the lot however the test left. + +// --------------------------------------------------------------------------- +// Byte assembly +// --------------------------------------------------------------------------- + +@(private = "file") +fn_alloc :: proc(doc: ^Document, n: int) -> []byte { + return make([]byte, n, doc.allocator) +} + +@(private = "file") +fn_w16 :: proc(b: []byte, off: int, v: u16) { + b[off] = u8(v >> 8) + b[off + 1] = u8(v) +} + +@(private = "file") +fn_w32 :: proc(b: []byte, off: int, v: u32) { + b[off] = u8(v >> 24) + b[off + 1] = u8(v >> 16) + b[off + 2] = u8(v >> 8) + b[off + 3] = u8(v) +} + +@(private = "file") +Fn_Table :: struct { + tag: string, + data: []byte, +} + +// fn_sfnt writes a table directory around `tables`. +@(private = "file") +fn_sfnt :: proc(doc: ^Document, tables: []Fn_Table, sfnt: u32 = 0x00010000) -> []byte { + total := 12 + 16 * len(tables) + for tb in tables { + total += (len(tb.data) + 3) & ~int(3) + } + out := fn_alloc(doc, total) + fn_w32(out, 0, sfnt) + fn_w16(out, 4, u16(len(tables))) + + pos := 12 + 16 * len(tables) + for tb, i in tables { + rec := 12 + 16 * i + for k in 0 ..< 4 { + out[rec + k] = tb.tag[k] if k < len(tb.tag) else ' ' + } + fn_w32(out, rec + 8, u32(pos)) + fn_w32(out, rec + 12, u32(len(tb.data))) + copy(out[pos:], tb.data) + pos += (len(tb.data) + 3) & ~int(3) + } + return out +} + +@(private = "file") +fn_head :: proc(doc: ^Document, upem: u16) -> []byte { + out := fn_alloc(doc, 54) + fn_w16(out, 18, upem) + return out // indexToLocFormat 0: short loca +} + +@(private = "file") +fn_maxp :: proc(doc: ^Document, num_glyphs: u16) -> []byte { + out := fn_alloc(doc, 32) + fn_w32(out, 0, 0x00010000) + fn_w16(out, 4, num_glyphs) + return out +} + +@(private = "file") +fn_hhea :: proc(doc: ^Document, n_metrics: u16) -> []byte { + out := fn_alloc(doc, 36) + fn_w16(out, 34, n_metrics) + return out +} + +// fn_hmtx writes one longHorMetric per advance: the advance itself and a zero +// left side bearing. +@(private = "file") +fn_hmtx :: proc(doc: ^Document, advances: []u16) -> []byte { + out := fn_alloc(doc, 4 * len(advances)) + for a, i in advances { + fn_w16(out, 4 * i, a) + } + return out +} + +@(private = "file") +Fn_Cmap_Rec :: struct { + plat, enc: u16, + sub: []byte, +} + +@(private = "file") +fn_cmap :: proc(doc: ^Document, recs: ..Fn_Cmap_Rec) -> []byte { + total := 4 + 8 * len(recs) + for r in recs { + total += len(r.sub) + } + out := fn_alloc(doc, total) + fn_w16(out, 2, u16(len(recs))) + pos := 4 + 8 * len(recs) + for r, i in recs { + rec := 4 + 8 * i + fn_w16(out, rec, r.plat) + fn_w16(out, rec + 2, r.enc) + fn_w32(out, rec + 4, u32(pos)) + copy(out[pos:], r.sub) + pos += len(r.sub) + } + return out +} + +// Fn_Seg is one format-4 segment: code `lo` maps to glyph `gid`, and the +// codes up to `hi` map to the glyphs after it. The idDelta the table actually +// stores is the difference, which is what the builder computes. +@(private = "file") +Fn_Seg :: struct { + lo, hi, gid: u16, +} + +// fn_cmap4 builds a format 4 subtable out of delta segments, appending the +// 0xFFFF terminator the format requires. idRangeOffset is zero throughout, so +// every segment is a pure delta -- the glyphIdArray indirection is +// font_truetype.odin's business and is tested there. +@(private = "file") +fn_cmap4 :: proc(doc: ^Document, segs: ..Fn_Seg) -> []byte { + n := len(segs) + 1 + out := fn_alloc(doc, 16 + 8 * n) + fn_w16(out, 0, 4) + fn_w16(out, 2, u16(len(out))) + fn_w16(out, 6, u16(2 * n)) + end_p := 14 + start_p := end_p + 2 * n + 2 + delta_p := start_p + 2 * n + for s, i in segs { + fn_w16(out, end_p + 2 * i, s.hi) + fn_w16(out, start_p + 2 * i, s.lo) + fn_w16(out, delta_p + 2 * i, u16((u32(s.gid) + 0x10000 - u32(s.lo)) & 0xFFFF)) + } + fn_w16(out, end_p + 2 * (n - 1), 0xFFFF) + fn_w16(out, start_p + 2 * (n - 1), 0xFFFF) + fn_w16(out, delta_p + 2 * (n - 1), 1) + return out +} + +// fn_cmap0 is the 262-byte byte-encoding table. +@(private = "file") +fn_cmap0 :: proc(doc: ^Document, pairs: ..[2]u8) -> []byte { + out := fn_alloc(doc, 262) + fn_w16(out, 2, 262) + for pr in pairs { + out[6 + int(pr[0])] = pr[1] + } + return out +} + +// fn_post2 writes a version 2.0 `post` table naming every glyph from its own +// string pool, so none of the names collide with the standard Macintosh set. +@(private = "file") +fn_post2 :: proc(doc: ^Document, names: []string) -> []byte { + pool := 0 + for nm in names { + pool += 1 + len(nm) + } + out := fn_alloc(doc, 32 + 2 + 2 * len(names) + pool) + fn_w32(out, 0, 0x00020000) + fn_w16(out, 32, u16(len(names))) + p := 34 + 2 * len(names) + for nm, i in names { + fn_w16(out, 34 + 2 * i, u16(258 + i)) + out[p] = u8(len(nm)) + copy(out[p + 1:], nm) + p += 1 + len(nm) + } + return out +} + +// A blank glyph: zero contours, so `loca` can point at it and `glyf` is +// non-empty, which is what makes tt_has_glyf true. +@(private = "file") +FN_GLYF_BLANK := [?]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +// fn_ttf assembles a TrueType font. `cmaps` may be empty, in which case the +// font has no `cmap` table at all. +@(private = "file") +fn_ttf :: proc(doc: ^Document, num_glyphs: int, upem: u16, cmaps: []Fn_Cmap_Rec, + post: []byte = nil, advances: []u16 = nil) -> []byte { + tables := make([dynamic]Fn_Table, 0, 8, doc.allocator) + append(&tables, Fn_Table{"head", fn_head(doc, upem)}) + append(&tables, Fn_Table{"maxp", fn_maxp(doc, u16(num_glyphs))}) + // Every glyph is the same blank, so `loca` repeats one offset pair. + loca := fn_alloc(doc, 2 * (num_glyphs + 1)) + for i in 0 ..= num_glyphs { + fn_w16(loca, 2 * i, u16(min(i, 1) * (len(FN_GLYF_BLANK) / 2))) + } + append(&tables, Fn_Table{"loca", loca}) + append(&tables, Fn_Table{"glyf", FN_GLYF_BLANK[:]}) + if len(cmaps) > 0 { + append(&tables, Fn_Table{"cmap", fn_cmap(doc, ..cmaps)}) + } + if len(post) > 0 { + append(&tables, Fn_Table{"post", post}) + } + if len(advances) > 0 { + append(&tables, Fn_Table{"hhea", fn_hhea(doc, u16(len(advances)))}) + append(&tables, Fn_Table{"hmtx", fn_hmtx(doc, advances)}) + } + return fn_sfnt(doc, tables[:]) +} + +// --------------------------------------------------------------------------- +// A minimal CFF +// +// Header, Name INDEX, Top DICT INDEX, an empty String and GSubr INDEX, a +// format 0 charset and a CharStrings INDEX. Every Top DICT offset is written +// in the five-byte integer form so that the dictionary's length does not +// depend on the values in it, which is what lets the layout be computed in one +// pass. +// --------------------------------------------------------------------------- + +// FN_CS is one charstring: "400 50 50 rmoveto endchar". rmoveto takes two +// operands, so the third is the glyph's width (T2 3.1) -- 400, since +// nominalWidthX defaults to zero. Six bytes, and it draws a single point. +@(private = "file") +FN_CS := [?]u8{0xF8, 0x24, 0xBD, 0xBD, 0x15, 0x0E} + +// FN_CFF_MATRIX is a Top DICT FontMatrix of [0.0005 0 0 0.0005 0 0]: 2000 +// units per em rather than the CFF default of 1000. Every operand is written +// in a fixed-width form -- the nibble-packed real of CFF Table 5 for the two +// scales, the five-byte integer for the four zeros -- so that the dictionary's +// length does not depend on its contents. +@(private = "file") +FN_CFF_MATRIX := [?]u8{ + 0x1E, 0x0A, 0x00, 0x05, 0xFF, // 0.0005 + 0x1D, 0x00, 0x00, 0x00, 0x00, // 0 + 0x1D, 0x00, 0x00, 0x00, 0x00, // 0 + 0x1E, 0x0A, 0x00, 0x05, 0xFF, // 0.0005 + 0x1D, 0x00, 0x00, 0x00, 0x00, // 0 + 0x1D, 0x00, 0x00, 0x00, 0x00, // 0 + 0x0C, 0x07, // FontMatrix +} + +@(private = "file") +fn_cff :: proc(doc: ^Document, ids: []u16, is_cid: bool, own_matrix := false) -> []byte { + ng := len(ids) + 1 // .notdef plus one per id + dict_len := 12 + (17 if is_cid else 0) + (len(FN_CFF_MATRIX) if own_matrix else 0) + charset_off := 4 + 6 + (5 + dict_len) + 2 + 2 + charset_len := 1 + 2 * len(ids) + cs_off := charset_off + charset_len + cs_len := 3 + (ng + 1) + len(FN_CS) * ng + out := fn_alloc(doc, cs_off + cs_len) + + out[0], out[1], out[2], out[3] = 1, 0, 4, 1 // header + + // Name INDEX: one name, "T". + p := 4 + fn_w16(out, p, 1) + out[p + 2] = 1 + out[p + 3] = 1 + out[p + 4] = 2 + out[p + 5] = 'T' + p += 6 + + // Top DICT INDEX. + fn_w16(out, p, 1) + out[p + 2] = 1 + out[p + 3] = 1 + out[p + 4] = u8(1 + dict_len) + d := p + 5 + if is_cid { + // ROS: two SIDs and a supplement, then the operator. Its presence is + // the whole of what makes a CFF CID-keyed. + for k in 0 ..< 3 { + out[d + 5 * k] = 29 + } + out[d + 15], out[d + 16] = 12, 30 + d += 17 + } + if own_matrix { + copy(out[d:], FN_CFF_MATRIX[:]) + d += len(FN_CFF_MATRIX) + } + out[d] = 29 + fn_w32(out, d + 1, u32(charset_off)) + out[d + 5] = 15 // charset + out[d + 6] = 29 + fn_w32(out, d + 7, u32(cs_off)) + out[d + 11] = 17 // CharStrings + p += 5 + dict_len + + fn_w16(out, p, 0) // String INDEX, empty + fn_w16(out, p + 2, 0) // Global Subr INDEX, empty + + // charset, format 0: one SID (or CID) per glyph from glyph 1 up. + out[charset_off] = 0 + for id, i in ids { + fn_w16(out, charset_off + 1 + 2 * i, id) + } + + // CharStrings INDEX. + fn_w16(out, cs_off, u16(ng)) + out[cs_off + 2] = 1 + for i in 0 ..= ng { + out[cs_off + 3 + i] = u8(1 + i * len(FN_CS)) + } + q := cs_off + 3 + ng + 1 + for _ in 0 ..< ng { + copy(out[q:], FN_CS[:]) + q += len(FN_CS) + } + return out +} + +// --------------------------------------------------------------------------- +// Font fixtures +// --------------------------------------------------------------------------- + +@(private = "file") +fn_dict :: proc(doc: ^Document, src: string) -> ^Dict { + d, _ := to_dict(cf_parse(doc, src)) + return d +} + +@(private = "file") +fn_load :: proc(t: ^testing.T, doc: ^Document, d: ^Dict, loc := #caller_location) -> ^Font { + f, err := font_load(doc, d) + testing.expectf(t, err == .None, "font_load: %v", err, loc = loc) + testing.expect(t, f != nil, "font_load returned no font", loc = loc) + return f +} + +@(private = "file") +fn_font :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) -> ^Font { + return fn_load(t, doc, fn_dict(doc, src), loc) +} + +// fn_simple builds a simple font whose descriptor carries `flags` and the +// program `prog` under `key`, which is how every embedded-program test here is +// set up. +@(private = "file") +fn_simple :: proc(t: ^testing.T, doc: ^Document, font_src, desc_src, key: string, + prog: []byte, loc := #caller_location) -> ^Font { + d := fn_dict(doc, font_src) + desc := fn_dict(doc, desc_src) + if prog != nil { + dict_set(desc, key, cf_stream(doc, "<< >>", prog)) + } + dict_set(d, "FontDescriptor", desc) + return fn_load(t, doc, d, loc) +} + +@(private = "file") +fn_expect_width :: proc(t: ^testing.T, f: ^Font, code: int, want: f64, what: string, + loc := #caller_location) { + if f == nil || code < 0 || code > 255 { + testing.expectf(t, false, "%s: no font", what, loc = loc) + return + } + got := f.code_width[code] + testing.expectf(t, got == want, "%s: width of code %d is %v, wanted %v", + what, code, got, want, loc = loc) +} + +@(private = "file") +fn_expect_gid :: proc(t: ^testing.T, f: ^Font, code: int, want: u32, what: string, + loc := #caller_location) { + if f == nil || code < 0 || code > 255 { + testing.expectf(t, false, "%s: no font", what, loc = loc) + return + } + got := f.code_gid[code] + testing.expectf(t, got == want, "%s: gid for code %d is %d, wanted %d", + what, code, got, want, loc = loc) +} + +// fn_decode runs a string through the font and hands back the codes. The +// dynamic array is the caller's to delete. +@(private = "file") +fn_decode :: proc(f: ^Font, s: string) -> [dynamic]Font_Code { + out := make([dynamic]Font_Code, 0, 8) + font_decode(f, transmute([]byte)s, &out) + return out +} + +// --------------------------------------------------------------------------- +// Standard 14 metrics +// --------------------------------------------------------------------------- + +@(test) +test_font_std14_widths :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No /Widths, no /FontDescriptor: 9.6.2.2 lets a standard font omit both, + // and the AFM metrics are then the only widths there are. + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + if f == nil { return } + testing.expect(t, f.std_index == STD_HELVETICA, "Helvetica was not recognised") + testing.expect(t, !f.symbolic, "Helvetica is not a symbolic font") + + fn_expect_width(t, f, 'A', 667, "Helvetica A") + fn_expect_width(t, f, ' ', 278, "Helvetica space") + fn_expect_width(t, f, 'W', 944, "Helvetica W") + // StandardEncoding is what a font with no /Encoding falls back to. + testing.expect_value(t, f.enc_names['A'], "A") + testing.expect_value(t, f.enc_names[0xA5], "yen") + fn_expect_width(t, f, 0xA5, 556, "Helvetica yen") +} + +@(test) +test_font_std14_subset_tag_and_alias :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A subset tag and a Monotype alias both have to resolve, or a font that + // is Times in everything but spelling loses every width it has. + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Times-Roman >>") + if f == nil { return } + testing.expect(t, f.std_index == STD_TIMES_ROMAN, "subset tag was not stripped") + fn_expect_width(t, f, 'A', 722, "Times A") + + g := fn_font(t, doc, "<< /Type /Font /Subtype /TrueType /BaseFont /Arial,Bold >>") + if g == nil { return } + testing.expect(t, g.std_index == STD_HELVETICA_BOLD, "Arial,Bold is Helvetica-Bold") +} + +@(test) +test_font_std14_symbol_builtin_encoding :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Symbol's built-in encoding is its own, not StandardEncoding: code 0x41 + // is Alpha, and its width is Alpha's. + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol >>") + if f == nil { return } + testing.expect(t, f.symbolic, "Symbol is a symbolic font") + testing.expect_value(t, f.enc_names[0x41], "Alpha") + fn_expect_width(t, f, 0x41, 722, "Symbol Alpha") + fn_expect_width(t, f, ' ', 250, "Symbol space") +} + +@(test) +test_font_widths_beat_metrics :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Widths is the file speaking about its own font and wins over the AFM, + // even for a standard 14 name. Codes outside /FirstChar../LastChar fall + // through to the metrics. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica " + + "/FirstChar 65 /LastChar 67 /Widths [100 200 300] >>") + if f == nil { return } + fn_expect_width(t, f, 'A', 100, "widths[0]") + fn_expect_width(t, f, 'B', 200, "widths[1]") + fn_expect_width(t, f, 'C', 300, "widths[2]") + fn_expect_width(t, f, 'D', 722, "past LastChar falls back to the AFM") +} + +@(test) +test_font_missing_width :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A font that is not one of the fourteen has no metrics to fall back on, + // so /MissingWidth is what an unlisted code is worth. + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Whatever " + + "/FirstChar 65 /LastChar 65 /Widths [400] >>", + "<< /Flags 32 /MissingWidth 555 >>", "", nil) + if f == nil { return } + fn_expect_width(t, f, 'A', 400, "listed code") + fn_expect_width(t, f, 'B', 555, "unlisted code") + fn_expect_width(t, f, 0, 555, "code 0") + + // With no /MissingWidth the answer is zero, not a guess. + g := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Whatever " + + "/FirstChar 65 /LastChar 65 /Widths [400] >>", + "<< /Flags 32 >>", "", nil) + if g == nil { return } + fn_expect_width(t, g, 'B', 0, "no MissingWidth") +} + +// --------------------------------------------------------------------------- +// Encodings +// --------------------------------------------------------------------------- + +@(test) +test_font_base_encoding_and_differences :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /BaseEncoding replaces the built-in encoding wholesale; /Differences + // then overrides individual codes of it. Both have to be visible in the + // result, so the assertions below check a code from each. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " + + "<< /BaseEncoding /WinAnsiEncoding /Differences [65 /bullet /Euro] >> >>") + if f == nil { return } + testing.expect_value(t, f.enc_names[65], "bullet") + testing.expect_value(t, f.enc_names[66], "Euro") + testing.expect_value(t, f.enc_names[67], "C") // WinAnsi, untouched + testing.expect_value(t, f.enc_names[0xA5], "yen") // WinAnsi 0xA5 + fn_expect_width(t, f, 65, 350, "bullet") + fn_expect_width(t, f, 66, 556, "Euro") + fn_expect_width(t, f, 67, 722, "C") +} + +@(test) +test_font_encoding_name_form :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Encoding as a bare name. MacRoman and WinAnsi disagree about 0xA5, + // which is what makes this assertion able to tell them apart. + mac := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /MacRomanEncoding >>") + if mac == nil { return } + testing.expect_value(t, mac.enc_names[0xA5], "bullet") + fn_expect_width(t, mac, 0xA5, 350, "MacRoman 0xA5") + + win := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>") + if win == nil { return } + testing.expect_value(t, win.enc_names[0xA5], "yen") +} + +@(test) +test_font_unknown_encoding_name_keeps_builtin :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // An /Encoding this library does not know is not a reason to reset the + // font to StandardEncoding -- for Symbol that would replace every glyph + // name with a Latin one the font does not have. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol /Encoding /PDFDocEncoding >>") + if f == nil { return } + testing.expect_value(t, f.enc_names[0x41], "Alpha") + + // A /Differences over the same unknown name still applies. + g := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol /Encoding " + + "<< /BaseEncoding /Nonsense /Differences [65 /beta] >> >>") + if g == nil { return } + testing.expect_value(t, g.enc_names[0x41], "beta") + testing.expect_value(t, g.enc_names[0x42], "Beta") +} + +@(test) +test_font_differences_malformed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Codes outside 0..255, a real where an integer belongs, and a trailing + // name with no code before it. None of them may write out of bounds, and + // the entries around them still have to land. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " + + "<< /Differences [-3 /A 300 /B 66.0 /X /Y (str) /Z] >> >>") + if f == nil { return } + testing.expect_value(t, f.enc_names[66], "X") + testing.expect_value(t, f.enc_names[67], "Y") + testing.expect_value(t, f.enc_names[68], "Z") +} + +// --------------------------------------------------------------------------- +// TrueType: the symbolic rules (9.6.5.4) +// --------------------------------------------------------------------------- + +@(test) +test_font_symbolic_truetype_symbol_cmap :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A (3,0) table mapping the bare byte 0x42 and the private-use 0xF041. + // Both conventions occur, so both codes have to resolve. + sym := fn_cmap4(doc, Fn_Seg{0x42, 0x42, 4}, Fn_Seg{0xF041, 0xF041, 3}) + prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{3, 0, sym}}) + + // The /Encoding is present AND deliberately wrong: a symbolic font's + // codes do not go through it. If it were consulted, code 0x41 would look + // for "A" in a font whose only cmap is (3,0) and find nothing. + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Wingdings " + + "/Encoding /WinAnsiEncoding >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, f.symbolic, "the descriptor said symbolic") + testing.expect(t, f.embedded && !f.program_failed, "the program should have parsed") + fn_expect_gid(t, f, 0x42, 4, "bare code through the (3,0) cmap") + fn_expect_gid(t, f, 0x41, 3, "0xF000 + code through the (3,0) cmap") + fn_expect_gid(t, f, 0x43, 0, "a code the symbol cmap does not have") +} + +@(test) +test_font_symbolic_truetype_mac_fallback :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No (3,0) at all: a symbolic font then goes through the (1,0) Mac Roman + // table with the raw code, which is the other half of the rule. + prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{1, 0, fn_cmap0(doc, {0x41, 7}, {0xA5, 8})}}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Weird >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_gid(t, f, 0x41, 7, "(1,0) Mac Roman") + fn_expect_gid(t, f, 0xA5, 8, "(1,0) Mac Roman, high byte") + fn_expect_gid(t, f, 0x42, 0, "not in the Mac table") +} + +@(test) +test_font_symbolic_truetype_symbol_beats_mac :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Both tables present and deliberately disagreeing. 9.6.5.4 orders them: + // the (3,0) symbol table is consulted first and the (1,0) Mac Roman one + // only answers what it left. A font carrying both is the ordinary shape + // of a Windows-authored symbol font, and reading them the other way round + // silently swaps every glyph on the page for a different one -- the sort + // of failure that renders and still looks wrong. + sym := fn_cmap4(doc, Fn_Seg{0x41, 0x41, 5}, Fn_Seg{0xF042, 0xF042, 6}) + mac := fn_cmap0(doc, {0x41, 7}, {0x42, 8}, {0x43, 9}) + prog := fn_ttf(doc, 12, 1000, []Fn_Cmap_Rec{{3, 0, sym}, {1, 0, mac}}) + + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Both >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, f.symbolic, "the descriptor said symbolic") + fn_expect_gid(t, f, 0x41, 5, "the bare code in (3,0) beats (1,0)") + fn_expect_gid(t, f, 0x42, 6, "0xF000 + code in (3,0) also beats (1,0)") + fn_expect_gid(t, f, 0x43, 9, "and (1,0) still answers what (3,0) does not") +} + +@(test) +test_font_nonsymbolic_truetype_name_to_unicode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The non-symbolic route: code -> name (through the encoding) -> Unicode + // (through the AGL) -> glyph (through the (3,1) cmap). /Differences puts + // "C" on code 65, so 65 must reach C's glyph and not A's. + uni := fn_cmap4(doc, Fn_Seg{'A', 'C', 1}) // A->1, B->2, C->3 + prog := fn_ttf(doc, 10, 1000, []Fn_Cmap_Rec{{3, 1, uni}}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Plain /Encoding " + + "<< /BaseEncoding /WinAnsiEncoding /Differences [65 /C] >> >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, !f.symbolic, "flag 32 is nonsymbolic") + fn_expect_gid(t, f, 'A', 3, "/Differences put C on code 65") + fn_expect_gid(t, f, 'B', 2, "B through WinAnsi and the (3,1) cmap") + fn_expect_gid(t, f, 'C', 3, "C likewise") + fn_expect_gid(t, f, 'D', 0, "the cmap has no D") +} + +@(test) +test_font_nonsymbolic_truetype_post_names :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A subsetter's names are not in the AGL, so the Unicode route fails and + // the `post` table is the only thing left that knows what "g99" is. + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + post := fn_post2(doc, []string{".notdef", "one", "g99", "three"}) + prog := fn_ttf(doc, 4, 1000, []Fn_Cmap_Rec{{3, 1, uni}}, post) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Sub /Encoding " + + "<< /Differences [65 /g99] >> >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_gid(t, f, 'A', 2, "the post table names glyph 2 g99") +} + +@(test) +test_font_nonsymbolic_truetype_name_through_macroman :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 9.6.5.4's third route, and the only one this font has: one cmap, (1,0), + // and no `post` names, which is the shape of anything authored on a Mac + // or run through a subsetter. The name has to be carried BACK through + // MacRomanEncoding to get the code that table is keyed by. WinAnsi puts + // "eacute" on 0xE9 and MacRomanEncoding puts it on 0x8E, so the glyph + // wanted is the one at 0x8E; looking the raw 0xE9 up in the same table + // finds MacRoman's "Egrave" instead -- a real glyph, the wrong letter, + // and a page that renders and is silently wrong. + mac := fn_cmap0(doc, {0x8E, 1}, {0xE9, 2}, {0x41, 3}) + prog := fn_ttf(doc, 4, 1000, []Fn_Cmap_Rec{{1, 0, mac}}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /MacOnly " + + "/Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, !f.symbolic, "flag 32 is nonsymbolic") + fn_expect_gid(t, f, 0xE9, 1, "WinAnsi eacute through MacRoman's 0x8E") + // The other direction, so that a fix which merely offsets the code cannot + // pass: WinAnsi 0xC8 is "Egrave", which MacRomanEncoding puts on 0xE9. + fn_expect_gid(t, f, 0xC8, 2, "WinAnsi Egrave through MacRoman's 0xE9") + fn_expect_gid(t, f, 0x41, 3, "ASCII, where the two encodings agree") + fn_expect_gid(t, f, 0xEA, 0, "a code neither route reaches") +} + +@(test) +test_font_truetype_no_cmap_is_gid :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A stripped subset with no cmap at all. Every viewer reads the code as a + // glyph index here, and there is nothing else it could mean. + prog := fn_ttf(doc, 6, 1000, nil) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Sub /Encoding /WinAnsiEncoding >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_gid(t, f, 3, 3, "code as glyph index") + fn_expect_gid(t, f, 5, 5, "code as glyph index") + fn_expect_gid(t, f, 6, 0, "past the glyph count there is no glyph") + fn_expect_gid(t, f, 200, 0, "far past the glyph count") +} + +@(test) +test_font_truetype_units_per_em :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Glyph space for a TrueType font is 1/unitsPerEm, not 1/1000: a 2048-unit + // font drawn through a thousandths matrix comes out twice too large. + prog := fn_ttf(doc, 4, 2048, nil) + f := fn_simple(t, doc, "<< /Type /Font /Subtype /TrueType /BaseFont /Big >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + m := font_matrix(f) + testing.expect_value(t, m.a, 1.0 / 2048.0) + testing.expect_value(t, m.d, 1.0 / 2048.0) + testing.expect(t, !font_is_type3(f), "a TrueType font is not Type3") +} + +@(test) +test_font_truetype_hmtx_advance :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No /Widths and no /MissingWidth: the program's own advances are the + // last thing left, and they have to be scaled out of font units into the + // thousandths every PDF width is in. 1024/2048 of an em is 500. + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + prog := fn_ttf(doc, 3, 2048, []Fn_Cmap_Rec{{3, 1, uni}}, nil, []u16{0, 1024, 512}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /NoWidths /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_gid(t, f, 'A', 1, "A is glyph 1") + fn_expect_width(t, f, 'A', 500, "hmtx advance scaled to 1/1000") +} + +// --------------------------------------------------------------------------- +// CFF programs +// --------------------------------------------------------------------------- + +@(test) +test_font_cff_simple_by_name :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A name-keyed Type1C: SIDs 34, 35 and 36 are "A", "B" and "C" among the + // CFF standard strings, so the charset names glyphs 1..3 after them and + // the encoding's names are what find them. + prog := fn_cff(doc, []u16{34, 35, 36}, false) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Emb /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile3", prog) + if f == nil { return } + testing.expect(t, f.cff != nil, "the CFF should have parsed") + testing.expect(t, !f.program_failed, "no failure expected") + fn_expect_gid(t, f, 'A', 1, "charset SID 34 is A") + fn_expect_gid(t, f, 'C', 3, "charset SID 36 is C") + fn_expect_gid(t, f, 'D', 0, "the font has no D") + + // A CFF's own FontMatrix is the glyph space, and this one has none, so it + // is the 1/1000 default rather than anything the sfnt would have said. + testing.expect_value(t, font_matrix(f).a, 0.001) + + p := path_make() + defer path_destroy(&p) + err := font_glyph_path(f, 1, &p) + testing.expectf(t, err == .None, "font_glyph_path: %v", err) + testing.expect(t, !path_is_empty(&p), "the charstring draws a point") +} + +@(test) +test_font_cff_cid_charset :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A CID-keyed CFF: its charset holds CIDs, not name SIDs, so the CID a + // string decodes to has to be looked up in it rather than used as a glyph + // index. Glyph 2 is CID 20 here, and reading CID 20 as a GID would draw + // glyph 20, which does not exist. + prog := fn_cff(doc, []u16{10, 20, 30}, true) + cid := fn_dict(doc, "<< /Type /Font /Subtype /CIDFontType0 /DW 600 >>") + desc := fn_dict(doc, "<< /Flags 4 >>") + dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /CIDFontType0C >>", prog)) + dict_set(cid, "FontDescriptor", desc) + f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid) + if f == nil { return } + testing.expect(t, f.cff != nil && f.cff.is_cid, "a CID-keyed CFF was expected") + + codes := fn_decode(f, "\x00\x14\x00\x0a\x00\x63") + defer delete(codes) + if !testing.expect(t, len(codes) == 3, "three two-byte codes") { return } + testing.expect_value(t, codes[0].cid, 20) + testing.expect_value(t, codes[0].gid, 2) + testing.expect_value(t, codes[1].cid, 10) + testing.expect_value(t, codes[1].gid, 1) + testing.expect_value(t, codes[2].gid, 0) // CID 99 is in no charset entry +} + +@(test) +test_font_opentype_cff :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /FontFile3 /OpenType with CFF outlines: an sfnt wrapper with a `CFF ` + // table and no `glyf`. Both readers stay live -- the wrapper answers + // character lookups and the CFF draws -- so the font must end up with the + // CFF's matrix and a working glyph path. + cff := fn_cff(doc, []u16{34, 35}, false) + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + otf := fn_sfnt(doc, []Fn_Table{ + {"head", fn_head(doc, 2048)}, + {"maxp", fn_maxp(doc, 3)}, + {"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})}, + {"CFF ", cff}, + }, 0x4F54544F) + + d := fn_dict(doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Otf /Encoding /WinAnsiEncoding >>") + desc := fn_dict(doc, "<< /Flags 32 >>") + dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /OpenType >>", otf)) + dict_set(d, "FontDescriptor", desc) + f := fn_load(t, doc, d) + if f == nil { return } + testing.expect(t, f.ttf != nil, "the sfnt wrapper should have parsed") + testing.expect(t, f.cff != nil, "its CFF table should have parsed too") + testing.expect(t, !f.program_failed, "no failure expected") + // 1/2048 would be the sfnt's answer; the CFF's own matrix is the right one. + testing.expect_value(t, font_matrix(f).a, 0.001) + fn_expect_gid(t, f, 'A', 1, "through the sfnt cmap") + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None) + testing.expect(t, !path_is_empty(&p), "the CFF charstring draws") +} + +@(test) +test_font_program_sniffed_not_declared :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A bare CFF filed under /FontFile2, which says TrueType. The bytes are + // unambiguous and the dictionary is wrong; producers do this and every + // viewer believes the bytes. + prog := fn_cff(doc, []u16{34}, false) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Lying /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, f.cff != nil, "the CFF should have been recognised by its header") + testing.expect(t, f.ttf == nil, "it is not a TrueType font") + fn_expect_gid(t, f, 'A', 1, "and its glyph is still reachable") +} + +// --------------------------------------------------------------------------- +// A program that will not parse +// --------------------------------------------------------------------------- + +@(test) +test_font_corrupt_program_keeps_widths :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The rule the whole file is built around: a font program that cannot be + // read costs the glyphs and nothing else. The widths, the encoding and the + // Unicode values all still have to come out, because the page still has to + // lay out and extract. + junk := fn_alloc(doc, 64) + for i in 0 ..< len(junk) { + junk[i] = u8(i * 7 + 3) + } + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Broken /FirstChar 65 " + + "/LastChar 66 /Widths [321 654] /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 /MissingWidth 7 >>", "FontFile2", junk) + if f == nil { return } + testing.expect(t, f.program_failed, "program_failed must be set") + testing.expect(t, !f.embedded, "nothing was embedded that could be read") + testing.expect(t, f.ttf == nil && f.cff == nil && f.t1 == nil, "no reader should be live") + fn_expect_width(t, f, 'A', 321, "widths survive a broken program") + fn_expect_width(t, f, 'B', 654, "widths survive a broken program") + fn_expect_width(t, f, 'C', 7, "and so does /MissingWidth") + testing.expect_value(t, f.enc_names['A'], "A") + + codes := fn_decode(f, "AB") + defer delete(codes) + if !testing.expect(t, len(codes) == 2, "two codes") { return } + testing.expect_value(t, codes[0].unicode, 'A') + testing.expect_value(t, codes[0].width, 321) + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(f, 1, &p), Error.No_Glyph) + testing.expect(t, path_is_empty(&p), "nothing may be drawn") + testing.expect(t, doc.is_damaged, "a repair should have been recorded") +} + +@(test) +test_font_empty_program :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Flags 32 >>", "FontFile", nil) + if f == nil { return } + // No /FontFile at all is not a failure: it is a non-embedded font, and + // the flag has to distinguish the two or a caller cannot tell a missing + // program from a broken one. + testing.expect(t, !f.program_failed, "no program is not a failed program") + testing.expect(t, !f.embedded, "and it is not embedded either") + fn_expect_width(t, f, 'A', 667, "the AFM still measures it") + + // A /FontFile that is present but decodes to nothing is a different case: + // the file said there was a program and there is not. + gd := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + gdesc := fn_dict(doc, "<< /Flags 32 >>") + dict_set(gdesc, "FontFile", cf_stream(doc, "<< >>", nil)) + dict_set(gd, "FontDescriptor", gdesc) + g := fn_load(t, doc, gd) + if g == nil { return } + testing.expect(t, g.program_failed, "an empty program is a failed one") + fn_expect_width(t, g, 'A', 667, "and the metrics still measure it") +} + +// --------------------------------------------------------------------------- +// Composite fonts +// --------------------------------------------------------------------------- + +@(private = "file") +fn_type0 :: proc(t: ^testing.T, doc: ^Document, font_src: string, cid: ^Dict, + loc := #caller_location) -> ^Font { + d := fn_dict(doc, font_src) + a := new_array(doc.allocator) + array_push(a, cid) + dict_set(d, "DescendantFonts", a) + return fn_load(t, doc, d, loc) +} + +@(test) +test_font_type0_identity_h :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H " + + "/DescendantFonts [<< /Subtype /CIDFontType2 /DW 1000 >>] >>") + if f == nil { return } + testing.expect(t, f.kind == .Type0, "Type0") + testing.expect(t, !f.vertical, "Identity-H is horizontal") + + // Two bytes per code, big-endian, and the CID is the code. + codes := fn_decode(f, "\x00\x41\x01\x02\xff") + defer delete(codes) + if !testing.expect(t, len(codes) == 3, "two whole codes and a truncated one") { return } + testing.expect_value(t, codes[0].code, 0x0041) + testing.expect_value(t, codes[0].cid, 0x0041) + testing.expect_value(t, codes[0].gid, 0x0041) + testing.expect_value(t, codes[0].nbytes, 2) + testing.expect_value(t, codes[1].code, 0x0102) + testing.expect_value(t, codes[1].width, 1000) + // A trailing half-code still consumes the byte, or the caller loops. + testing.expect_value(t, codes[2].nbytes, 1) +} + +@(test) +test_font_type0_w_both_forms :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Both /W forms in one array: "c [w ...]" for CIDs 1 and 2, then + // "c_first c_last w" for 5..7. Everything else is /DW. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 1000 /W [1 [100 200] 5 7 300] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 1), 100) + testing.expect_value(t, font_cid_width(f, 2), 200) + testing.expect_value(t, font_cid_width(f, 3), 1000) + testing.expect_value(t, font_cid_width(f, 4), 1000) + testing.expect_value(t, font_cid_width(f, 5), 300) + testing.expect_value(t, font_cid_width(f, 6), 300) + testing.expect_value(t, font_cid_width(f, 7), 300) + testing.expect_value(t, font_cid_width(f, 8), 1000) + testing.expect_value(t, font_cid_width(f, 0), 1000) +} + +@(test) +test_font_type0_w_overlapping :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Overlapping ranges are malformed but occur: a broad run with a narrow + // correction written over part of it. The correction is the range that + // starts later, and it is the one that has to win. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 50 /W [1 10 300 4 6 900] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 3), 300) + testing.expect_value(t, font_cid_width(f, 4), 900) + testing.expect_value(t, font_cid_width(f, 6), 900) + testing.expect_value(t, font_cid_width(f, 7), 300) + testing.expect_value(t, font_cid_width(f, 11), 50) +} + +@(test) +test_font_type0_w_malformed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A name where a number belongs, a range that runs backwards, and a CID + // past the 16-bit ceiling. None may be fatal and none may swallow the + // entries after them. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 7 /W [/Bogus 20 18 400 99999 [1] 30 [600]] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 19), 400) + testing.expect_value(t, font_cid_width(f, 30), 600) + testing.expect_value(t, font_cid_width(f, 31), 7) +} + +@(test) +test_font_type0_cid_to_gid_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /CIDToGIDMap as a stream of big-endian u16 indexed by CID. + m := fn_alloc(doc, 6) + fn_w16(m, 0, 0) + fn_w16(m, 2, 5) + fn_w16(m, 4, 7) + cid := fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 1000 >>") + dict_set(cid, "CIDToGIDMap", cf_stream(doc, "<< >>", m)) + f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid) + if f == nil { return } + testing.expect_value(t, len(f.cid_to_gid), 3) + testing.expect_value(t, font_cid_gid(f, 0), 0) + testing.expect_value(t, font_cid_gid(f, 1), 5) + testing.expect_value(t, font_cid_gid(f, 2), 7) + // Past the end of the map the CID has no glyph -- the table is not an + // "identity from here on" promise. + testing.expect_value(t, font_cid_gid(f, 3), 0) + + codes := fn_decode(f, "\x00\x02") + defer delete(codes) + if !testing.expect(t, len(codes) == 1, "one code") { return } + testing.expect_value(t, codes[0].gid, 7) +} + +@(test) +test_font_type0_cid_to_gid_identity :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /CIDToGIDMap /Identity >>] >>") + if f == nil { return } + testing.expect_value(t, len(f.cid_to_gid), 0) + testing.expect_value(t, font_cid_gid(f, 1234), 1234) +} + +@(test) +test_font_type0_vertical :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Identity-V: the writing mode comes from the CMap, not from the font + // dictionary. With no /W2 the displacement is DW2[1] and the position + // vector is (w0/2, DW2[0]). + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 800 >>] >>") + if f == nil { return } + testing.expect(t, f.vertical, "Identity-V is vertical") + testing.expect_value(t, f.default_w1, -1000) + testing.expect_value(t, f.default_vy, 880) + + codes := fn_decode(f, "\x00\x41") + defer delete(codes) + if !testing.expect(t, len(codes) == 1, "one code") { return } + testing.expect_value(t, codes[0].width, -1000) + testing.expect_value(t, codes[0].vx, 400) + testing.expect_value(t, codes[0].vy, 880) +} + +@(test) +test_font_type0_w2 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /W2's two forms. Its entries are triples: w1y, then the position + // vector. /DW2 replaces the defaults for everything they do not cover. + // The two runs are written out of order, which /W2 is allowed to be and + // which a binary search over them is not. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 500 /DW2 [700 -900] " + + "/W2 [5 6 -820 120 920 1 [-800 100 900 -810 110 910]] >>] >>") + if f == nil { return } + testing.expect_value(t, f.default_vy, 700) + testing.expect_value(t, f.default_w1, -900) + + w, vx, vy := font_cid_vmetric(f, 1, 500) + testing.expect_value(t, w, -800) + testing.expect_value(t, vx, 100) + testing.expect_value(t, vy, 900) + + w, vx, vy = font_cid_vmetric(f, 2, 500) + testing.expect_value(t, w, -810) + testing.expect_value(t, vx, 110) + + w, vx, vy = font_cid_vmetric(f, 6, 500) + testing.expect_value(t, w, -820) + testing.expect_value(t, vx, 120) + testing.expect_value(t, vy, 920) + + // Uncovered: the /DW2 defaults, with the position vector at half the + // horizontal width. + w, vx, vy = font_cid_vmetric(f, 40, 500) + testing.expect_value(t, w, -900) + testing.expect_value(t, vx, 250) + testing.expect_value(t, vy, 700) +} + +@(test) +test_font_type0_embedded_cmap :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // An embedded CMap stream with a one-byte and a two-byte codespace. A + // composite font's codes are not all two bytes wide, and a decoder that + // assumes they are reads the rest of the string at the wrong offset. + src := `/CIDInit /ProcSet findresource begin +begincmap +2 begincodespacerange +<00> <7f> +<8140> <9ffc> +endcodespacerange +2 begincidrange +<20> <7e> 1 +<8140> <8145> 100 +endcidrange +endcmap +end` + d := fn_dict(doc, "<< /Type /Font /Subtype /Type0 /BaseFont /Mixed >>") + dict_set(d, "Encoding", cf_stream(doc, "<< >>", transmute([]byte)src)) + a := new_array(doc.allocator) + array_push(a, fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 1000 >>")) + dict_set(d, "DescendantFonts", a) + f := fn_load(t, doc, d) + if f == nil { return } + + codes := fn_decode(f, "A\x81\x42B") + defer delete(codes) + if !testing.expect(t, len(codes) == 3, "one byte, two bytes, one byte") { return } + testing.expect_value(t, codes[0].nbytes, 1) + testing.expect_value(t, codes[0].cid, 1 + 0x41 - 0x20) + testing.expect_value(t, codes[1].nbytes, 2) + testing.expect_value(t, codes[1].code, 0x8142) + testing.expect_value(t, codes[1].cid, 102) + testing.expect_value(t, codes[2].nbytes, 1) +} + +@(test) +test_font_type0_degrades :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No /DescendantFonts and no /Encoding. Neither is recoverable, but the + // font still has to decode text: /ToUnicode does not need either of them, + // and extraction is the last thing to give up. + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type0 /BaseFont /Nothing >>") + if f == nil { return } + testing.expect(t, f.cmap != nil, "Identity-H stands in for a missing /Encoding") + testing.expect(t, doc.is_damaged, "and the substitution is a repair") + + codes := fn_decode(f, "\x00\x41\x00\x42") + defer delete(codes) + testing.expect_value(t, len(codes), 2) +} + +@(test) +test_font_type0_predefined_fallback :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A CJK CMap this library does not ship. cmap.odin substitutes a two-byte + // identity and flags it; what matters here is that the font still + // segments its string two bytes at a time instead of one. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /UniJIS-UCS2-H /DescendantFonts " + + "[<< /Subtype /CIDFontType0 /DW 1000 >>] >>") + if f == nil { return } + testing.expect(t, f.cmap != nil && f.cmap.predefined_fallback, "the fallback flag") + codes := fn_decode(f, "\x30\x42\x30\x44") + defer delete(codes) + testing.expect_value(t, len(codes), 2) + testing.expect_value(t, codes[0].code, 0x3042) + + // A "-V" name is vertical even when the mapping itself is a substitute. + g := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /UniJIS-UCS2-V /DescendantFonts " + + "[<< /Subtype /CIDFontType0 >>] >>") + if g == nil { return } + testing.expect(t, g.vertical, "a -V CMap is vertical") +} + +// --------------------------------------------------------------------------- +// Type3 (9.6.4) +// --------------------------------------------------------------------------- + +@(test) +test_font_type3_matrix :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A Type3 font's /FontMatrix is its own and is NOT 1/1000. This one draws + // in hundredths, which is a scale factor of ten against the thousandths + // every other font uses: honouring it is the difference between text at + // the right size and text ten times too small. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " + + "/FontBBox [0 0 100 100] /FirstChar 97 /LastChar 98 /Widths [75 50] " + + "/CharProcs << >> /Encoding << /Differences [97 /square /tri] >> >>") + if f == nil { return } + testing.expect(t, font_is_type3(f), "font_is_type3") + m := font_matrix(f) + testing.expect_value(t, m.a, 0.01) + testing.expect_value(t, m.d, 0.01) + testing.expect_value(t, m.e, 0.0) + testing.expect_value(t, f.enc_names[97], "square") + testing.expect_value(t, f.enc_names[98], "tri") + // Widths are in that same glyph space, untouched: 75 of these units is + // 0.75 of a text space unit, which only the matrix can say. + fn_expect_width(t, f, 97, 75, "Type3 width") + fn_expect_width(t, f, 98, 50, "Type3 width") + fn_expect_width(t, f, 99, 0, "an unlisted Type3 code") + + // A Type3 glyph is a content stream, so there is no outline to hand back. + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(f, 97, &p), Error.No_Glyph) +} + +@(test) +test_font_type3_char_procs :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + procs := fn_dict(doc, "<< >>") + dict_set(procs, "square", cf_stream(doc, "<< >>", cf_bytes("0 0 10 10 re f"))) + d := fn_dict(doc, + "<< /Type /Font /Subtype /Type3 /FontMatrix [0.001 0 0 0.001 0 0] " + + "/Encoding << /Differences [65 /square] >> >>") + dict_set(d, "CharProcs", procs) + dict_set(d, "Resources", fn_dict(doc, "<< /ProcSet [/PDF] >>")) + f := fn_load(t, doc, d) + if f == nil { return } + + testing.expect(t, f.char_procs != nil, "/CharProcs") + testing.expect(t, f.t3_resources != nil, "/Resources") + // The interpreter finds a glyph by the NAME the encoding gives its code, + // which is the only route a Type3 font has. + testing.expect(t, font_type3_proc(f, 65) != nil, "code 65 is /square") + testing.expect(t, font_type3_proc(f, 66) == nil, "code 66 names nothing") + testing.expect(t, font_type3_proc(f, 9999) == nil, "an impossible code") + + // A Type3 font has no program to index, so the code stands in as the + // glyph id: the interpreter still has to carry something through to the + // device, and the code is the only identifier there is. + codes := fn_decode(f, "AB") + defer delete(codes) + if !testing.expect(t, len(codes) == 2, "two codes") { return } + testing.expect_value(t, codes[0].gid, 65) + testing.expect_value(t, codes[1].gid, 66) +} + +@(test) +test_font_type3_bad_matrix :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A singular /FontMatrix would collapse every glyph to a line. It is not + // recoverable as written, so the usual thousandths stand in and a repair + // is recorded. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type3 /FontMatrix [0 0 0 0 0 0] /CharProcs << >> >>") + if f == nil { return } + testing.expect_value(t, font_matrix(f).a, 0.001) + testing.expect(t, doc.is_damaged, "a repair should have been recorded") + + // A /FontMatrix that is too short is the same case. + g := fn_font(t, doc, + "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0] /CharProcs << >> >>") + if g == nil { return } + testing.expect_value(t, font_matrix(g).a, 0.001) +} + +@(test) +test_font_kind_guessed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A font dictionary with no /Subtype is malformed and perfectly readable: + // its other keys say what it is. + f := fn_font(t, doc, "<< /Type /Font /CharProcs << >> /FontMatrix [1 0 0 1 0 0] >>") + if f == nil { return } + testing.expect(t, f.kind == .Type3, "/CharProcs makes it Type3") + testing.expect_value(t, font_matrix(f).a, 1.0) + + g := fn_font(t, doc, + "<< /Type /Font /DescendantFonts [<< /Subtype /CIDFontType2 >>] >>") + if g == nil { return } + testing.expect(t, g.kind == .Type0, "/DescendantFonts makes it Type0") + + h := fn_font(t, doc, "<< /Type /Font /BaseFont /Helvetica >>") + if h == nil { return } + testing.expect(t, h.kind == .Type1, "and a simple font is the default") + fn_expect_width(t, h, 'A', 667, "which still measures") +} + +// --------------------------------------------------------------------------- +// Decoding +// --------------------------------------------------------------------------- + +@(test) +test_font_is_space :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Word spacing applies to a single-byte code 32 and to nothing else + // (9.3.3). A two-byte 0x0020 is an ordinary CID, and taking it for a + // space stretches every CJK line that happens to contain one. + simple := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + if simple == nil { return } + sc := fn_decode(simple, "A B") + defer delete(sc) + if !testing.expect(t, len(sc) == 3, "three codes") { return } + testing.expect(t, !sc[0].is_space, "A is not a space") + testing.expect(t, sc[1].is_space, "a one-byte 32 IS a space") + testing.expect(t, !sc[2].is_space, "B is not a space") + + comp := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 >>] >>") + if comp == nil { return } + cc := fn_decode(comp, "\x00\x20\x20\x20") + defer delete(cc) + if !testing.expect(t, len(cc) == 2, "two two-byte codes") { return } + testing.expect_value(t, cc[0].code, 0x0020) + testing.expect(t, !cc[0].is_space, "a two-byte 32 is NOT a space") + testing.expect(t, !cc[1].is_space, "nor is 0x2020") + + // The half of 9.3.3 that the rule is actually written for: word spacing + // applies to "a composite font that defines code 32 as a single-byte + // code" too. The test above passes just as well if the code keys on the + // font being composite; only this one pins it to the CODE's width, which + // is what the format says. A one-byte codespace is what every + // /Encoding-with-a-one-byte-range CMap gives, and a Type0 font over such + // a CMap has to justify its spaces like any other. + src := `begincmap +1 begincodespacerange +<00> +endcodespacerange +1 begincidrange +<00> 0 +endcidrange +endcmap` + d := fn_dict(doc, "<< /Type /Font /Subtype /Type0 /BaseFont /OneByte >>") + dict_set(d, "Encoding", cf_stream(doc, "<< >>", transmute([]byte)src)) + a := new_array(doc.allocator) + array_push(a, fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 500 >>")) + dict_set(d, "DescendantFonts", a) + one := fn_load(t, doc, d) + if one == nil { return } + oc := fn_decode(one, "A B") + defer delete(oc) + if !testing.expect(t, len(oc) == 3, "three one-byte codes") { return } + testing.expect_value(t, oc[1].nbytes, 1) + testing.expect(t, !oc[0].is_space, "A is not a space") + testing.expect(t, oc[1].is_space, "a ONE-byte 32 is a space even in a Type0 font") + testing.expect(t, !oc[2].is_space, "B is not a space") +} + +@(test) +test_font_decode_fills_everything :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /FirstChar 65 " + + "/LastChar 66 /Widths [123 456] >>") + if f == nil { return } + codes := fn_decode(f, "AB") + defer delete(codes) + if !testing.expect(t, len(codes) == 2, "two codes") { return } + testing.expect_value(t, codes[0].code, 'A') + testing.expect_value(t, codes[0].cid, 'A') + testing.expect_value(t, codes[0].nbytes, 1) + testing.expect_value(t, codes[0].width, 123) + testing.expect_value(t, codes[0].unicode, 'A') + testing.expect_value(t, codes[1].width, 456) + testing.expect_value(t, codes[1].unicode, 'B') +} + +@(test) +test_font_decode_clears_output :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + if f == nil { return } + out := make([dynamic]Font_Code, 0, 8) + defer delete(out) + + font_decode(f, cf_bytes("ABC"), &out) + testing.expect_value(t, len(out), 3) + // The buffer is meant to be reused across every string on a page, so the + // second call must not append to the first. + font_decode(f, cf_bytes("X"), &out) + testing.expect_value(t, len(out), 1) + testing.expect_value(t, out[0].code, 'X') + font_decode(f, nil, &out) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_font_to_unicode_wins :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /ToUnicode is the producer saying what it meant, and it beats the glyph + // name: a subset font's names are frequently lies and its /ToUnicode is + // the only thing that makes the text extractable. + tu := `begincmap +1 begincodespacerange <00> endcodespacerange +2 beginbfchar +<41> <2660> +<42> <0066006600690065> +endbfchar +endcmap` + d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + dict_set(d, "ToUnicode", cf_stream(doc, "<< >>", transmute([]byte)tu)) + f := fn_load(t, doc, d) + if f == nil { return } + testing.expect(t, f.to_unicode != nil, "/ToUnicode should have loaded") + + codes := fn_decode(f, "ABC") + defer delete(codes) + if !testing.expect(t, len(codes) == 3, "three codes") { return } + testing.expect_value(t, codes[0].unicode, '♠') + // A code that stands for several characters reports the first of them; + // the whole string is cmap_lookup_text's to give. + testing.expect_value(t, codes[1].unicode, 'f') + // A code /ToUnicode says nothing about falls back to the glyph name. + testing.expect_value(t, codes[2].unicode, 'C') +} + +@(test) +test_font_unicode_routes :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // With no /ToUnicode the glyph name is the route, through the AGL. A + // "uniXXXX" name works the same way, and a name that means nothing at all + // leaves the code with no character rather than a guessed one. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding " + + "<< /Differences [65 /Alpha 66 /uni0416 67 /g42] >> >>") + if f == nil { return } + codes := fn_decode(f, "ABC") + defer delete(codes) + if !testing.expect(t, len(codes) == 3, "three codes") { return } + testing.expect_value(t, codes[0].unicode, 'Α') + testing.expect_value(t, codes[1].unicode, 'Ж') + // "g42" is not a name the AGL knows. The code is a printable ASCII byte + // in a non-symbolic font, which is the last-resort reading. + testing.expect_value(t, codes[2].unicode, 'C') +} + +@(test) +test_font_symbolic_unicode_not_guessed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The ASCII last resort must not fire for a symbolic font: code 0x41 in + // a dingbat font is a dingbat, and calling it "A" puts nonsense into + // extracted text rather than leaving a gap that a reader can see. + sym := fn_cmap4(doc, Fn_Seg{0xF041, 0xF041, 3}) + prog := fn_ttf(doc, 6, 1000, []Fn_Cmap_Rec{{3, 0, sym}}) + // The name is deliberately not one std_font_lookup recognises: matching + // ZapfDingbats would hand the font that AFM's encoding and the code WOULD + // then have a name, which is a different test. + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Icons >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + codes := fn_decode(f, "A") + defer delete(codes) + if !testing.expect(t, len(codes) == 1, "one code") { return } + testing.expect_value(t, codes[0].gid, 3) + testing.expect_value(t, codes[0].unicode, rune(-1)) +} + +@(test) +test_font_unicode_from_post_names :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A symbolic font with no PDF encoding still has names in its `post` + // table, and they are the only thing that can make its text extractable. + sym := fn_cmap4(doc, Fn_Seg{0xF041, 0xF041, 2}) + post := fn_post2(doc, []string{".notdef", "one", "Sigma"}) + prog := fn_ttf(doc, 3, 1000, []Fn_Cmap_Rec{{3, 0, sym}}, post) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Sym >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + codes := fn_decode(f, "A") + defer delete(codes) + if !testing.expect(t, len(codes) == 1, "one code") { return } + testing.expect_value(t, codes[0].gid, 2) + testing.expect_value(t, codes[0].unicode, rune(0x03A3)) +} + +// --------------------------------------------------------------------------- +// Caching, arguments and clamping +// --------------------------------------------------------------------------- + +@(test) +test_font_cached_on_document :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A page that selects the same font two hundred times must parse its + // embedded program once, so the cache is keyed by the dictionary itself. + d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + a := fn_load(t, doc, d) + b := fn_load(t, doc, d) + testing.expect(t, a == b && a != nil, "the same dictionary must yield the same Font") + + other := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + c := fn_load(t, doc, other) + testing.expect(t, c != a, "a different dictionary is a different font") +} + +@(test) +test_font_nil_arguments :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + _, e1 := font_load(nil, nil) + testing.expect_value(t, e1, Error.Invalid_Argument) + _, e2 := font_load(doc, nil) + testing.expect_value(t, e2, Error.Invalid_Argument) + + testing.expect_value(t, font_matrix(nil).a, 0.001) + testing.expect(t, !font_is_type3(nil), "nil is not a Type3 font") + testing.expect(t, font_type3_proc(nil, 0) == nil, "nil has no char procs") + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(nil, 0, &p), Error.Invalid_Argument) + + f := fn_font(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + testing.expect_value(t, font_glyph_path(f, 0, nil), Error.Invalid_Argument) + // A glyph index wider than the u16 every program indexes with. + testing.expect_value(t, font_glyph_path(f, 0x10000, &p), Error.No_Glyph) + + // A nil output buffer is a caller mistake, not a crash. + font_decode(f, cf_bytes("A"), nil) + out := make([dynamic]Font_Code, 0, 4) + defer delete(out) + font_decode(nil, cf_bytes("A"), &out) + testing.expect_value(t, len(out), 0) +} + +@(test) +test_font_width_clamping :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A width from a file is attacker-controlled and ends up multiplied into + // the text matrix. Nothing below the device line may see an infinity, so + // an absurd width is clamped rather than carried. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Whatever /FirstChar 65 " + + "/LastChar 67 /Widths [1e300 -1e300 12] >>") + if f == nil { return } + testing.expect_value(t, f.code_width['A'], f64(MAX_GLYPH_WIDTH)) + testing.expect_value(t, f.code_width['B'], -f64(MAX_GLYPH_WIDTH)) + testing.expect_value(t, f.code_width['C'], 12) + testing.expect(t, is_finite(f.code_width['A']), "no infinity may survive") + + g := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 1e300 /W [1 1 -1e300] >>] >>") + if g == nil { return } + testing.expect_value(t, font_cid_width(g, 1), -f64(MAX_GLYPH_WIDTH)) + testing.expect_value(t, font_cid_width(g, 2), f64(MAX_GLYPH_WIDTH)) +} + +@(test) +test_font_descriptor_flags :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 9.8.2, table 121: FixedPitch is bit 1, Serif bit 2, Symbolic bit 3, + // Nonsymbolic bit 6, Italic bit 7. Producers set Symbolic and Nonsymbolic + // together often enough that the pair has to be read as a whole, and a + // font claiming both is claiming nothing. + f := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>", + "<< /Flags 67 >>", "", nil) // 1 | 2 | 64 + if f == nil { return } + testing.expect(t, f.fixed_pitch, "FixedPitch") + testing.expect(t, f.serif, "Serif") + testing.expect(t, f.italic, "Italic") + testing.expect(t, !f.symbolic, "not symbolic") + + g := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>", + "<< /Flags 4 >>", "", nil) + if g == nil { return } + testing.expect(t, g.symbolic, "Symbolic alone") + + h := fn_simple(t, doc, "<< /Type /Font /Subtype /Type1 /BaseFont /X >>", + "<< /Flags 36 >>", "", nil) // 4 | 32 + if h == nil { return } + testing.expect(t, !h.symbolic, "Symbolic and Nonsymbolic together is neither") +} + +@(test) +test_font_symbolic_truetype_unicode_only_cmap :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A subset that sets the Symbolic flag and then ships a (3,1) cmap and + // nothing else. It is one of the most common shapes there is -- TeX and + // several web-to-PDF tools emit it -- and both prescribed routes are dead + // ends for it: there is no (3,0), no (1,0), and no /Encoding to give the + // code a name. Reading the code as a character is what is left. + uni := fn_cmap4(doc, Fn_Seg{'A', 'C', 11}) + prog := fn_ttf(doc, 20, 1000, []Fn_Cmap_Rec{{3, 1, uni}}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Subset >>", + "<< /Flags 4 >>", "FontFile2", prog) + if f == nil { return } + testing.expect(t, f.symbolic, "the descriptor said symbolic") + fn_expect_gid(t, f, 'A', 11, "the code read as a character") + fn_expect_gid(t, f, 'C', 13, "likewise") + // The fallback is a last resort, not a licence: a code the cmap does not + // have is still nothing, and NOT a glyph index (the font has a cmap). + fn_expect_gid(t, f, 'D', 0, "no glyph, and not a glyph index either") +} + +@(test) +test_font_cid_keyed_cff_as_simple_font :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A CID-keyed CFF embedded under a /Type1 dictionary. It has no glyph + // names and no encoding of its own (CFF 12), so the name route and the + // built-in-encoding route both find nothing; reading the code as a CID + // through the charset is the only thing that recovers the glyphs, and + // without it the page comes out blank. + prog := fn_cff(doc, []u16{65, 66, 67}, true) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Odd /Encoding /WinAnsiEncoding >>", + "<< /Flags 4 >>", "FontFile3", prog) + if f == nil { return } + testing.expect(t, f.cff != nil && f.cff.is_cid, "a CID-keyed CFF") + fn_expect_gid(t, f, 'A', 1, "code 65 is CID 65, which the charset puts at glyph 1") + fn_expect_gid(t, f, 'C', 3, "code 67 likewise") + fn_expect_gid(t, f, 'D', 0, "and a code the charset has no CID for") +} + +@(test) +test_font_sfnt_under_a_type1c_subtype :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /FontFile3 with /Subtype /Type1C holding an sfnt-wrapped OpenType font. + // The dictionary is wrong and the four magic bytes are not: believing the + // subtype here would hand an sfnt to the CFF reader, which would reject + // it and lose a font that is perfectly readable. + cff := fn_cff(doc, []u16{34}, false) + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + otf := fn_sfnt(doc, []Fn_Table{ + {"head", fn_head(doc, 1000)}, + {"maxp", fn_maxp(doc, 2)}, + {"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})}, + {"CFF ", cff}, + }, 0x4F54544F) + + d := fn_dict(doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Mislabelled /Encoding /WinAnsiEncoding >>") + desc := fn_dict(doc, "<< /Flags 32 >>") + dict_set(desc, "FontFile3", cf_stream(doc, "<< /Subtype /Type1C >>", otf)) + dict_set(d, "FontDescriptor", desc) + f := fn_load(t, doc, d) + if f == nil { return } + testing.expect(t, f.ttf != nil, "the sfnt wrapper should have parsed") + testing.expect(t, f.cff != nil, "and so should its CFF table") + testing.expect(t, !f.program_failed, "nothing failed here") + fn_expect_gid(t, f, 'A', 1, "and the glyph is reachable") +} + +@(test) +test_font_missing_width_is_the_last_word :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /MissingWidth is consulted before the embedded program's own advances: + // a descriptor that carries one is the file being explicit about codes it + // did not list, and the program's hmtx is the guess made when it is not. + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + prog := fn_ttf(doc, 3, 1000, []Fn_Cmap_Rec{{3, 1, uni}}, nil, []u16{0, 900, 900}) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Explicit /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 /MissingWidth 42 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_width(t, f, 'A', 42, "/MissingWidth beats the hmtx advance") + + // Without one, the program's advance is what is left. + g := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Implicit /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile2", prog) + if g == nil { return } + fn_expect_width(t, g, 'A', 900, "and without one the hmtx advance is") +} + +// --------------------------------------------------------------------------- +// Type1 programs +// +// A Type1 font is a PostScript header, the token `eexec`, and an enciphered +// Private dictionary whose /CharStrings are themselves enciphered. Both +// ciphers are four lines each, so the font below is assembled rather than +// pasted in as a byte literal: what is under test is the dictionary's use of +// the program's built-in encoding, and a literal would hide which byte was +// which. +// --------------------------------------------------------------------------- + +// fn_t1_encrypt is the Type 1 cipher (Type 1 spec, 7.3), used with key 55665 +// for the eexec block and 4330 for a charstring. `pad` random bytes are +// prefixed to the plaintext, which is what the decipherer drops. +@(private = "file") +fn_t1_encrypt :: proc(doc: ^Document, plain: []byte, key: u16, pad: int) -> []byte { + out := fn_alloc(doc, len(plain) + pad) + r := u32(key) + for i in 0 ..< len(out) { + // A padding byte that is not a hex digit keeps the eexec block from + // being mistaken for the hex-ASCII form a .pfa uses. + p := u8(0x7A) if i < pad else plain[i - pad] + c := p ~ u8(r >> 8) + out[i] = c + r = ((u32(c) + r) * 52845 + 22719) & 0xFFFF + } + return out +} + +// FN_T1_CS is "0 500 hsbw 50 50 rmoveto closepath endchar": a glyph 500 units +// wide that draws a single point. Nine bytes, thirteen once the four bytes of +// /lenIV padding are enciphered in front of it. +@(private = "file") +FN_T1_CS := [?]u8{0x8B, 0xF8, 0x88, 0x0D, 0xBD, 0xBD, 0x15, 0x09, 0x0E} + +// fn_type1 builds a font program with three glyphs -- .notdef, Aacute and +// beta -- and a built-in encoding putting the last two on codes 65 and 66. +// Its /FontMatrix is 2000 units per em rather than the usual 1000, so a +// caller that assumes thousandths gets visibly wrong widths. +@(private = "file") +fn_type1 :: proc(doc: ^Document) -> []byte { + cs := fn_t1_encrypt(doc, FN_T1_CS[:], 4330, 4) + + priv := make([dynamic]u8, 0, 256, doc.allocator) + append_string(&priv, "dup /Private 8 dict dup begin\n/lenIV 4 def\nend\n") + append_string(&priv, "/CharStrings 3 dict dup begin\n") + for name in ([]string{"/.notdef ", "/Aacute ", "/beta "}) { + append_string(&priv, name, "13 RD ") + append(&priv, ..cs) + append_string(&priv, " ND\n") + } + append_string(&priv, "end\nend\n") + + body := fn_t1_encrypt(doc, priv[:], 55665, 4) + out := make([dynamic]u8, 0, 512, doc.allocator) + append_string(&out, + "%!PS-AdobeFont-1.0: Test 001.001\n" + + "/FontName /Test def\n" + + "/FontMatrix [0.0005 0 0 0.0005 0 0] readonly def\n" + + "/Encoding 256 array\n" + + "0 1 255 {1 index exch /.notdef put} for\n" + + "dup 65 /Aacute put\n" + + "dup 66 /beta put\n" + + "readonly def\n" + + "currentdict end\n" + + "currentfile eexec ") + append(&out, ..body) + append_string(&out, "\n0000000000000000\ncleartomark\n") + return out[:] +} + +@(test) +test_font_type1_builtin_encoding :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // With no /Encoding in the font dictionary, the program's own encoding is + // the encoding. A font that ignored it would put StandardEncoding's names + // on the codes and find neither of this font's two glyphs. + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Test >>", + "<< /Flags 4 >>", "FontFile", fn_type1(doc)) + if f == nil { return } + testing.expect(t, f.t1 != nil, "the Type1 program should have parsed") + testing.expect(t, !f.program_failed && f.embedded, "and it is embedded") + testing.expect_value(t, f.enc_names[65], "Aacute") + testing.expect_value(t, f.enc_names[66], "beta") + fn_expect_gid(t, f, 65, 1, "Aacute is the second charstring") + fn_expect_gid(t, f, 66, 2, "beta is the third") + + // 2000 units per em, and the program's own advances have to come back in + // the thousandths a PDF width is in: 500 of these units is a quarter em, + // which is 250 of those. + testing.expect_value(t, font_matrix(f).a, 0.0005) + fn_expect_width(t, f, 65, 250, "hsbw width through the FontMatrix") + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None) + testing.expect(t, !path_is_empty(&p), "the charstring draws a point") +} + +@(test) +test_font_type1_differences_over_builtin :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Differences is applied over the program's built-in encoding, not over + // StandardEncoding: code 65 becomes beta, and code 66 keeps the built-in + // name the program gave it. + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Test /Encoding " + + "<< /Differences [65 /beta] >> >>", + "<< /Flags 4 >>", "FontFile", fn_type1(doc)) + if f == nil { return } + testing.expect_value(t, f.enc_names[65], "beta") + testing.expect_value(t, f.enc_names[66], "beta") + fn_expect_gid(t, f, 65, 2, "/Differences moved beta onto code 65") + fn_expect_gid(t, f, 66, 2, "and the built-in encoding still names 66") + + // A /BaseEncoding replaces the built-in one wholesale, and this font has + // none of StandardEncoding's names, so nothing resolves. + g := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Test " + + "/Encoding /WinAnsiEncoding >>", + "<< /Flags 4 >>", "FontFile", fn_type1(doc)) + if g == nil { return } + testing.expect_value(t, g.enc_names[65], "A") + fn_expect_gid(t, g, 65, 0, "the font has no glyph called A") +} + +// FN_GLYF_SQUARE is one contour of four on-curve points at (0,0) (100,0) +// (100,100) (0,100). The same 22 bytes font_truetype_test.odin builds its +// outlines from; here they exist only so that there is a real outline to +// route a font_glyph_path call to. +@(private = "file") +FN_GLYF_SQUARE := [?]u8{ + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, + 0x00, 0x03, + 0x00, 0x00, + 0x31, 0x33, 0x35, 0x21, + 0x64, 0xFF, 0x9C, + 0x64, +} + +@(test) +test_font_glyph_path_truetype :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A font with real `glyf` outlines has to be drawn from them. The two + // readers a Font can hold are not interchangeable, and reaching for the + // wrong one draws nothing at all. + loca := fn_alloc(doc, 6) + fn_w16(loca, 0, 0) + fn_w16(loca, 2, 0) + fn_w16(loca, 4, u16(len(FN_GLYF_SQUARE) / 2)) // short loca stores halves + uni := fn_cmap4(doc, Fn_Seg{'A', 'A', 1}) + prog := fn_sfnt(doc, []Fn_Table{ + {"head", fn_head(doc, 1000)}, + {"maxp", fn_maxp(doc, 2)}, + {"loca", loca}, + {"glyf", FN_GLYF_SQUARE[:]}, + {"cmap", fn_cmap(doc, Fn_Cmap_Rec{3, 1, uni})}, + }) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /TrueType /BaseFont /Sq /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile2", prog) + if f == nil { return } + fn_expect_gid(t, f, 'A', 1, "A is glyph 1") + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, font_glyph_path(f, 1, &p), Error.None) + testing.expect(t, len(p.points) >= 4, "the square has four points") + testing.expect_value(t, p.points[0], Point{0, 0}) + // Glyph space, untouched by the font matrix: 100 font units, not 0.1. + testing.expect_value(t, p.points[1], Point{100, 0}) + + // A glyph the font does not have draws nothing rather than failing. + path_clear(&p) + testing.expect_value(t, font_glyph_path(f, 9, &p), Error.No_Glyph) +} + +@(test) +test_font_descendant_written_directly :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 9.7.6.1 requires /DescendantFonts to be a one-element array. Producers + // write the dictionary itself, and it means the same thing: refusing it + // would cost every width and every glyph in the font. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "<< /Subtype /CIDFontType2 /DW 640 /W [3 3 900] >> >>") + if f == nil { return } + testing.expect(t, f.descendant != nil, "the descendant should have been found") + testing.expect_value(t, font_cid_width(f, 1), 640) + testing.expect_value(t, font_cid_width(f, 3), 900) +} + +@(test) +test_font_cff_font_matrix_and_advance :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A CFF that declares its own FontMatrix. The 1/1000 default is what all + // but a handful of fonts use, which is exactly why the handful have to be + // honoured: at 2000 units per em, a font drawn through thousandths comes + // out twice too big, and the charstring's own advance -- which is in those + // same units -- has to be halved on its way into a PDF width. + prog := fn_cff(doc, []u16{34, 35}, false, true) + f := fn_simple(t, doc, + "<< /Type /Font /Subtype /Type1 /BaseFont /Wide /Encoding /WinAnsiEncoding >>", + "<< /Flags 32 >>", "FontFile3", prog) + if f == nil { return } + testing.expect(t, f.cff != nil, "the CFF should have parsed") + testing.expect_value(t, font_matrix(f).a, 0.0005) + testing.expect_value(t, font_matrix(f).d, 0.0005) + // The charstring declares 400 glyph-space units, which is a fifth of this + // font's em, which is 200 thousandths. + fn_expect_width(t, f, 'A', 200, "charstring advance through the FontMatrix") +} + +@(test) +test_font_widths_array_clamped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A /Widths array longer than the 256 codes that exist. The length came + // out of the file, so it is clamped rather than believed; the entries + // that can actually be indexed still have to be the right ones. + a := new_array(doc.allocator, 512) + for i in 0 ..< 400 { + array_push(a, i64(1000 + i)) + } + d := fn_dict(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Long /FirstChar 0 >>") + dict_set(d, "Widths", a) + f := fn_load(t, doc, d) + if f == nil { return } + testing.expect_value(t, len(f.widths), 256) + fn_expect_width(t, f, 0, 1000, "the first entry") + fn_expect_width(t, f, 255, 1255, "the last code that exists") +} + +@(test) +test_font_type0_w_out_of_order :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /W is not required to be sorted and files do write it backwards. The + // lookup is a binary search, so the entries have to be put in order first + // or every width past the first is wrong. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 5 /W [90 92 900 50 52 500 10 12 100] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 11), 100) + testing.expect_value(t, font_cid_width(f, 51), 500) + testing.expect_value(t, font_cid_width(f, 91), 900) + testing.expect_value(t, font_cid_width(f, 60), 5) + testing.expect_value(t, font_cid_width(f, 9), 5) + testing.expect_value(t, font_cid_width(f, 93), 5) +} + +@(test) +test_font_type0_w_broad_range_split :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A narrow correction written inside a broad run splits it in three: the + // part before, the correction, and the part after. Dropping the part after + // -- the easy mistake, since it is the half that is not adjacent to + // anything -- silently gives every CID above the correction the default + // width, which is most of the font. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 7 /W [1 100 300 50 50 900] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 1), 300) + testing.expect_value(t, font_cid_width(f, 49), 300) + testing.expect_value(t, font_cid_width(f, 50), 900) + testing.expect_value(t, font_cid_width(f, 51), 300) + testing.expect_value(t, font_cid_width(f, 100), 300) + testing.expect_value(t, font_cid_width(f, 101), 7) + testing.expect_value(t, font_cid_width(f, 0), 7) +} + +@(test) +test_font_type0_w_many_overrides :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // One run over the whole CID space with forty single-CID corrections + // written inside it -- a perfectly legal way to say "everything is 300 + // except these". The answer for a CID above all forty is still 300, and + // it lies forty entries behind where a search by starting CID lands: any + // resolution that scans back a fixed distance from there gets it wrong. + a := new_array(doc.allocator, 256) + array_push(a, i64(0)) + array_push(a, i64(2000)) + array_push(a, i64(300)) + for c in 1 ..= 40 { + array_push(a, i64(c)) + array_push(a, i64(c)) + array_push(a, i64(900)) + } + cid := fn_dict(doc, "<< /Subtype /CIDFontType2 /DW 5 >>") + dict_set(cid, "W", a) + f := fn_type0(t, doc, "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H >>", cid) + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 7), 900) + testing.expect_value(t, font_cid_width(f, 40), 900) + testing.expect_value(t, font_cid_width(f, 41), 300) + testing.expect_value(t, font_cid_width(f, 500), 300) + testing.expect_value(t, font_cid_width(f, 2000), 300) + testing.expect_value(t, font_cid_width(f, 2001), 5) +} + +@(test) +test_font_type0_w2_out_of_order :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The same treatment for /W2: written backwards, with a correction inside + // a broad run. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-V /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 500 /W2 [20 20 -700 200 750 1 100 -900 50 880] >>] >>") + if f == nil { return } + w, vx, _ := font_cid_vmetric(f, 20, 500) + testing.expect_value(t, w, -700) + testing.expect_value(t, vx, 200) + w, vx, _ = font_cid_vmetric(f, 19, 500) + testing.expect_value(t, w, -900) + testing.expect_value(t, vx, 50) + w, vx, _ = font_cid_vmetric(f, 21, 500) + testing.expect_value(t, w, -900) + w, _, _ = font_cid_vmetric(f, 101, 500) + testing.expect_value(t, w, -1000) // the /DW2 default +} + +@(test) +test_font_type0_w_nested_overlaps :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Three runs nested inside one another, each narrower and later than the + // last: 0..100 is A, 10..20 is B written over it, and 11..12 is C written + // over that. Every CID has exactly one right answer -- + // + // 0..9 A, 10 B, 11..12 C, 13..20 B, 21..100 A + // + // -- and the shape is what catches an implementation that keeps only one + // leftover piece per correction: the part of A above 20 is left stranded + // behind the part of B above 12, and both have to come back. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 7 /W [0 100 100 10 20 200 11 12 300] >>] >>") + if f == nil { return } + for pair in ([][2]f64{ + {0, 100}, {9, 100}, {10, 200}, {11, 300}, {12, 300}, + {13, 200}, {20, 200}, {21, 100}, {100, 100}, {101, 7}, + }) { + got := font_cid_width(f, u32(pair[0])) + testing.expectf(t, got == pair[1], "CID %v is %v wide, wanted %v", + pair[0], got, pair[1]) + } +} + +@(test) +test_font_type0_w_identical_and_adjacent :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Two runs covering exactly the same CIDs, and a run that abuts another + // without overlapping it. Neither may lose a CID or invent one. + f := fn_font(t, doc, + "<< /Type /Font /Subtype /Type0 /Encoding /Identity-H /DescendantFonts " + + "[<< /Subtype /CIDFontType2 /DW 7 /W [5 9 100 5 9 200 10 14 300] >>] >>") + if f == nil { return } + testing.expect_value(t, font_cid_width(f, 4), 7) + // One of the two identical runs wins; which is the file's problem, but a + // CID they both cover must not fall through to /DW. + w := font_cid_width(f, 7) + testing.expectf(t, w == 100 || w == 200, "CID 7 fell through to /DW: %v", w) + testing.expect_value(t, font_cid_width(f, 9), w) + testing.expect_value(t, font_cid_width(f, 10), 300) + testing.expect_value(t, font_cid_width(f, 14), 300) + testing.expect_value(t, font_cid_width(f, 15), 7) +} diff --git a/font_truetype.odin b/font_truetype.odin new file mode 100644 index 0000000..5d797e0 --- /dev/null +++ b/font_truetype.odin @@ -0,0 +1,1037 @@ +package pdf + +import "core:math" +import "core:mem" + +// TrueType and OpenType font programs: the `glyf`/`loca` outlines, the `cmap` +// character map, `hmtx` advances and `post` glyph names. +// +// References are to ISO/IEC 14496-22 (Open Font Format), whose table layouts +// are identical to Apple's TrueType Reference Manual for everything read here. +// +// Nothing in this file knows about PDF. Bytes in, outlines out: a font program +// is a self-contained format that happens to be embedded in PDF, and keeping +// the two apart is what makes this separately testable and what lets the same +// reader serve a FontFile2, a CIDFontType2 and an OpenType wrapper alike. +// +// The fonts this sees are the most malformed data in a PDF. Subsetters strip +// tables, rewrite `loca` badly, leave `numGlyphs` disagreeing with the data, +// and truncate the last table in the file. Every offset here is therefore +// bounds-checked at the point of use and every loop is bounded, and a glyph +// that cannot be read draws nothing rather than failing the font: the page +// still has to come out. +// +// MEMORY: `tt_parse` takes an `allocator` and makes exactly two lasting +// allocations from it -- the TrueType_Font itself and, when the font has a +// usable `post` table, its glyph-name array. Everything else it keeps is a +// sub-slice of `data`, which the caller owns and must keep alive for as long +// as the font. There is no destructor: the intended allocator is the +// Document's arena. `tt_parse` and `tt_glyph_path` each take scratch from +// `context.allocator` as well, and free it before returning. + +// A composite glyph may reference other composites. Five levels is deeper than +// any real font goes; a subsetter that produces a glyph referencing itself is +// stopped here. +@(private) +MAX_TT_COMPONENT_DEPTH :: 6 +// Components one glyph may expand to, across all nesting levels. A composite +// chain is a graph the file controls, so it needs fuel as well as a depth cap. +@(private) +MAX_TT_COMPONENTS :: 512 +// Points one glyph may expand to. The format's own ceiling is 32767 points per +// simple glyph; a composite can legitimately hold several of those. +@(private) +MAX_TT_GLYPH_POINTS :: 1 << 18 +// `numGlyphs` is a u16 in `maxp`, so this is the format's own limit and not an +// invented one. +@(private) +MAX_TT_GLYPHS :: 65536 +// Table records one directory may name. The format allows 65535; no real font +// has fifty. +@(private) +MAX_TT_TABLES :: 512 + +// Sfnt tags, big-endian, as they appear in the file. +@(private) TT_TAG_TTCF :: u32(0x74746366) // 'ttcf', a TrueType Collection +@(private) TT_TAG_OTTO :: u32(0x4F54544F) // 'OTTO', CFF outlines +@(private) TT_TAG_TRUE :: u32(0x74727565) // 'true', Apple's variant +@(private) TT_TAG_V1 :: u32(0x00010000) +@(private) TT_TAG_V2 :: u32(0x00020000) + +TrueType_Font :: struct { + data: []byte, + + // Tables, as sub-slices of `data`. Empty when the font has none. + glyf: []byte, + loca: []byte, + hmtx: []byte, + cff: []byte, + + units_per_em: f64, + num_glyphs: int, + num_h_metrics: int, + // `head.indexToLocFormat`: false for u16 offsets stored halved, true for + // u32 offsets. + long_loca: bool, + // An OpenType font whose outlines are CFF: no `glyf`, and `tt_cff_data` + // hands the table to the CFF reader. + is_otto: bool, + + // The `cmap` subtables worth having, chosen once when the font is parsed. + // All three are exposed rather than merged because the rules for which one + // a PDF font should use depend on the font dictionary's symbolic flag, + // which this file deliberately knows nothing about. + cmap_unicode: []byte, // (3,10), (3,1) or a platform-0 table + cmap_symbol: []byte, // (3,0): codes are conventionally 0xF000 + a byte + cmap_mac: []byte, // (1,0): Mac Roman byte codes + + // `post` glyph names indexed by glyph id, "" where the font gives none. + // Entries point into `data` or at a static name; the slice itself is the + // font's only other allocation. + names: []string, +} + +// --------------------------------------------------------------------------- +// Byte access +// +// Every reader returns zero for an offset that is not wholly inside the slice. +// A header field that reads as zero degrades into a default a few lines later, +// which is what a truncated table should do; the loops that walk arrays check +// their own bounds before reading, because there a zero would be data. +// --------------------------------------------------------------------------- + +@(private) +tt_u8 :: proc "contextless" (b: []byte, off: int) -> u8 { + if off < 0 || off >= len(b) { return 0 } + return b[off] +} + +@(private) +tt_u16 :: proc "contextless" (b: []byte, off: int) -> u16 { + if off < 0 || off + 2 > len(b) { return 0 } + return u16(b[off]) << 8 | u16(b[off + 1]) +} + +@(private) +tt_i16 :: proc "contextless" (b: []byte, off: int) -> i16 { + return i16(tt_u16(b, off)) +} + +@(private) +tt_u32 :: proc "contextless" (b: []byte, off: int) -> u32 { + if off < 0 || off + 4 > len(b) { return 0 } + return u32(b[off]) << 24 | u32(b[off + 1]) << 16 | u32(b[off + 2]) << 8 | u32(b[off + 3]) +} + +// tt_f2dot14 reads the fixed-point number composite transforms are written in: +// a signed 16-bit value with 14 fraction bits. +@(private) +tt_f2dot14 :: proc "contextless" (b: []byte, off: int) -> f64 { + return f64(tt_i16(b, off)) / 16384 +} + +// tt_slice returns the sub-slice a table record names, clamped to the data +// that is actually there. A last table whose length runs past the end of the +// file is the single most common damage in an embedded font program, so it is +// truncated rather than dropped. +@(private) +tt_slice :: proc "contextless" (data: []byte, off, length: u32) -> []byte { + if u64(off) >= u64(len(data)) { return nil } + o := int(off) + n := len(data) - o + if u64(length) < u64(n) { n = int(length) } + return data[o:][:n] +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +// tt_parse reads a font program's table directory. +// +// `data` is borrowed, not copied: the font holds sub-slices of it and the +// caller must keep it alive. The font and its glyph-name table come from +// `allocator`. +@(private) +tt_parse :: proc(data: []byte, allocator := context.allocator) -> (^TrueType_Font, Error) { + if len(data) < 12 { return nil, .Bad_TrueType } + + base := 0 + tag := tt_u32(data, 0) + if tag == TT_TAG_TTCF { + // A TrueType Collection holds several fonts sharing tables. A PDF + // FontFile2 has no way to say which face it means, so the first one is + // the only defensible choice. ISO 14496-22, 5.2. + if tt_u32(data, 8) == 0 { return nil, .Bad_TrueType } + base = int(tt_u32(data, 12)) + if base < 0 || base + 12 > len(data) { return nil, .Bad_TrueType } + tag = tt_u32(data, base) + } + otto := tag == TT_TAG_OTTO + if tag != TT_TAG_V1 && tag != TT_TAG_V2 && tag != TT_TAG_TRUE && !otto { + return nil, .Bad_TrueType + } + + num_tables := int(tt_u16(data, base + 4)) + if num_tables > MAX_TT_TABLES { num_tables = MAX_TT_TABLES } + // A directory truncated mid-record still names the tables before it. + avail := (len(data) - (base + 12)) / 16 + if avail < 0 { avail = 0 } + if num_tables > avail { num_tables = avail } + if num_tables == 0 { return nil, .Bad_TrueType } + + head, maxp, hhea, post, cmap: []byte + f := new(TrueType_Font, allocator) + if f == nil { return nil, .Out_Of_Memory } + f.data = data + f.is_otto = otto + + for i in 0 ..< num_tables { + rec := base + 12 + i * 16 + t := tt_slice(data, tt_u32(data, rec + 8), tt_u32(data, rec + 12)) + if len(t) == 0 { continue } + // The first record for a tag wins. Directories with a tag twice exist, + // and no reader agrees on which one is meant; taking the first at + // least makes this deterministic. + switch tt_u32(data, rec) { + case 0x68656164: if head == nil { head = t } // 'head' + case 0x6D617870: if maxp == nil { maxp = t } // 'maxp' + case 0x68686561: if hhea == nil { hhea = t } // 'hhea' + case 0x686D7478: if f.hmtx == nil { f.hmtx = t } // 'hmtx' + case 0x676C7966: if f.glyf == nil { f.glyf = t } // 'glyf' + case 0x6C6F6361: if f.loca == nil { f.loca = t } // 'loca' + case 0x706F7374: if post == nil { post = t } // 'post' + case 0x636D6170: if cmap == nil { cmap = t } // 'cmap' + case 0x43464620: if f.cff == nil { f.cff = t } // 'CFF ' + } + } + + // ISO 14496-22, 5.2.2: unitsPerEm at 18, indexToLocFormat at 50. + f.units_per_em = f64(tt_u16(head, 18)) + if f.units_per_em == 0 { + // Zero would make every glyph infinitely large. 1000 is the value a + // PDF font matrix assumes when it has nothing better. + f.units_per_em = 1000 + } + f.long_loca = tt_i16(head, 50) != 0 + + f.num_glyphs = int(tt_u16(maxp, 4)) + if f.num_glyphs == 0 { + // No usable maxp. loca has one more entry than there are glyphs, which + // is enough to keep the outlines reachable. + if f.long_loca { + f.num_glyphs = len(f.loca) / 4 - 1 + } else { + f.num_glyphs = len(f.loca) / 2 - 1 + } + if f.num_glyphs < 0 { f.num_glyphs = 0 } + } + if f.num_glyphs > MAX_TT_GLYPHS { f.num_glyphs = MAX_TT_GLYPHS } + + // ISO 14496-22, 5.2.3: numberOfHMetrics is the last field of hhea. + f.num_h_metrics = int(tt_u16(hhea, 34)) + if f.num_h_metrics > f.num_glyphs { f.num_h_metrics = f.num_glyphs } + if f.num_h_metrics == 0 && len(f.hmtx) >= 4 && f.num_glyphs > 0 { + // A hhea that is missing or truncated leaves no metric count, but the + // hmtx beside it is usually intact. One advance shared by every glyph + // is a poor answer; zero for every glyph is a worse one, because it + // piles the whole line of text on one spot. + f.num_h_metrics = 1 + } + + tt_select_cmaps(f, cmap) + tt_parse_post(f, post, allocator) + return f, .None +} + +// tt_glyph_count is the number of glyph ids the font defines. Glyph 0 is +// .notdef and is included. +@(private) +tt_glyph_count :: proc(f: ^TrueType_Font) -> int { + if f == nil { return 0 } + return f.num_glyphs +} + +// tt_units_per_em is the side of the em square in font units: the divisor that +// turns a glyph outline into text space. 1000 for a font that gives none. +@(private) +tt_units_per_em :: proc(f: ^TrueType_Font) -> f64 { + if f == nil { return 1000 } + return f.units_per_em +} + +// tt_has_glyf reports whether the font carries TrueType outlines. An OpenType +// font with CFF outlines does not; `tt_cff_data` has its font program. +@(private) +tt_has_glyf :: proc(f: ^TrueType_Font) -> bool { + if f == nil { return false } + return len(f.glyf) > 0 && len(f.loca) >= 4 +} + +// tt_cff_data returns the 'CFF ' table for an OpenType font with CFF outlines, +// or an empty slice. The bytes belong to the data passed to `tt_parse`. +@(private) +tt_cff_data :: proc(f: ^TrueType_Font) -> []byte { + if f == nil { return nil } + return f.cff +} + +// --------------------------------------------------------------------------- +// Advances +// --------------------------------------------------------------------------- + +// tt_glyph_advance is the horizontal advance in font units, 0 when the font +// has no usable `hmtx`. +@(private) +tt_glyph_advance :: proc(f: ^TrueType_Font, gid: u16) -> f64 { + if f == nil { return 0 } + return f64(tt_hmtx_advance(f, tt_metrics_gid(f, gid, 0))) +} + +// tt_hmtx_advance reads `hmtx` directly. The table stores advances only for +// the first numberOfHMetrics glyphs; every glyph after that shares the last +// one, which is how a monospaced or a right-heavy font stays small. +// ISO 14496-22, 5.2.4. +@(private) +tt_hmtx_advance :: proc(f: ^TrueType_Font, gid: u16) -> u16 { + n := f.num_h_metrics + if n <= 0 { return 0 } + i := int(gid) + if i >= n { i = n - 1 } + off := i * 4 + if off + 2 > len(f.hmtx) { + // A truncated hmtx still answers for the glyphs it does hold, and the + // last of those stands in for the rest. + have := len(f.hmtx) / 4 + if have <= 0 { return 0 } + off = (have - 1) * 4 + } + return tt_u16(f.hmtx, off) +} + +// tt_metrics_gid follows USE_MY_METRICS: a composite that sets it on one of +// its components takes that component's advance rather than its own. In a +// well-formed font the two agree and this changes nothing; in a font whose +// hmtx was rewritten by a subsetter it is the only remaining truth. +@(private) +tt_metrics_gid :: proc(f: ^TrueType_Font, gid: u16, depth: int) -> u16 { + if depth >= MAX_TT_COMPONENT_DEPTH || !tt_has_glyf(f) { return gid } + g, ok := tt_glyph_data(f, gid) + if !ok || len(g) < 10 || tt_i16(g, 0) >= 0 { return gid } + + p := 10 + for _ in 0 ..< MAX_TT_COMPONENTS { + if p + 4 > len(g) { break } + flags := tt_u16(g, p) + cgid := tt_u16(g, p + 2) + p += 4 + p += 4 if flags & 0x0001 != 0 else 2 + switch { + case flags & 0x0008 != 0: p += 2 + case flags & 0x0040 != 0: p += 4 + case flags & 0x0080 != 0: p += 8 + } + if flags & 0x0200 != 0 { + return tt_metrics_gid(f, cgid, depth + 1) + } + if flags & 0x0020 == 0 { break } + } + return gid +} + +// --------------------------------------------------------------------------- +// Outlines +// --------------------------------------------------------------------------- + +@(private) +Tt_Point :: struct { + x, y: f64, + on: bool, +} + +// Tt_Contour is one closed contour as a half-open range of Tt_Outline.pts. +// Ranges are kept rather than end indices because a composite concatenates the +// points of several glyphs and a malformed contour in the middle must not +// shift the ones after it. +@(private) +Tt_Contour :: struct { + start, end: int, +} + +// Tt_Outline accumulates a glyph's points before any of them is emitted. A +// composite's point-matching offsets are expressed in point indices, so the +// points have to exist as an array at least until the component that follows +// them is placed. +@(private) +Tt_Outline :: struct { + pts: [dynamic]Tt_Point, + contours: [dynamic]Tt_Contour, + // Scratch for one simple glyph's flag array, reused across components. + flags: [dynamic]u8, + fuel: int, +} + +// tt_glyph_path appends glyph `gid`'s outline to `out`, in font units (divide +// by `tt_units_per_em` for text space). +// +// Returns .No_Glyph when the font has no outline for the id at all, and +// .Bad_TrueType when the glyph's data is malformed -- in which case whatever +// could be decoded has still been appended, because half a glyph is closer to +// right than a blank page. An empty glyph, which is what a space is, is not an +// error. +@(private) +tt_glyph_path :: proc(f: ^TrueType_Font, gid: u16, out: ^Path) -> Error { + if f == nil || out == nil { return .Invalid_Argument } + if !tt_has_glyf(f) { return .No_Glyph } + if int(gid) >= f.num_glyphs { return .No_Glyph } + + ol := Tt_Outline{ + pts = make([dynamic]Tt_Point, 0, 64), + contours = make([dynamic]Tt_Contour, 0, 8), + flags = make([dynamic]u8, 0, 64), + fuel = MAX_TT_COMPONENTS, + } + defer { + delete(ol.pts) + delete(ol.contours) + delete(ol.flags) + } + + err := tt_build_glyph(f, gid, MATRIX_IDENTITY, 0, &ol) + for c in ol.contours { + // Fewer than two points enclose no area; emitting them would leave a + // degenerate subpath for the rasteriser to step over. + if c.start < 0 || c.end > len(ol.pts) || c.end - c.start < 2 { continue } + tt_emit_contour(out, ol.pts[c.start:c.end]) + } + return err +} + +// tt_glyph_data returns the bytes of one glyph's `glyf` entry. +// +// ok is false when `loca` does not cover the id; an empty slice with ok means +// the glyph is legitimately blank. A `loca` whose entries do not increase is a +// subsetter bug seen often enough that a non-increasing pair is read as a +// blank glyph rather than as damage. +@(private) +tt_glyph_data :: proc(f: ^TrueType_Font, gid: u16) -> ([]byte, bool) { + i := int(gid) + start, end: int + if f.long_loca { + if (i + 2) * 4 > len(f.loca) { return nil, false } + start = int(tt_u32(f.loca, i * 4)) + end = int(tt_u32(f.loca, i * 4 + 4)) + } else { + if (i + 2) * 2 > len(f.loca) { return nil, false } + // Short offsets are stored halved, so the table can reach 128K. + start = int(tt_u16(f.loca, i * 2)) * 2 + end = int(tt_u16(f.loca, i * 2 + 2)) * 2 + } + if end <= start || start >= len(f.glyf) { return nil, true } + if end > len(f.glyf) { end = len(f.glyf) } + return f.glyf[start:end], true +} + +@(private) +tt_build_glyph :: proc(f: ^TrueType_Font, gid: u16, m: Matrix, depth: int, ol: ^Tt_Outline) -> Error { + if depth > MAX_TT_COMPONENT_DEPTH { return .Recursion_Limit } + g, ok := tt_glyph_data(f, gid) + if !ok { return .No_Glyph } + if len(g) == 0 { return .None } + // The glyph header is numberOfContours and a bounding box: 10 bytes. + if len(g) < 10 { return .Bad_TrueType } + nc := int(tt_i16(g, 0)) + if nc >= 0 { + return tt_simple_glyph(g, nc, m, ol) + } + return tt_composite_glyph(f, g, m, depth, ol) +} + +// tt_simple_glyph decodes one glyph's contours into `ol`, transformed by `m`. +// ISO 14496-22, 5.3.3. +@(private) +tt_simple_glyph :: proc(g: []byte, n_contours: int, m: Matrix, ol: ^Tt_Outline) -> Error { + if n_contours == 0 { return .None } + ends_off := 10 + if ends_off + 2 * n_contours + 2 > len(g) { return .Bad_TrueType } + + // The last contour's end point index gives the point count. + n_pts := int(tt_u16(g, ends_off + 2 * (n_contours - 1))) + 1 + if n_pts <= 0 || n_pts > MAX_TT_GLYPH_POINTS { return .Bad_TrueType } + if len(ol.pts) + n_pts > MAX_TT_GLYPH_POINTS { return .Bad_TrueType } + + p := ends_off + 2 * n_contours + instr := int(tt_u16(g, p)) + p += 2 + instr + if p > len(g) { return .Bad_TrueType } + + // Flags, with the run-length encoding: a flag with REPEAT set is followed + // by a count of extra points that share it. + clear(&ol.flags) + for len(ol.flags) < n_pts { + if p >= len(g) { return .Bad_TrueType } + fl := g[p] + p += 1 + append(&ol.flags, fl) + if fl & 0x08 != 0 { + if p >= len(g) { return .Bad_TrueType } + rep := int(g[p]) + p += 1 + for _ in 0 ..< rep { + if len(ol.flags) >= n_pts { break } + append(&ol.flags, fl) + } + } + } + + base := len(ol.pts) + // X then Y, each a delta from the previous point. A short vector is one + // unsigned byte whose sign comes from the SAME_OR_POSITIVE bit; a long one + // is a signed word, and that same bit set with no short flag means the + // coordinate simply repeats. + x := 0 + for k in 0 ..< n_pts { + fl := ol.flags[k] + if fl & 0x02 != 0 { + if p >= len(g) { return .Bad_TrueType } + d := int(g[p]) + p += 1 + x += d if fl & 0x10 != 0 else -d + } else if fl & 0x10 == 0 { + if p + 2 > len(g) { return .Bad_TrueType } + x += int(tt_i16(g, p)) + p += 2 + } + append(&ol.pts, Tt_Point{x = f64(x), on = fl & 0x01 != 0}) + } + y := 0 + for k in 0 ..< n_pts { + fl := ol.flags[k] + if fl & 0x04 != 0 { + if p >= len(g) { return .Bad_TrueType } + d := int(g[p]) + p += 1 + y += d if fl & 0x20 != 0 else -d + } else if fl & 0x20 == 0 { + if p + 2 > len(g) { return .Bad_TrueType } + y += int(tt_i16(g, p)) + p += 2 + } + ol.pts[base + k].y = f64(y) + } + + if m != MATRIX_IDENTITY { + for k in base ..< len(ol.pts) { + pt := transform_point(m, Point{ol.pts[k].x, ol.pts[k].y}) + ol.pts[k].x = pt.x + ol.pts[k].y = pt.y + } + } + + prev := 0 + for c in 0 ..< n_contours { + e := int(tt_u16(g, ends_off + 2 * c)) + 1 + // End indices must increase and stay inside the point array. One that + // does not is skipped rather than taken as a reason to drop the glyph. + if e <= prev || e > n_pts { continue } + append(&ol.contours, Tt_Contour{base + prev, base + e}) + prev = e + } + return .None +} + +// tt_composite_glyph places other glyphs into this one. ISO 14496-22, 5.3.4. +@(private) +tt_composite_glyph :: proc(f: ^TrueType_Font, g: []byte, m: Matrix, depth: int, ol: ^Tt_Outline) -> Error { + // Point-matching offsets index the points of the composite being built, so + // where its own points start has to be remembered. + glyph_base := len(ol.pts) + p := 10 + err := Error.None + for { + if ol.fuel <= 0 { return .Recursion_Limit } + ol.fuel -= 1 + if p + 4 > len(g) { return .Bad_TrueType } + flags := tt_u16(g, p) + cgid := tt_u16(g, p + 2) + p += 4 + + xy := flags & 0x0002 != 0 // ARGS_ARE_XY_VALUES + a1, a2: int + if flags & 0x0001 != 0 { // ARG_1_AND_2_ARE_WORDS + if p + 4 > len(g) { return .Bad_TrueType } + if xy { + a1 = int(tt_i16(g, p)) + a2 = int(tt_i16(g, p + 2)) + } else { + a1 = int(tt_u16(g, p)) + a2 = int(tt_u16(g, p + 2)) + } + p += 4 + } else { + if p + 2 > len(g) { return .Bad_TrueType } + if xy { + a1 = int(i8(g[p])) + a2 = int(i8(g[p + 1])) + } else { + a1 = int(g[p]) + a2 = int(g[p + 1]) + } + p += 2 + } + + cm := MATRIX_IDENTITY + switch { + case flags & 0x0008 != 0: // WE_HAVE_A_SCALE + if p + 2 > len(g) { return .Bad_TrueType } + s := tt_f2dot14(g, p) + p += 2 + cm.a, cm.d = s, s + case flags & 0x0040 != 0: // WE_HAVE_AN_X_AND_Y_SCALE + if p + 4 > len(g) { return .Bad_TrueType } + cm.a = tt_f2dot14(g, p) + cm.d = tt_f2dot14(g, p + 2) + p += 4 + case flags & 0x0080 != 0: // WE_HAVE_A_TWO_BY_TWO + if p + 8 > len(g) { return .Bad_TrueType } + cm.a = tt_f2dot14(g, p) + cm.b = tt_f2dot14(g, p + 2) + cm.c = tt_f2dot14(g, p + 4) + cm.d = tt_f2dot14(g, p + 6) + p += 8 + } + + if xy { + dx, dy := f64(a1), f64(a2) + // Whether the offset is in the component's own space or the + // composite's is a genuine disagreement between Apple and + // Microsoft. Unscaled is both the Microsoft behaviour and the + // default every font is drawn with, so it takes an explicit + // SCALED_COMPONENT_OFFSET to get the other one. + if flags & 0x0800 != 0 && flags & 0x1000 == 0 { + v := transform_vector(cm, Point{dx, dy}) + dx, dy = v.x, v.y + } + if flags & 0x0004 != 0 { // ROUND_XY_TO_GRID + dx = math.round(dx) + dy = math.round(dy) + } + cm.e, cm.f = dx, dy + } + + base := len(ol.pts) + cerr := tt_build_glyph(f, cgid, matrix_mul(cm, m), depth + 1, ol) + if cerr != .None && err == .None { err = cerr } + + if !xy { + // The alternative to an offset: arg1 names a point already placed + // in this composite and arg2 the matching point of the component, + // and the component moves so that the two coincide. + i1 := glyph_base + a1 + i2 := base + a2 + if i1 >= glyph_base && i1 < base && i2 >= base && i2 < len(ol.pts) { + dx := ol.pts[i1].x - ol.pts[i2].x + dy := ol.pts[i1].y - ol.pts[i2].y + if flags & 0x0004 != 0 { + dx = math.round(dx) + dy = math.round(dy) + } + for k in base ..< len(ol.pts) { + ol.pts[k].x += dx + ol.pts[k].y += dy + } + } + } + + if flags & 0x0020 == 0 { break } // MORE_COMPONENTS + } + return err +} + +// tt_emit_contour turns one quadratic contour into cubics on `out`. +// +// A TrueType contour is a closed quadratic B-spline: on-curve points are on the +// outline, off-curve points are control points, and two consecutive off-curve +// points imply an on-curve point at their midpoint. A contour with no on-curve +// point at all -- an ellipse drawn as four control points -- starts at the +// midpoint of its last and first points. +@(private) +tt_emit_contour :: proc(out: ^Path, pts: []Tt_Point) { + n := len(pts) + if n < 2 { return } + + first := -1 + for i in 0 ..< n { + if pts[i].on { + first = i + break + } + } + + sx, sy: f64 + start_i, count: int + if first < 0 { + sx = (pts[n - 1].x + pts[0].x) * 0.5 + sy = (pts[n - 1].y + pts[0].y) * 0.5 + start_i = 0 + count = n + } else { + sx, sy = pts[first].x, pts[first].y + // Walk the other n-1 points; the closing segment back to the start is + // the Close verb, or the trailing curve below. + start_i = first + 1 + count = n - 1 + } + + path_move_to(out, sx, sy) + cx, cy := sx, sy + ctl_x, ctl_y: f64 + have_ctl := false + + for k in 0 ..< count { + pt := pts[(start_i + k) % n] + if pt.on { + if have_ctl { + tt_quad_to(out, cx, cy, ctl_x, ctl_y, pt.x, pt.y) + have_ctl = false + } else { + path_line_to(out, pt.x, pt.y) + } + cx, cy = pt.x, pt.y + } else { + if have_ctl { + mx := (ctl_x + pt.x) * 0.5 + my := (ctl_y + pt.y) * 0.5 + tt_quad_to(out, cx, cy, ctl_x, ctl_y, mx, my) + cx, cy = mx, my + } + ctl_x, ctl_y = pt.x, pt.y + have_ctl = true + } + } + if have_ctl { + tt_quad_to(out, cx, cy, ctl_x, ctl_y, sx, sy) + } + path_close(out) +} + +// tt_quad_to appends the cubic equal to the quadratic from (cx,cy) through +// control (qx,qy) to (x,y). The two are exactly interconvertible: a cubic whose +// controls sit two thirds of the way from each end towards the quadratic's +// control traces the same curve, so nothing is approximated here. +@(private) +tt_quad_to :: proc(out: ^Path, cx, cy, qx, qy, x, y: f64) { + K :: 2.0 / 3.0 + path_curve_to(out, + cx + (qx - cx) * K, cy + (qy - cy) * K, + x + (qx - x) * K, y + (qy - y) * K, + x, y) +} + +// --------------------------------------------------------------------------- +// cmap +// --------------------------------------------------------------------------- + +// tt_select_cmaps picks the best subtable of each of the three kinds this +// reader offers. ISO 14496-22, 5.2.6. +// +// Choosing among the platform/encoding pairs is where readers get symbolic +// fonts wrong, so all three kinds are kept and none is merged into another: +// which one a PDF font should consult depends on its descriptor flags and its +// /Encoding, and that decision belongs in font.odin. +@(private) +tt_select_cmaps :: proc(f: ^TrueType_Font, cm: []byte) { + if len(cm) < 4 { return } + n := int(tt_u16(cm, 2)) + avail := (len(cm) - 4) / 8 + if avail < 0 { avail = 0 } + if n > avail { n = avail } + + best := 0 + for i in 0 ..< n { + rec := 4 + i * 8 + plat := tt_u16(cm, rec) + enc := tt_u16(cm, rec + 2) + off := tt_u32(cm, rec + 4) + if u64(off) >= u64(len(cm)) { continue } + // A subtable runs to the end of the cmap table rather than to its own + // declared length: format 4 tables whose length excludes the trailing + // glyphIdArray are a well-known producer bug, and reading past a + // correct length costs nothing. + sub := cm[off:] + if len(sub) < 4 { continue } + format := tt_u16(sub, 0) + if !tt_cmap_supported(format) { continue } + + // (3,0) and (1,0) are not Unicode and are kept as themselves. + if plat == 3 && enc == 0 { + if f.cmap_symbol == nil { f.cmap_symbol = sub } + continue + } + if plat == 1 && enc == 0 { + if f.cmap_mac == nil { f.cmap_mac = sub } + continue + } + + score := 0 + switch { + case plat == 3 && enc == 10: score = 5 // Windows, full repertoire + case plat == 3 && enc == 1: score = 4 // Windows, BMP + case plat == 0 && enc >= 4: score = 3 // Unicode, full repertoire + case plat == 0: score = 2 // Unicode, BMP + } + if score > best { + best = score + f.cmap_unicode = sub + } + } +} + +@(private) +tt_cmap_supported :: proc "contextless" (format: u16) -> bool { + // Format 2 is the high-byte scheme for legacy CJK encodings, format 8 and + // 10 were never produced. Refusing them here rather than at lookup time + // means a font with both an unreadable (3,1) and a readable (1,0) still + // gets the (1,0). + return format == 0 || format == 4 || format == 6 || format == 12 +} + +// tt_cmap_lookup maps one character code through one subtable. Zero means the +// subtable has no glyph for the code -- glyph 0 is .notdef, which is the same +// answer. +@(private) +tt_cmap_lookup :: proc(sub: []byte, c: u32) -> u16 { + if len(sub) < 4 { return 0 } + switch tt_u16(sub, 0) { + case 0: + // Byte encoding: 256 one-byte glyph ids at offset 6. + if c > 255 { return 0 } + off := 6 + int(c) + return u16(tt_u8(sub, off)) if off < len(sub) else 0 + + case 4: + if c > 0xFFFF { return 0 } + seg := int(tt_u16(sub, 6)) / 2 + if seg == 0 { return 0 } + cc := u16(c) + // The segments are sorted by endCode, so the one that can hold the + // code is the first whose endCode is not below it. The format also + // ships searchRange/entrySelector for exactly this search; they are + // ignored because a wrong pair is a common damage and recomputing the + // bounds costs nothing. + lo, hi := 0, seg + for lo < hi { + mid := lo + (hi - lo) / 2 + if tt_u16(sub, 14 + 2 * mid) < cc { + lo = mid + 1 + } else { + hi = mid + } + } + if lo >= seg { return 0 } + start_off := 16 + 2 * seg + delta_off := 16 + 4 * seg + range_off := 16 + 6 * seg + start := tt_u16(sub, start_off + 2 * lo) + if cc < start { return 0 } + delta := u32(u16(tt_i16(sub, delta_off + 2 * lo))) + ro := int(tt_u16(sub, range_off + 2 * lo)) + if ro == 0 { + // idDelta is added modulo 65536, which is why it is signed and why + // the sum has to wrap rather than saturate. + return u16((u32(cc) + delta) & 0xFFFF) + } + // idRangeOffset is a byte offset from its own slot, which is what lets + // several segments share one glyphIdArray. + addr := range_off + 2 * lo + ro + 2 * int(cc - start) + if addr < 0 || addr + 2 > len(sub) { return 0 } + g := tt_u16(sub, addr) + if g == 0 { return 0 } + return u16((u32(g) + delta) & 0xFFFF) + + case 6: + // A single trimmed range. + first := u32(tt_u16(sub, 6)) + count := u32(tt_u16(sub, 8)) + if c < first || c - first >= count { return 0 } + off := 10 + 2 * int(c - first) + if off + 2 > len(sub) { return 0 } + return tt_u16(sub, off) + + case 12: + // Sorted groups covering the whole code space. + n := int(min(tt_u32(sub, 12), u32(1 << 24))) + have := (len(sub) - 16) / 12 + if have < 0 { have = 0 } + if n > have { n = have } + lo, hi := 0, n + for lo < hi { + mid := lo + (hi - lo) / 2 + rec := 16 + mid * 12 + if c < tt_u32(sub, rec) { + hi = mid + } else if c > tt_u32(sub, rec + 4) { + lo = mid + 1 + } else { + g := u64(tt_u32(sub, rec + 8)) + u64(c - tt_u32(sub, rec)) + return u16(g) if g <= 0xFFFF else 0 + } + } + return 0 + } + return 0 +} + +// tt_gid_for_unicode maps a Unicode code point through the font's Unicode +// cmap. ok is false when the font has no such table or no glyph for the +// character. +@(private) +tt_gid_for_unicode :: proc(f: ^TrueType_Font, r: rune) -> (u16, bool) { + if f == nil || r < 0 || r > 0x10FFFF { return 0, false } + g := tt_cmap_lookup(f.cmap_unicode, u32(r)) + return g, g != 0 +} + +// tt_gid_for_mac_code maps a byte through the (1,0) Mac Roman cmap, which is +// what a non-symbolic TrueType font's built-in encoding amounts to. +@(private) +tt_gid_for_mac_code :: proc(f: ^TrueType_Font, code: u8) -> (u16, bool) { + if f == nil { return 0, false } + g := tt_cmap_lookup(f.cmap_mac, u32(code)) + return g, g != 0 +} + +// tt_gid_for_symbol maps a byte through the (3,0) symbol cmap. +// +// A symbol font's cmap is keyed in the private-use area: the convention is +// 0xF000 plus the byte, and it is only a convention -- fonts exist that map the +// bare byte, and fonts exist that do both. Both are tried, which is what every +// viewer does and the only way Wingdings and Symbol come out right. +@(private) +tt_gid_for_symbol :: proc(f: ^TrueType_Font, code: u8) -> (u16, bool) { + if f == nil { return 0, false } + if g := tt_cmap_lookup(f.cmap_symbol, u32(code)); g != 0 { + return g, true + } + if g := tt_cmap_lookup(f.cmap_symbol, 0xF000 | u32(code)); g != 0 { + return g, true + } + return 0, false +} + +// --------------------------------------------------------------------------- +// post +// --------------------------------------------------------------------------- + +// tt_parse_post reads glyph names. ISO 14496-22, 5.2.9. +// +// Format 2.0 is the only one that carries names of its own; 1.0 says the font +// is exactly the 258 standard Macintosh glyphs in order, and 3.0 -- what a +// subsetter emits -- says there are no names at all, which is not an error. +@(private) +tt_parse_post :: proc(f: ^TrueType_Font, post: []byte, allocator: mem.Allocator) { + if len(post) < 32 || f.num_glyphs <= 0 { return } + switch tt_u32(post, 0) { + case 0x00010000: + n := min(f.num_glyphs, len(MAC_GLYPH_NAMES)) + names := make([]string, n, allocator) + for i in 0 ..< n { + names[i] = MAC_GLYPH_NAMES[i] + } + f.names = names + + case 0x00020000: + n := int(tt_u16(post, 32)) + if n > f.num_glyphs { n = f.num_glyphs } + // A post whose index array is cut short still names the glyphs it + // reaches. + if 34 + 2 * n > len(post) { n = (len(post) - 34) / 2 } + if n <= 0 { return } + + // The names themselves are Pascal strings in one run after the index + // array, and an index of 258 or more selects the (index-258)th of + // them. A well-formed font has at most one string per glyph, so + // collecting more than that would only be following damage. + strs := make([dynamic]string, 0, min(n, 64), context.allocator) + defer delete(strs) + q := 34 + 2 * n + for len(strs) < n && q < len(post) { + l := int(post[q]) + q += 1 + if q + l > len(post) { break } + append(&strs, string(post[q:][:l])) + q += l + } + + names := make([]string, n, allocator) + for i in 0 ..< n { + idx := int(tt_u16(post, 34 + 2 * i)) + if idx < len(MAC_GLYPH_NAMES) { + names[i] = MAC_GLYPH_NAMES[idx] + } else if idx - len(MAC_GLYPH_NAMES) < len(strs) { + names[i] = strs[idx - len(MAC_GLYPH_NAMES)] + } + } + f.names = names + } +} + +// tt_gid_for_name looks a glyph up by the name its `post` table gives it. ok is +// false when the font has no names or none of them match. +// +// The scan is linear. A simple font asks at most 256 times and a CID font never +// asks at all, so the index a binary search would need would cost more memory +// than the search saves. +@(private) +tt_gid_for_name :: proc(f: ^TrueType_Font, name: string) -> (u16, bool) { + if f == nil || len(name) == 0 { return 0, false } + for i in 0 ..< len(f.names) { + if f.names[i] == name { + return u16(i), true + } + } + return 0, false +} + +// The standard Macintosh glyph ordering: the 258 names a `post` table refers to +// by index alone. ISO 14496-22, 5.2.9, table 32. +@(private, rodata) +MAC_GLYPH_NAMES := [258]string{ + ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", + "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", + "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", + "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", + "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", + "backslash", "bracketright", "asciicircum", "underscore", "grave", + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", + "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", + "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", + "adieresis", "atilde", "aring", "ccedilla", "eacute", "egrave", + "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", + "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", + "uacute", "ugrave", "ucircumflex", "udieresis", "dagger", "degree", "cent", + "sterling", "section", "bullet", "paragraph", "germandbls", "registered", + "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash", + "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", + "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", + "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", + "logicalnot", "radical", "florin", "approxequal", "Delta", "guillemotleft", + "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde", + "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", + "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", + "fraction", "currency", "guilsinglleft", "guilsinglright", "fi", "fl", + "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", + "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", + "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", + "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", + "dotlessi", "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", + "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", "Scaron", + "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", + "Thorn", "thorn", "minus", "multiply", "onesuperior", "twosuperior", + "threesuperior", "onehalf", "onequarter", "threequarters", "franc", + "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", + "cacute", "Ccaron", "ccaron", "dcroat", +} diff --git a/font_truetype_test.odin b/font_truetype_test.odin new file mode 100644 index 0000000..c7d97ee --- /dev/null +++ b/font_truetype_test.odin @@ -0,0 +1,1515 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:testing" +import "core:time" + +// --------------------------------------------------------------------------- +// Fixture and font assembly +// +// Every font under test is built in a local arena, so a failing assertion +// cannot leak: the arena is destroyed on the way out whatever happened. +// +// The tables themselves are byte literals -- the bytes are the thing under +// test -- and only the scaffolding around them is assembled: the sfnt table +// directory, whose layout is uninteresting, and the `head`/`maxp`/`hhea` +// headers, which exist only to carry three numbers each. +// --------------------------------------------------------------------------- + +@(private="file") +Tt_Fixture :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private="file") +ttf_init :: proc(f: ^Tt_Fixture) { + _ = virtual.arena_init_growing(&f.arena) + f.alloc = virtual.arena_allocator(&f.arena) +} + +@(private="file") +ttf_destroy :: proc(f: ^Tt_Fixture) { + virtual.arena_destroy(&f.arena) +} + +@(private="file") +Tt_Table :: struct { + tag: string, + data: []byte, +} + +// ttf_font writes a table directory around `tables`, in the order given. That +// order is deliberately not sorted by tag in most tests: the format requires +// sorted records and real files disobey, so the reader must not depend on it. +@(private="file") +ttf_font :: proc(f: ^Tt_Fixture, tables: []Tt_Table, sfnt: u32 = 0x00010000) -> []byte { + total := 12 + 16 * len(tables) + for t in tables { + total += (len(t.data) + 3) & ~int(3) + } + out := make([]byte, total, f.alloc) + + put32 :: proc(b: []byte, off: int, v: u32) { + b[off] = u8(v >> 24) + b[off + 1] = u8(v >> 16) + b[off + 2] = u8(v >> 8) + b[off + 3] = u8(v) + } + put32(out, 0, sfnt) + out[4] = u8(len(tables) >> 8) + out[5] = u8(len(tables)) + + pos := 12 + 16 * len(tables) + for t, i in tables { + rec := 12 + 16 * i + for k in 0 ..< 4 { + out[rec + k] = t.tag[k] if k < len(t.tag) else ' ' + } + put32(out, rec + 8, u32(pos)) + put32(out, rec + 12, u32(len(t.data))) + copy(out[pos:], t.data) + pos += (len(t.data) + 3) & ~int(3) + } + return out +} + +@(private="file") +ttf_bytes :: proc(f: ^Tt_Fixture, vals: ..u8) -> []byte { + out := make([]byte, len(vals), f.alloc) + copy(out, vals) + return out +} + +// ttf_head is the 54-byte header, of which only unitsPerEm (18) and +// indexToLocFormat (50) are read. +@(private="file") +ttf_head :: proc(f: ^Tt_Fixture, upem: u16, long_loca: bool) -> []byte { + out := make([]byte, 54, f.alloc) + out[18] = u8(upem >> 8) + out[19] = u8(upem) + out[51] = 1 if long_loca else 0 + return out +} + +@(private="file") +ttf_maxp :: proc(f: ^Tt_Fixture, num_glyphs: u16) -> []byte { + out := make([]byte, 32, f.alloc) + out[0] = 0x00 + out[1] = 0x01 + out[4] = u8(num_glyphs >> 8) + out[5] = u8(num_glyphs) + return out +} + +// ttf_hhea is the 36-byte header whose last field is numberOfHMetrics. +@(private="file") +ttf_hhea :: proc(f: ^Tt_Fixture, n: u16) -> []byte { + out := make([]byte, 36, f.alloc) + out[34] = u8(n >> 8) + out[35] = u8(n) + return out +} + +@(private="file") +ttf_loca_short :: proc(f: ^Tt_Fixture, offs: ..u16) -> []byte { + out := make([]byte, 2 * len(offs), f.alloc) + for v, i in offs { + out[2 * i] = u8(v >> 8) + out[2 * i + 1] = u8(v) + } + return out +} + +@(private="file") +ttf_loca_long :: proc(f: ^Tt_Fixture, offs: ..u32) -> []byte { + out := make([]byte, 4 * len(offs), f.alloc) + for v, i in offs { + out[4 * i] = u8(v >> 24) + out[4 * i + 1] = u8(v >> 16) + out[4 * i + 2] = u8(v >> 8) + out[4 * i + 3] = u8(v) + } + return out +} + +@(private="file") +Tt_Cmap_Rec :: struct { + plat, enc: u16, + sub: []byte, +} + +// ttf_cmap wraps subtables in a cmap header: version, count, and one +// (platform, encoding, offset) record each. +@(private="file") +ttf_cmap :: proc(f: ^Tt_Fixture, recs: ..Tt_Cmap_Rec) -> []byte { + total := 4 + 8 * len(recs) + for r in recs { + total += len(r.sub) + } + out := make([]byte, total, f.alloc) + out[3] = u8(len(recs)) // version 0, numTables + pos := 4 + 8 * len(recs) + for r, i in recs { + rec := 4 + 8 * i + out[rec] = u8(r.plat >> 8) + out[rec + 1] = u8(r.plat) + out[rec + 2] = u8(r.enc >> 8) + out[rec + 3] = u8(r.enc) + out[rec + 4] = u8(pos >> 24) + out[rec + 5] = u8(pos >> 16) + out[rec + 6] = u8(pos >> 8) + out[rec + 7] = u8(pos) + copy(out[pos:], r.sub) + pos += len(r.sub) + } + return out +} + +// --------------------------------------------------------------------------- +// Glyph programs, as byte literals +// --------------------------------------------------------------------------- + +// GLYF_SQUARE is one contour of four on-curve points at (0,0) (100,0) +// (100,100) (0,100), 22 bytes. Its flags exercise every coordinate encoding +// there is: SAME (a zero delta with no byte at all), a positive short vector, +// and a signed 16-bit delta. +@(private="file") +GLYF_SQUARE := [?]u8{ + 0x00, 0x01, // numberOfContours = 1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, // bbox 0 0 100 100 + 0x00, 0x03, // endPtsOfContours = {3} + 0x00, 0x00, // instructionLength = 0 + 0x31, // on, x same, y same + 0x33, // on, x short +, y same + 0x35, // on, x same, y short + + 0x21, // on, x word, y same + 0x64, 0xFF, 0x9C, // x: +100, then -100 as a word + 0x64, // y: +100 +} + +// GLYF_QUAD has an on-curve point, two consecutive off-curve points -- which +// imply an on-curve point at their midpoint (100,100) -- and a closing +// on-curve point. Points: (0,0) (50,100)off (150,100)off (100,0). Its last +// point is reached by a negative short vector in both axes. 24 bytes, the last +// one padding. +@(private="file") +GLYF_QUAD := [?]u8{ + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x00, 0x64, // bbox 0 0 150 100 + 0x00, 0x03, + 0x00, 0x00, + 0x31, // on, x same, y same + 0x36, // off, x short +, y short + + 0x32, // off, x short +, y same + 0x07, // on, x short -, y short - + 0x32, 0x64, 0x32, // x: +50, +100, -50 + 0x64, 0x64, // y: +100, -100 + 0x00, +} + +// GLYF_LINE is two points whose x deltas are both signed words and whose y +// deltas are both the SAME encoding, so the last byte of the glyph belongs to +// a coordinate: the shape a truncation sweep needs to reach the 16-bit read. +@(private="file") +GLYF_LINE := [?]u8{ + 0x00, 0x01, + 0x00, 0x64, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, // bbox 100 0 400 0 + 0x00, 0x01, // endPtsOfContours = {1} + 0x00, 0x00, + 0x21, 0x21, // on, x word, y same + 0x00, 0x64, 0x01, 0x2C, // x: +100, +300 +} + +// GLYF_REPEAT is four on-curve points written with the REPEAT flag: one flag +// byte covers all four. Deltas (10,10) (20,0) (0,20) (30,30), so the points are +// (10,10) (30,10) (30,30) (60,60). +@(private="file") +GLYF_REPEAT := [?]u8{ + 0x00, 0x01, + 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x3C, 0x00, 0x3C, + 0x00, 0x03, + 0x00, 0x00, + 0x3F, 0x03, // on, both short +, REPEAT 3 + 0x0A, 0x14, 0x00, 0x1E, // x deltas + 0x0A, 0x00, 0x14, 0x1E, // y deltas +} + +// GLYF_CONTOURS has three contour end indices, the second and third of which +// go backwards: {5, 2, 5} over six points. Only the first is a contour; the +// other two must not re-cut points that already belong to it. +@(private="file") +GLYF_CONTOURS := [?]u8{ + 0x00, 0x03, // numberOfContours = 3 + 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x3C, 0x00, 0x3C, + 0x00, 0x05, 0x00, 0x02, 0x00, 0x05, // endPtsOfContours + 0x00, 0x00, + 0x3F, 0x05, // on, both short +, REPEAT 5 + 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, // x deltas + 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, // y deltas +} + +// GLYF_CIRCLE is a contour with no on-curve point at all: four control points +// at the corners of a square. Legal, and produced by real fonts. +@(private="file") +GLYF_CIRCLE := [?]u8{ + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, + 0x00, 0x03, + 0x00, 0x00, + 0x30, // off, x same, y same + 0x32, // off, x short +, y same + 0x34, // off, x same, y short + + 0x20, // off, x word, y same + 0x64, 0xFF, 0x9C, + 0x64, +} + +// --------------------------------------------------------------------------- +// Assertions +// --------------------------------------------------------------------------- + +@(private="file") +ttf_expect_path :: proc(t: ^testing.T, p: ^Path, verbs: []Path_Verb, pts: []Point, + loc := #caller_location) { + if !testing.expectf(t, len(p.verbs) == len(verbs), "%v verbs, wanted %v", + p.verbs[:], verbs, loc = loc) { + return + } + for v, i in verbs { + if !testing.expectf(t, p.verbs[i] == v, "verb %d is %v, wanted %v (%v)", + i, p.verbs[i], v, p.verbs[:], loc = loc) { + return + } + } + if !testing.expectf(t, len(p.points) == len(pts), "%d points, wanted %d: %v", + len(p.points), len(pts), p.points[:], loc = loc) { + return + } + for want, i in pts { + got := p.points[i] + ok := abs(got.x - want.x) < 1e-6 && abs(got.y - want.y) < 1e-6 + testing.expectf(t, ok, "point %d is %v, wanted %v", i, got, want, loc = loc) + } +} + +// ttf_square_font is the font most tests start from: two glyphs, the blank +// .notdef and the square, with a short loca. +@(private="file") +ttf_square_font :: proc(f: ^Tt_Fixture) -> ^TrueType_Font { + data := ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 1000, false)}, + {"maxp", ttf_maxp(f, 2)}, + {"loca", ttf_loca_short(f, 0, 0, 11)}, // glyph 0 empty, glyph 1 at 0..22 + {"glyf", GLYF_SQUARE[:]}, + }) + font, err := tt_parse(data, f.alloc) + if err != .None { return nil } + return font +} + +// ttf_glyf concatenates glyph programs into a `glyf` table, padding each to a +// four-byte boundary, and returns the loca offsets that go with it. +@(private="file") +ttf_glyf :: proc(f: ^Tt_Fixture, parts: ..[]byte) -> (data: []byte, offs: []u32) { + total := 0 + for p in parts { + total += (len(p) + 3) & ~int(3) + } + data = make([]byte, total, f.alloc) + offs = make([]u32, len(parts) + 1, f.alloc) + pos := 0 + for p, i in parts { + offs[i] = u32(pos) + copy(data[pos:], p) + pos += (len(p) + 3) & ~int(3) + } + offs[len(parts)] = u32(pos) + return +} + +// ttf_glyph_font builds a font whose only tables are head, maxp, loca and +// glyf, with one glyph per part and a long loca. +@(private="file") +ttf_glyph_font :: proc(f: ^Tt_Fixture, parts: ..[]byte) -> ^TrueType_Font { + glyf, offs := ttf_glyf(f, ..parts) + data := ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 1000, true)}, + {"maxp", ttf_maxp(f, u16(len(parts)))}, + {"loca", ttf_loca_long(f, ..offs)}, + {"glyf", glyf}, + }) + font, err := tt_parse(data, f.alloc) + if err != .None { return nil } + return font +} + +// --------------------------------------------------------------------------- +// Simple glyphs +// --------------------------------------------------------------------------- + +@(test) +test_tt_simple_glyph :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_square_font(&f) + if !testing.expect(t, font != nil, "parse failed") { return } + testing.expect_value(t, tt_glyph_count(font), 2) + testing.expect_value(t, tt_units_per_em(font), 1000) + testing.expect(t, tt_has_glyf(font), "font has glyf outlines") + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + // The last point of the contour is the one the Close verb returns to, so + // it is not emitted twice. + ttf_expect_path(t, &p, + {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) + + // Glyph 0 is a legitimately empty entry: no outline, and not an error. + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 0, &p), Error.None) + testing.expect(t, path_is_empty(&p), "empty loca entry draws nothing") + + // Past numGlyphs there is no glyph at all, which is a different answer. + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.No_Glyph) +} + +@(test) +test_tt_implied_midpoint :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_glyph_font(&f, nil, GLYF_QUAD[:]) + if !testing.expect(t, font != nil, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + + // (0,0) on, (50,100) off, (150,100) off, (100,0) on. The two off-curve + // points imply an on-curve point at (100,100) between them, so the contour + // is two quadratics, each written as the cubic with controls two thirds of + // the way to the quadratic's control point. + K :: 2.0 / 3.0 + ttf_expect_path(t, &p, + {.Move, .Curve, .Curve, .Close}, + { + {0, 0}, + {50 * K, 100 * K}, {100 - 50 * K, 100}, {100, 100}, + {100 + 50 * K, 100}, {100 + 50 * K, 100 * K}, {100, 0}, + }) +} + +@(test) +test_tt_all_off_curve :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_glyph_font(&f, nil, GLYF_CIRCLE[:]) + if !testing.expect(t, font != nil, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + + // No point is on the curve, so the contour starts at the midpoint of the + // last and first control points -- (0,100) and (0,0) -- and runs as four + // quadratics from midpoint to midpoint, ending back where it started. + K :: 2.0 / 3.0 + M :: 50 * K + ttf_expect_path(t, &p, + {.Move, .Curve, .Curve, .Curve, .Curve, .Close}, + { + {0, 50}, + {0, 50 - M}, {50 - M, 0}, {50, 0}, + {50 + M, 0}, {100, 50 - M}, {100, 50}, + {100, 50 + M}, {50 + M, 100}, {50, 100}, + {50 - M, 100}, {0, 50 + M}, {0, 50}, + }) +} + +@(test) +test_tt_backwards_contour_ends :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_glyph_font(&f, nil, GLYF_CONTOURS[:]) + if !testing.expect(t, font != nil, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + // The first end index claims all six points. The two after it are dropped + // rather than allowed to carve a second contour out of the same points. + ttf_expect_path(t, &p, + {.Move, .Line, .Line, .Line, .Line, .Line, .Close}, + {{10, 10}, {20, 20}, {30, 30}, {40, 40}, {50, 50}, {60, 60}}) +} + +@(test) +test_tt_repeat_flag :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_glyph_font(&f, nil, GLYF_REPEAT[:]) + if !testing.expect(t, font != nil, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + ttf_expect_path(t, &p, + {.Move, .Line, .Line, .Line, .Close}, + {{10, 10}, {30, 10}, {30, 30}, {60, 60}}) +} + +// --------------------------------------------------------------------------- +// loca +// --------------------------------------------------------------------------- + +@(test) +test_tt_loca_short_and_long :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // The same glyph reached through a short loca, whose entries are halved, + // and a long one, whose entries are not. + short_font := ttf_square_font(&f) + long_data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 2)}, + {"loca", ttf_loca_long(&f, 0, 0, 22)}, + {"glyf", GLYF_SQUARE[:]}, + }) + long_font, err := tt_parse(long_data, f.alloc) + if !testing.expect(t, err == .None && short_font != nil, "parse failed") { return } + testing.expect(t, !short_font.long_loca && long_font.long_loca, "loca formats differ") + + a := path_make() + defer path_destroy(&a) + b := path_make() + defer path_destroy(&b) + testing.expect_value(t, tt_glyph_path(short_font, 1, &a), Error.None) + testing.expect_value(t, tt_glyph_path(long_font, 1, &b), Error.None) + ttf_expect_path(t, &a, {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) + ttf_expect_path(t, &b, {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) +} + +@(test) +test_tt_loca_non_monotonic :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // Two copies of the square, with glyph 1 pointed backwards through the + // middle of the table: from 24 down to 0. A subsetter that rewrites loca + // badly produces exactly this. The backwards entry is an empty glyph, and + // the glyph after it still has to work. + glyf, _ := ttf_glyf(&f, GLYF_SQUARE[:], GLYF_SQUARE[:]) + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 3)}, + {"loca", ttf_loca_long(&f, 0, 24, 0, 48)}, + {"glyf", glyf}, + }) + font, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + testing.expect(t, path_is_empty(&p), "a backwards loca pair is an empty glyph") + + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.None) + ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) +} + +@(test) +test_tt_glyph_runs_past_table :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // loca claims the glyph is a kilobyte long in a 22-byte glyf: the entry is + // clamped to the table, and the glyph inside it is complete, so it draws. + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 3)}, + {"loca", ttf_loca_long(&f, 0, 0, 1024, 4096)}, + {"glyf", GLYF_SQUARE[:]}, + }) + font, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + ttf_expect_path(t, &p, {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) + + // Glyph 2 starts past the end of the table entirely. + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.None) + testing.expect(t, path_is_empty(&p), "a glyph beyond glyf draws nothing") +} + +@(test) +test_tt_truncated_glyph :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // Every prefix of a valid glyph: the header, the contour ends, the flags + // and both coordinate arrays each run out somewhere. The three shapes end + // on a different kind of field -- a short y delta, a padding byte and the + // second half of a 16-bit x delta -- so no one bounds check stands in for + // the others. + cases := [?]struct { + glyph: []byte, + complete: int, // bytes before the padding, if any + }{ + {GLYF_SQUARE[:], 22}, + {GLYF_QUAD[:], 23}, + {GLYF_LINE[:], 20}, + } + for c in cases { + for cut in 1 ..= len(c.glyph) { + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 2)}, + {"loca", ttf_loca_long(&f, 0, 0, u32(cut))}, + {"glyf", c.glyph[:cut]}, + }) + font, err := tt_parse(data, f.alloc) + if !testing.expectf(t, err == .None, "cut %d: parse failed", cut) { continue } + p := path_make() + defer path_destroy(&p) + gerr := tt_glyph_path(font, 1, &p) + if cut >= c.complete { + testing.expectf(t, gerr == .None, "cut %d: got %v", cut, gerr) + } else { + testing.expectf(t, gerr == .Bad_TrueType, "cut %d: got %v", cut, gerr) + testing.expectf(t, path_is_empty(&p), "cut %d: emitted %v", cut, p.verbs[:]) + } + } + } +} + +// --------------------------------------------------------------------------- +// Composite glyphs +// --------------------------------------------------------------------------- + +// A component with a word-sized x/y offset and nothing else. +@(private="file") +GLYF_COMP_OFFSET := [?]u8{ + 0xFF, 0xFF, // numberOfContours = -1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // bbox + 0x00, 0x03, // ARG_1_AND_2_ARE_WORDS | ARGS_ARE_XY_VALUES + 0x00, 0x01, // component glyph 1 + 0x00, 0xC8, 0x00, 0x32, // dx = 200, dy = 50 +} + +// The same, with WE_HAVE_A_SCALE. The offset is not scaled, which is the +// Microsoft behaviour and the default. +@(private="file") +GLYF_COMP_SCALE := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0B, // WORDS | XY | WE_HAVE_A_SCALE + 0x00, 0x01, + 0x00, 0x0A, 0x00, 0x14, // dx = 10, dy = 20 + 0x20, 0x00, // scale = 0.5 in F2Dot14 +} + +// SCALED_COMPONENT_OFFSET puts the offset in the component's space: 101 halves +// to 50.5. Without ROUND_XY_TO_GRID that half unit survives. +@(private="file") +GLYF_COMP_SCALED_OFFSET := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0B, // WORDS | XY | SCALE | SCALED_OFFSET + 0x00, 0x01, + 0x00, 0x65, 0x00, 0x00, // dx = 101, dy = 0 + 0x20, 0x00, +} + +// The same again with ROUND_XY_TO_GRID, which rounds 50.5 to 51. +@(private="file") +GLYF_COMP_SCALED_ROUND := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0F, // ... | ROUND_XY_TO_GRID + 0x00, 0x01, + 0x00, 0x65, 0x00, 0x00, + 0x20, 0x00, +} + +// A TWO_BY_TWO transform: the matrix {0, 1, -1, 0} rotates a quarter turn. +@(private="file") +GLYF_COMP_2X2 := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x83, // WORDS | XY | WE_HAVE_A_TWO_BY_TWO + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x00, 0x00, // a=0 b=1 c=-1 d=0 +} + +// Two components: the second is placed by matching its point 0 onto point 2 of +// the composite so far, rather than by an offset. +@(private="file") +GLYF_COMP_MATCH := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, // WORDS | XY | MORE_COMPONENTS + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // byte args, point matching, last + 0x00, 0x01, + 0x02, 0x00, // composite point 2, component point 0 +} + +// A composite that is its own component: glyph 2 of the font it is put in. +@(private="file") +GLYF_COMP_SELF := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, + 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, +} + +// USE_MY_METRICS: the composite's advance comes from component glyph 1. +@(private="file") +GLYF_COMP_METRICS := [?]u8{ + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x03, // WORDS | XY | USE_MY_METRICS + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, +} + +@(private="file") +SQUARE_VERBS := [?]Path_Verb{.Move, .Line, .Line, .Line, .Close} + +@(private="file") +ttf_composite_path :: proc(t: ^testing.T, f: ^Tt_Fixture, comp: []byte, p: ^Path) -> bool { + font := ttf_glyph_font(f, nil, GLYF_SQUARE[:], comp) + if !testing.expect(t, font != nil, "parse failed") { return false } + err := tt_glyph_path(font, 2, p) + return testing.expectf(t, err == .None, "glyph 2: %v", err) +} + +@(test) +test_tt_composite_offset :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + p := path_make() + defer path_destroy(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_OFFSET[:], &p) { return } + ttf_expect_path(t, &p, SQUARE_VERBS[:], + {{200, 50}, {300, 50}, {300, 150}, {200, 150}}) +} + +@(test) +test_tt_composite_scale :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + p := path_make() + defer path_destroy(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_SCALE[:], &p) { return } + ttf_expect_path(t, &p, SQUARE_VERBS[:], + {{10, 20}, {60, 20}, {60, 70}, {10, 70}}) +} + +@(test) +test_tt_composite_scaled_offset_and_rounding :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + p := path_make() + defer path_destroy(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_SCALED_OFFSET[:], &p) { return } + ttf_expect_path(t, &p, SQUARE_VERBS[:], + {{50.5, 0}, {100.5, 0}, {100.5, 50}, {50.5, 50}}) + + path_clear(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_SCALED_ROUND[:], &p) { return } + ttf_expect_path(t, &p, SQUARE_VERBS[:], + {{51, 0}, {101, 0}, {101, 50}, {51, 50}}) +} + +@(test) +test_tt_composite_two_by_two :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + p := path_make() + defer path_destroy(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_2X2[:], &p) { return } + // (x,y) -> (a*x + c*y, b*x + d*y) = (-y, x): a quarter turn. + ttf_expect_path(t, &p, SQUARE_VERBS[:], + {{0, 0}, {0, 100}, {-100, 100}, {-100, 0}}) +} + +@(test) +test_tt_composite_point_match :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + p := path_make() + defer path_destroy(&p) + if !ttf_composite_path(t, &f, GLYF_COMP_MATCH[:], &p) { return } + // The first copy sits at the origin. The second moves so that its point 0, + // at (0,0), lands on the composite's point 2, at (100,100). + verbs := [?]Path_Verb{ + .Move, .Line, .Line, .Line, .Close, + .Move, .Line, .Line, .Line, .Close, + } + ttf_expect_path(t, &p, verbs[:], { + {0, 0}, {100, 0}, {100, 100}, {0, 100}, + {100, 100}, {200, 100}, {200, 200}, {100, 200}, + }) +} + +@(test) +test_tt_composite_self_reference :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_glyph_font(&f, nil, GLYF_SQUARE[:], GLYF_COMP_SELF[:]) + if !testing.expect(t, font != nil, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + start := time.now() + err := tt_glyph_path(font, 2, &p) + elapsed := time.since(start) + testing.expect_value(t, err, Error.Recursion_Limit) + testing.expect(t, path_is_empty(&p), "a glyph that is its own component draws nothing") + testing.expectf(t, elapsed < time.Second, "took %v", elapsed) +} + +// --------------------------------------------------------------------------- +// hmtx +// --------------------------------------------------------------------------- + +@(test) +test_tt_hmtx_trailing_run :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // Two long metrics for four glyphs: everything from glyph 1 on shares the + // last advance, which is how a font with a long tail of equal-width glyphs + // stays small. ISO 14496-22, 5.2.4. + hmtx := ttf_bytes(&f, + 0x01, 0xF4, 0x00, 0x00, // glyph 0: advance 500 + 0x02, 0x58, 0x00, 0x00, // glyph 1: advance 600 + 0x00, 0x0A, // glyph 2: left side bearing only + 0x00, 0x14) // glyph 3: left side bearing only + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 2048, false)}, + {"maxp", ttf_maxp(&f, 4)}, + {"hhea", ttf_hhea(&f, 2)}, + {"hmtx", hmtx}, + }) + font, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + + testing.expect_value(t, tt_units_per_em(font), 2048) + testing.expect_value(t, tt_glyph_advance(font, 0), 500) + testing.expect_value(t, tt_glyph_advance(font, 1), 600) + testing.expect_value(t, tt_glyph_advance(font, 2), 600) + testing.expect_value(t, tt_glyph_advance(font, 3), 600) + // Past the end of the font the answer is still the last advance rather + // than a read off the end of the table. + testing.expect_value(t, tt_glyph_advance(font, 400), 600) +} + +@(test) +test_tt_hmtx_without_hhea :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // No hhea at all, so nothing says how many long metrics hmtx holds. The + // first entry stands in for every glyph rather than every glyph coming + // back zero wide and piling up on one spot. + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, false)}, + {"maxp", ttf_maxp(&f, 4)}, + {"hmtx", ttf_bytes(&f, 0x01, 0xF4, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00)}, + }) + font, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + testing.expect_value(t, tt_glyph_advance(font, 0), 500) + testing.expect_value(t, tt_glyph_advance(font, 3), 500) + + // With no hmtx either there is nothing to report. + data = ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, false)}, + {"maxp", ttf_maxp(&f, 4)}, + }) + font, err = tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + testing.expect_value(t, tt_glyph_advance(font, 0), 0) +} + +@(test) +test_tt_use_my_metrics :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + check :: proc(t: ^testing.T, f: ^Tt_Fixture, comp: []byte) -> f64 { + glyf, offs := ttf_glyf(f, nil, GLYF_SQUARE[:], comp) + hmtx := ttf_bytes(f, + 0x01, 0xF4, 0x00, 0x00, // glyph 0: 500 + 0x02, 0x58, 0x00, 0x00, // glyph 1: 600 + 0x03, 0xE7, 0x00, 0x00) // glyph 2: 999 + data := ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 1000, true)}, + {"maxp", ttf_maxp(f, 3)}, + {"hhea", ttf_hhea(f, 3)}, + {"hmtx", hmtx}, + {"loca", ttf_loca_long(f, ..offs)}, + {"glyf", glyf}, + }) + font, err := tt_parse(data, f.alloc) + if err != .None || font == nil { return -1 } + return tt_glyph_advance(font, 2) + } + + // The composite's own hmtx entry says 999; USE_MY_METRICS makes it take + // component glyph 1's 600 instead. + testing.expect_value(t, check(t, &f, GLYF_COMP_METRICS[:]), 600) + // Without the flag the composite keeps its own. + testing.expect_value(t, check(t, &f, GLYF_COMP_OFFSET[:]), 999) +} + +// --------------------------------------------------------------------------- +// cmap +// --------------------------------------------------------------------------- + +// Three segments. The first maps 'A'..'C' through idDelta; the second maps +// 0x100..0x102 through the glyphIdArray indirection, with a zero in the middle +// standing for a code the font does not have; the third is the 0xFFFF +// terminator every format 4 table must end with. searchRange, entrySelector +// and rangeShift are deliberately zero: they are redundant with segCountX2 and +// wrong often enough that this reader recomputes the search instead. +@(private="file") +CMAP4 := [?]u8{ + 0x00, 0x04, // format + 0x00, 0x2E, // length = 46 + 0x00, 0x00, // language + 0x00, 0x06, // segCountX2 = 6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x43, 0x01, 0x02, 0xFF, 0xFF, // endCode + 0x00, 0x00, // reservedPad + 0x00, 0x41, 0x01, 0x00, 0xFF, 0xFF, // startCode + 0xFF, 0xC0, 0x00, 0x01, 0x00, 0x01, // idDelta + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // idRangeOffset + 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0C, // glyphIdArray = 10, missing, 12 +} + +// 'A'..'C' and two astral code points, in the 32-bit grouped format. +@(private="file") +CMAP12 := [?]u8{ + 0x00, 0x0C, 0x00, 0x00, // format 12, reserved + 0x00, 0x00, 0x00, 0x28, // length = 40 + 0x00, 0x00, 0x00, 0x00, // language + 0x00, 0x00, 0x00, 0x02, // nGroups = 2 + 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x01, 0xF6, 0x00, 0x00, 0x01, 0xF6, 0x01, 0x00, 0x00, 0x00, 0x64, +} + +// A trimmed table covering three codes. +@(private="file") +CMAP6 := [?]u8{ + 0x00, 0x06, // format + 0x00, 0x10, // length = 16 + 0x00, 0x00, // language + 0x00, 0x41, // firstCode = 'A' + 0x00, 0x03, // entryCount + 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, +} + +// A (3,0) symbol table: one glyph at the bare byte 0x42 and one in the +// private-use area at 0xF041, so that both halves of the 0xF000 convention are +// exercised by one table. +@(private="file") +CMAP4_SYMBOL := [?]u8{ + 0x00, 0x04, + 0x00, 0x28, // length = 40 + 0x00, 0x00, + 0x00, 0x06, // segCountX2 = 6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x42, 0xF0, 0x41, 0xFF, 0xFF, // endCode + 0x00, 0x00, + 0x00, 0x42, 0xF0, 0x41, 0xFF, 0xFF, // startCode + 0xFF, 0xC2, 0x0F, 0xC2, 0x00, 0x01, // idDelta: 0x42->4, 0xF041->3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // idRangeOffset +} + +// A format 2 stub: the high-byte scheme for legacy CJK encodings, which this +// reader does not implement and must therefore not choose. +@(private="file") +CMAP2_STUB := [?]u8{ + 0x00, 0x02, 0x02, 0x0C, 0x00, 0x00, +} + +// ttf_cmap0 builds a 262-byte byte-encoding table with the given code/glyph +// pairs set. +@(private="file") +ttf_cmap0 :: proc(f: ^Tt_Fixture, pairs: ..[2]u8) -> []byte { + out := make([]byte, 262, f.alloc) + out[1] = 0 // format 0 + out[2] = 0x01 // length 262 + out[3] = 0x06 + for pr in pairs { + out[6 + int(pr[0])] = pr[1] + } + return out +} + +@(private="file") +ttf_cmap_font :: proc(f: ^Tt_Fixture, recs: ..Tt_Cmap_Rec) -> ^TrueType_Font { + data := ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 1000, false)}, + {"maxp", ttf_maxp(f, 200)}, + {"cmap", ttf_cmap(f, ..recs)}, + }) + font, err := tt_parse(data, f.alloc) + if err != .None { return nil } + return font +} + +@(private="file") +ttf_expect_gid :: proc(t: ^testing.T, got: u16, ok: bool, want: u16, loc := #caller_location) { + testing.expectf(t, ok == (want != 0), "found=%v, wanted %v", ok, want != 0, loc = loc) + testing.expectf(t, got == want, "gid %d, wanted %d", got, want, loc = loc) +} + +@(test) +test_tt_cmap_format4 :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_cmap_font(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + + // The first segment: gid = code + idDelta, modulo 65536. + g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 1) + g, ok = tt_gid_for_unicode(font, 'B'); ttf_expect_gid(t, g, ok, 2) + g, ok = tt_gid_for_unicode(font, 'C'); ttf_expect_gid(t, g, ok, 3) + // Below the first segment's startCode, and between two segments: the + // search lands on a segment that does not contain the code. + g, ok = tt_gid_for_unicode(font, 0x40); ttf_expect_gid(t, g, ok, 0) + g, ok = tt_gid_for_unicode(font, 'D'); ttf_expect_gid(t, g, ok, 0) + // The second segment: gid comes from glyphIdArray through idRangeOffset, + // with idDelta added to what it finds there. + g, ok = tt_gid_for_unicode(font, 0x100); ttf_expect_gid(t, g, ok, 11) + g, ok = tt_gid_for_unicode(font, 0x102); ttf_expect_gid(t, g, ok, 13) + // A zero in glyphIdArray means no glyph, and idDelta is not added to it. + g, ok = tt_gid_for_unicode(font, 0x101); ttf_expect_gid(t, g, ok, 0) + // The terminating segment maps 0xFFFF to 0. + g, ok = tt_gid_for_unicode(font, 0xFFFF); ttf_expect_gid(t, g, ok, 0) + // Beyond the BMP a format 4 table has nothing at all. + g, ok = tt_gid_for_unicode(font, 0x1F600); ttf_expect_gid(t, g, ok, 0) +} + +@(test) +test_tt_cmap_format12 :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_cmap_font(&f, Tt_Cmap_Rec{3, 10, CMAP12[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + + g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) + g, ok = tt_gid_for_unicode(font, 'C'); ttf_expect_gid(t, g, ok, 7) + g, ok = tt_gid_for_unicode(font, 0x40); ttf_expect_gid(t, g, ok, 0) + g, ok = tt_gid_for_unicode(font, 0x44); ttf_expect_gid(t, g, ok, 0) + // The second group is past the BMP, which is the whole point of format 12. + g, ok = tt_gid_for_unicode(font, 0x1F600); ttf_expect_gid(t, g, ok, 100) + g, ok = tt_gid_for_unicode(font, 0x1F601); ttf_expect_gid(t, g, ok, 101) + g, ok = tt_gid_for_unicode(font, 0x1F602); ttf_expect_gid(t, g, ok, 0) +} + +@(test) +test_tt_cmap_mac_and_symbol :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_cmap_font(&f, + Tt_Cmap_Rec{1, 0, ttf_cmap0(&f, {0x41, 5}, {0xA5, 6})}, + Tt_Cmap_Rec{3, 0, CMAP4_SYMBOL[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + + // (1,0) is a Mac Roman byte table and is reached only through the mac + // entry point: it is not a Unicode cmap and must not be used as one. + g, ok := tt_gid_for_mac_code(font, 0x41); ttf_expect_gid(t, g, ok, 5) + g, ok = tt_gid_for_mac_code(font, 0xA5); ttf_expect_gid(t, g, ok, 6) + g, ok = tt_gid_for_mac_code(font, 0x42); ttf_expect_gid(t, g, ok, 0) + g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 0) + + // (3,0) is a symbol table: 0x42 is mapped by the bare byte and 0x41 only + // through the 0xF000 convention. Both have to be tried. + g, ok = tt_gid_for_symbol(font, 0x42); ttf_expect_gid(t, g, ok, 4) + g, ok = tt_gid_for_symbol(font, 0x41); ttf_expect_gid(t, g, ok, 3) + g, ok = tt_gid_for_symbol(font, 0x99); ttf_expect_gid(t, g, ok, 0) + // The same font read as Unicode has nothing: a (3,0) table is not one. + g, ok = tt_gid_for_unicode(font, 0x42); ttf_expect_gid(t, g, ok, 0) +} + +@(test) +test_tt_cmap_format6 :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // A second subtable follows this one, so the bytes after its entryCount + // belong to another table: a code past the end of the trimmed range has to + // be rejected by the count, not by running out of buffer. + font := ttf_cmap_font(&f, Tt_Cmap_Rec{1, 0, CMAP6[:]}, Tt_Cmap_Rec{3, 1, CMAP4[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + + g, ok := tt_gid_for_mac_code(font, 0x41); ttf_expect_gid(t, g, ok, 7) + g, ok = tt_gid_for_mac_code(font, 0x43); ttf_expect_gid(t, g, ok, 9) + g, ok = tt_gid_for_mac_code(font, 0x40); ttf_expect_gid(t, g, ok, 0) + g, ok = tt_gid_for_mac_code(font, 0x44); ttf_expect_gid(t, g, ok, 0) +} + +@(test) +test_tt_cmap_preference :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // (3,1) and (3,10) both map 'A', to different glyphs. The full-repertoire + // table wins whichever order the records are in. + font := ttf_cmap_font(&f, + Tt_Cmap_Rec{3, 1, CMAP4[:]}, + Tt_Cmap_Rec{3, 10, CMAP12[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + g, ok := tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) + + font = ttf_cmap_font(&f, + Tt_Cmap_Rec{3, 10, CMAP12[:]}, + Tt_Cmap_Rec{3, 1, CMAP4[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 5) + + // A (3,1) in a format this reader cannot read must not be chosen over a + // platform-0 table it can. + font = ttf_cmap_font(&f, + Tt_Cmap_Rec{3, 1, CMAP2_STUB[:]}, + Tt_Cmap_Rec{0, 3, CMAP4[:]}) + if !testing.expect(t, font != nil, "parse failed") { return } + g, ok = tt_gid_for_unicode(font, 'A'); ttf_expect_gid(t, g, ok, 1) + + // A record whose offset is past the end of the table is skipped rather + // than followed. + bad := ttf_cmap(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]}) + bad[10] = 0xFF // the record's offset, high byte + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, false)}, + {"maxp", ttf_maxp(&f, 200)}, + {"cmap", bad}, + }) + broken, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + g, ok = tt_gid_for_unicode(broken, 'A'); ttf_expect_gid(t, g, ok, 0) +} + +@(test) +test_tt_no_cmap :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // A font with no cmap and no post at all parses: those tables are how a + // glyph is found by character, and a CIDFontType2 subset needs neither. + font := ttf_square_font(&f) + if !testing.expect(t, font != nil, "parse failed") { return } + _, ok := tt_gid_for_unicode(font, 'A') + testing.expect(t, !ok, "no unicode cmap") + _, ok = tt_gid_for_mac_code(font, 0x41) + testing.expect(t, !ok, "no mac cmap") + _, ok = tt_gid_for_symbol(font, 0x41) + testing.expect(t, !ok, "no symbol cmap") + _, ok = tt_gid_for_name(font, "A") + testing.expect(t, !ok, "no post table") +} + +// --------------------------------------------------------------------------- +// post +// --------------------------------------------------------------------------- + +// ttf_post prefixes the 32-byte header -- a version and nine fields nothing +// here reads -- to a literal tail. +@(private="file") +ttf_post :: proc(f: ^Tt_Fixture, version: u32, tail: []u8) -> []byte { + out := make([]byte, 32 + len(tail), f.alloc) + out[0] = u8(version >> 24) + out[1] = u8(version >> 16) + out[2] = u8(version >> 8) + out[3] = u8(version) + copy(out[32:], tail) + return out +} + +// Three glyphs: one named by a standard Macintosh index, one by a Pascal +// string of its own, and one by a standard index that is not zero. +@(private="file") +POST2_TAIL := [?]u8{ + 0x00, 0x03, // numberOfGlyphs + 0x00, 0x00, 0x01, 0x02, 0x00, 0x03, // indices: 0, 258, 3 + 0x06, 'A', 'a', 'c', 'u', 't', 'e', // the one custom name +} + +@(private="file") +ttf_post_font :: proc(f: ^Tt_Fixture, post: []byte) -> ^TrueType_Font { + data := ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 1000, false)}, + {"maxp", ttf_maxp(f, 3)}, + {"post", post}, + }) + font, err := tt_parse(data, f.alloc) + if err != .None { return nil } + return font +} + +@(test) +test_tt_post_names :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + font := ttf_post_font(&f, ttf_post(&f, 0x00020000, POST2_TAIL[:])) + if !testing.expect(t, font != nil, "parse failed") { return } + + g, ok := tt_gid_for_name(font, ".notdef") + testing.expect(t, ok && g == 0, "index below 258 is a standard Macintosh name") + g, ok = tt_gid_for_name(font, "Aacute") + testing.expect(t, ok && g == 1, "index 258 is the first Pascal string") + g, ok = tt_gid_for_name(font, "space") + testing.expect(t, ok && g == 2, "index 3 is the fourth standard name") + _, ok = tt_gid_for_name(font, "Q") + testing.expect(t, !ok, "a name the font does not have") + _, ok = tt_gid_for_name(font, "") + testing.expect(t, !ok, "the empty name never matches") + + // Format 1.0 says the font is the 258 standard glyphs in order. + font = ttf_post_font(&f, ttf_post(&f, 0x00010000, nil)) + if !testing.expect(t, font != nil, "parse failed") { return } + g, ok = tt_gid_for_name(font, "nonmarkingreturn") + testing.expect(t, ok && g == 2, "format 1.0 is the standard order") + + // Format 3.0 -- what a subsetter writes -- has no names, which is not an + // error and must not stop the font loading. + font = ttf_post_font(&f, ttf_post(&f, 0x00030000, nil)) + if !testing.expect(t, font != nil, "parse failed") { return } + _, ok = tt_gid_for_name(font, "space") + testing.expect(t, !ok, "format 3.0 carries no names") + + // A post cut off inside its Pascal strings still names the glyphs whose + // names arrived. + truncated := ttf_post(&f, 0x00020000, POST2_TAIL[:len(POST2_TAIL) - 3]) + font = ttf_post_font(&f, truncated) + if !testing.expect(t, font != nil, "parse failed") { return } + g, ok = tt_gid_for_name(font, "space") + testing.expect(t, ok && g == 2, "the standard names survive a cut") + _, ok = tt_gid_for_name(font, "Aacute") + testing.expect(t, !ok, "the cut-off Pascal string is dropped") +} + +// --------------------------------------------------------------------------- +// The table directory +// --------------------------------------------------------------------------- + +@(test) +test_tt_otto_has_no_glyf :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + cff := ttf_bytes(&f, 0x01, 0x00, 0x04, 0x02, 0xDE, 0xAD) + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, false)}, + {"maxp", ttf_maxp(&f, 4)}, + {"CFF ", cff}, + }, 0x4F54544F) + font, err := tt_parse(data, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + + testing.expect(t, font.is_otto, "OTTO was recognised") + testing.expect(t, !tt_has_glyf(font), "CFF outlines, not glyf") + got := tt_cff_data(font) + testing.expect_value(t, len(got), len(cff)) + testing.expect(t, len(got) == len(cff) && got[4] == 0xDE, "the CFF table came back") + + // Asking for an outline from a font that has none is not a crash. + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.No_Glyph) +} + +// ttf_collection wraps one font in a TrueType Collection header, moving its +// tables and fixing up the offsets in its directory. +@(private="file") +ttf_collection :: proc(f: ^Tt_Fixture, font: []byte) -> []byte { + HDR :: 16 + out := make([]byte, HDR + len(font), f.alloc) + copy(out, []byte{'t', 't', 'c', 'f', 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, HDR}) + copy(out[HDR:], font) + n := int(u16(font[4]) << 8 | u16(font[5])) + for i in 0 ..< n { + rec := HDR + 12 + 16 * i + off := u32(out[rec + 8]) << 24 | u32(out[rec + 9]) << 16 | + u32(out[rec + 10]) << 8 | u32(out[rec + 11]) + off += HDR + out[rec + 8] = u8(off >> 24) + out[rec + 9] = u8(off >> 16) + out[rec + 10] = u8(off >> 8) + out[rec + 11] = u8(off) + } + return out +} + +@(test) +test_tt_collection :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + inner := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, false)}, + {"maxp", ttf_maxp(&f, 2)}, + {"loca", ttf_loca_short(&f, 0, 0, 11)}, + {"glyf", GLYF_SQUARE[:]}, + }) + font, err := tt_parse(ttf_collection(&f, inner), f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) +} + +@(test) +test_tt_directory_tolerances :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + // Not a font at all. + _, err := tt_parse(nil, f.alloc) + testing.expect_value(t, err, Error.Bad_TrueType) + _, err = tt_parse(ttf_bytes(&f, 'O', 'T', 'T', 'O'), f.alloc) + testing.expect_value(t, err, Error.Bad_TrueType) + junk := ttf_font(&f, []Tt_Table{{"head", ttf_head(&f, 1000, false)}}, 0x25504446) + _, err = tt_parse(junk, f.alloc) + testing.expect_value(t, err, Error.Bad_TrueType) + + // unitsPerEm of zero would scale every glyph to infinity. + data := ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 0, false)}, + {"maxp", ttf_maxp(&f, 2)}, + }) + font, perr := tt_parse(data, f.alloc) + if !testing.expect(t, perr == .None, "parse failed") { return } + testing.expect_value(t, tt_units_per_em(font), 1000) + + // No maxp: the glyph count comes from loca, which has one entry more than + // there are glyphs. + data = ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"loca", ttf_loca_long(&f, 0, 0, 22)}, + {"glyf", GLYF_SQUARE[:]}, + }) + font, perr = tt_parse(data, f.alloc) + if !testing.expect(t, perr == .None, "parse failed") { return } + testing.expect_value(t, tt_glyph_count(font), 2) + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) + + // maxp says two glyphs while loca describes three. The count in maxp is + // what a cmap's glyph ids are validated against, so it wins: the third + // entry is not reachable however complete it looks. + two_squares, offs := ttf_glyf(&f, nil, GLYF_SQUARE[:], GLYF_SQUARE[:]) + data = ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 2)}, + {"loca", ttf_loca_long(&f, ..offs)}, + {"glyf", two_squares}, + }) + font, perr = tt_parse(data, f.alloc) + if !testing.expect(t, perr == .None, "parse failed") { return } + testing.expect_value(t, tt_glyph_count(font), 2) + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + testing.expect(t, !path_is_empty(&p), "glyph 1 is inside the count") + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 2, &p), Error.No_Glyph) + + // A record whose length runs past the end of the file is clamped to it, + // and one whose offset does is dropped. Both are ordinary damage in an + // embedded font program. + data = ttf_font(&f, []Tt_Table{ + {"head", ttf_head(&f, 1000, true)}, + {"maxp", ttf_maxp(&f, 2)}, + {"loca", ttf_loca_long(&f, 0, 0, 22)}, + {"glyf", GLYF_SQUARE[:]}, + {"cmap", ttf_cmap(&f, Tt_Cmap_Rec{3, 1, CMAP4[:]})}, + }) + glyf_rec := 12 + 16 * 3 + data[glyf_rec + 12] = 0xFF // glyf length, far past the end + cmap_rec := 12 + 16 * 4 + data[cmap_rec + 8] = 0x7F // cmap offset, past the end + font, perr = tt_parse(data, f.alloc) + if !testing.expect(t, perr == .None, "parse failed") { return } + path_clear(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + ttf_expect_path(t, &p, SQUARE_VERBS[:], {{0, 0}, {100, 0}, {100, 100}, {0, 100}}) + _, ok := tt_gid_for_unicode(font, 'A') + testing.expect(t, !ok, "a table past the end of the file is not there") +} + +// --------------------------------------------------------------------------- +// Damage +// --------------------------------------------------------------------------- + +// ttf_rich_font is a font with one of everything: two real glyphs, a +// composite, all three kinds of cmap subtable, metrics and names. It is the +// input the damage tests chew on. +@(private="file") +ttf_rich_font :: proc(f: ^Tt_Fixture) -> []byte { + glyf, offs := ttf_glyf(f, nil, GLYF_SQUARE[:], GLYF_QUAD[:], GLYF_COMP_MATCH[:]) + return ttf_font(f, []Tt_Table{ + {"head", ttf_head(f, 2048, true)}, + {"maxp", ttf_maxp(f, 4)}, + {"hhea", ttf_hhea(f, 2)}, + {"hmtx", ttf_bytes(f, 0x01, 0xF4, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x14)}, + {"loca", ttf_loca_long(f, ..offs)}, + {"glyf", glyf}, + {"cmap", ttf_cmap(f, + Tt_Cmap_Rec{3, 1, CMAP4[:]}, + Tt_Cmap_Rec{3, 0, CMAP4_SYMBOL[:]}, + Tt_Cmap_Rec{1, 0, CMAP6[:]})}, + {"post", ttf_post(f, 0x00020000, POST2_TAIL[:])}, + }) +} + +// ttf_exercise calls every entry point over a spread of glyph ids and codes. +// It asserts nothing: what it is for is to be run against damaged input under +// the bounds checker, where a read off the end of a table is a crash. +@(private="file") +ttf_exercise :: proc(font: ^TrueType_Font) { + if font == nil { return } + _ = tt_glyph_count(font) + _ = tt_units_per_em(font) + _ = tt_has_glyf(font) + _ = tt_cff_data(font) + p := path_make() + defer path_destroy(&p) + for gid in u16(0) ..= 6 { + path_clear(&p) + _ = tt_glyph_path(font, gid, &p) + _ = tt_glyph_advance(font, gid) + } + _ = tt_glyph_path(font, 65535, &p) + _ = tt_glyph_advance(font, 65535) + for c in rune(0x40) ..= 0x44 { + _, _ = tt_gid_for_unicode(font, c) + } + _, _ = tt_gid_for_unicode(font, 0x1F600) + _, _ = tt_gid_for_unicode(font, 0x10FFFF) + for c in u8(0x40) ..= 0x44 { + _, _ = tt_gid_for_mac_code(font, c) + _, _ = tt_gid_for_symbol(font, c) + } + _, _ = tt_gid_for_name(font, "space") + _, _ = tt_gid_for_name(font, "Aacute") +} + +@(test) +test_tt_truncated_font :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + whole := ttf_rich_font(&f) + start := time.now() + for cut in 0 ..= len(whole) { + font, err := tt_parse(whole[:cut], f.alloc) + // Whether a prefix parses depends on where it was cut; what matters is + // that everything it then answers is bounded by the bytes it has. + testing.expectf(t, (font != nil) == (err == .None), "cut %d: %v", cut, err) + ttf_exercise(font) + } + elapsed := time.since(start) + testing.expectf(t, elapsed < 30 * time.Second, "took %v", elapsed) + + // The whole font still works, so the sweep above was not measuring a font + // that never parsed in the first place. + font, err := tt_parse(whole, f.alloc) + if !testing.expect(t, err == .None, "parse failed") { return } + g, ok := tt_gid_for_unicode(font, 'A') + testing.expect(t, ok && g == 1, "the undamaged font maps 'A'") + p := path_make() + defer path_destroy(&p) + testing.expect_value(t, tt_glyph_path(font, 1, &p), Error.None) + testing.expect(t, !path_is_empty(&p), "the undamaged font draws glyph 1") +} + +@(test) +test_tt_corrupted_font :: proc(t: ^testing.T) { + f: Tt_Fixture + ttf_init(&f) + defer ttf_destroy(&f) + + whole := ttf_rich_font(&f) + scratch := make([]byte, len(whole), f.alloc) + start := time.now() + for pattern in ([?]u8{0x00, 0xFF, 0x7F}) { + for i in 0 ..< len(whole) { + copy(scratch, whole) + scratch[i] = pattern + font, err := tt_parse(scratch, f.alloc) + testing.expectf(t, (font != nil) == (err == .None), "byte %d: %v", i, err) + ttf_exercise(font) + } + } + elapsed := time.since(start) + testing.expectf(t, elapsed < 30 * time.Second, "took %v", elapsed) +} diff --git a/font_type1.odin b/font_type1.odin new file mode 100644 index 0000000..c18160e --- /dev/null +++ b/font_type1.odin @@ -0,0 +1,1533 @@ +package pdf + +import "core:mem" +import "core:slice" + +// Type 1 font programs (Adobe, "Adobe Type 1 Font Format", 1990). +// +// Bytes in, outlines out. Nothing here knows what a PDF is; a Type 1 program +// is the same file whether it arrived as a /FontFile stream, a .pfb on disk or +// a .pfa pasted into a PostScript job, and keeping this file ignorant of the +// difference is what makes it testable from byte literals. +// +// SHAPE OF THE FILE +// +// %!FontType1-1.0 ... cleartext PostScript: /FontMatrix, /Encoding +// currentfile eexec +// the Private dict: /lenIV, /Subrs, /CharStrings +// 512 zeros, cleartomark trailer +// +// The encrypted portion is enciphered with the "eexec" stream cipher keyed at +// r = 55665, whose first four plaintext bytes are random padding. It arrives +// either as raw binary (.pfb, and PDF /FontFile streams) or as hex ASCII +// (.pfa); both are accepted, told apart by whether the first four bytes are hex +// digits -- which works because Adobe's own encryptor is required to choose +// padding that keeps the two forms distinguishable. Each charstring inside is +// enciphered again with the same cipher keyed at r = 4330, with /lenIV bytes +// (4 unless the font says otherwise) of padding to drop. +// +// A .pfb wraps the three parts in segment headers -- 0x80, a type byte, then a +// little-endian length -- which are stripped and the payloads concatenated +// before anything else looks at the bytes. +// +// WHAT SUBSETTERS DO TO THESE FILES +// +// Font programs are the most reliably damaged data in a PDF, because every +// producer rewrites them. So: a /Subrs or /CharStrings count that disagrees +// with the entries that follow is ignored in favour of the entries; a +// charstring shorter than /lenIV yields an empty glyph rather than an error; an +// eexec block that stops in the middle yields the glyphs decoded up to that +// point; and a font with no eexec section at all -- some are shipped with that +// layer already stripped -- is read as though its Private dict were the +// cleartext it is. + +// --------------------------------------------------------------------------- +// Limits. Every one of these bounds something an attacker controls. +// --------------------------------------------------------------------------- + +// Tokens read from one cleartext or Private section. These are data, not +// programs, so the cap only ever fires on garbage -- which is exactly what the +// tail of a truncated eexec block decrypts to. +@(private) +MAX_T1_TOKENS :: 1 << 20 +// Glyph indices are u16 everywhere in this layer, so the dictionary stops one +// short of where the index would stop being representable. +@(private) +MAX_T1_GLYPHS :: 0xFFFF +@(private) +MAX_T1_SUBRS :: 1 << 16 +// The charstring interpreter's bounds. The format allows 24 operands and a +// call depth of 10; the margin absorbs the fonts that exceed the spec without +// letting a hostile one run away. +@(private) +T1_STACK_MAX :: 48 +@(private) +T1_PS_MAX :: 32 +@(private) +T1_MAX_CALL_DEPTH :: 30 +// Charstring bytes executed for one glyph, subroutine calls included. +@(private) +T1_MAX_FUEL :: 1 << 17 +// Points a FLEX collects: a reference point plus the six that make two curves. +@(private) +T1_FLEX_POINTS :: 7 + +// The two cipher keys, and the constants they share (Type 1 spec, 7.3). +@(private) +T1_EEXEC_KEY :: 55665 +@(private) +T1_CHARSTRING_KEY :: 4330 + +// --------------------------------------------------------------------------- +// The parsed font +// --------------------------------------------------------------------------- + +// T1_Name_Entry is one glyph name and the glyph index it belongs to, in a table +// sorted by name so that a lookup is a binary search. A simple Type 1 font is +// addressed entirely by name, so this is the hot path. +@(private) +T1_Name_Entry :: struct { + name: string, + gid: u16, +} + +// Type1_Font is the parsed form of a font program. Declared here so that Font can +// reference it; the reader that fills it in owns this file and adds the fields +// it needs. Nothing in this file may depend on Document or on any PDF type -- +// a font program is bytes in and outlines out, and stays separately testable. +Type1_Font :: struct { + // The whole program with any PFB segment headers removed. When the input + // was not PFB-wrapped this aliases the caller's bytes rather than copying + // them, so `data` passed to `t1_parse` must outlive the font: the built-in + // encoding's glyph names point into it. + data: []byte, + // The decrypted Private dictionary, always a buffer of this font's own. + // Glyph names, charstrings and subroutines point into it. + priv: []byte, + + // /FontName, for diagnostics only. + name: string, + // /FontMatrix, defaulting to the 1000-units-per-em [0.001 0 0 0.001 0 0]. + mtx: Matrix, + + // The program's built-in encoding: 256 glyph names, "" where the font + // leaves a code undefined. Meaningful only when `has_encoding` is set. + encoding: [256]string, + has_encoding: bool, + + // /lenIV: bytes of padding at the head of every decrypted charstring. + // Negative means the charstrings are not enciphered at all. + len_iv: int, + + subrs: [][]byte, + names: []string, + chars: [][]byte, + // `names` sorted, for t1_gid_for_name. + by_name: []T1_Name_Entry, + + // Advances are memoised: laying out a page asks for the same handful of + // glyphs thousands of times, and each answer costs a charstring run. + advances: []f64, + adv_known: []bool, + + // Set when the eexec block or a charstring ran off the end of the input. + // The font is still usable; it simply holds fewer glyphs than it claims. + truncated: bool, +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +// t1_parse reads a Type 1 font program: PFB-wrapped, raw binary or PFA. +// +// The returned font and everything it points at are allocated with `allocator` +// and belong to the caller; nothing is freed individually. `data` is referenced, +// not copied, so it must outlive the font. +// +// An error comes back only when there is genuinely nothing to hand over -- no +// charstring could be read at all. A font that is damaged but still holds one +// glyph parses successfully, because a page that draws that glyph should draw +// it. +@(private) +t1_parse :: proc(data: []byte, allocator := context.allocator) -> (^Type1_Font, Error) { + if len(data) == 0 { + return nil, .Bad_Type1 + } + + f := new(Type1_Font, allocator) + if f == nil { + return nil, .Out_Of_Memory + } + f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} + f.len_iv = 4 + + raw, raw_owned := t1_unwrap_pfb(data, allocator) + f.data = raw + + // The cleartext is everything before `eexec`. For a font that has none, + // stop at the first thing that introduces binary data instead, so the + // tokeniser never walks over charstring bytes. + clear_end := len(raw) + eexec_at := t1_find_token(raw, "eexec") + if eexec_at >= 0 { + clear_end = eexec_at + } + if p := t1_find_token(raw[:clear_end], "/CharStrings"); p >= 0 { + clear_end = p + } + if p := t1_find_token(raw[:clear_end], "/Subrs"); p >= 0 { + clear_end = p + } + t1_parse_cleartext(f, raw[:clear_end]) + + // The encrypted region: everything after `eexec` and the whitespace that + // follows it. Adobe requires the first byte of the ciphertext not to be + // whitespace precisely so that skipping it is unambiguous (Type 1 spec, + // 7.2). + body := raw + encrypted := false + if eexec_at >= 0 { + i := eexec_at + len("eexec") + for i < len(raw) && t1_is_space(raw[i]) { + i += 1 + } + body = raw[i:] + encrypted = true + } + // A font whose eexec layer has already been stripped still says `eexec`. + // Twelve plaintext bytes surviving encipherment is not a thing that + // happens, so finding the token means the region is not enciphered. + if encrypted && t1_find_token(body, "/CharStrings") >= 0 { + encrypted = false + } + + // `f.priv` is always the whole allocation, so it can be handed back to the + // allocator; `priv_body` is the part after the four bytes of random + // padding at the head of the eexec plaintext. + priv_body: []byte + switch { + case encrypted && t1_is_hex_eexec(body): + buf := make([]byte, len(body) / 2 + 1, allocator) + n := t1_hex_decode(buf, body) + t1_decipher(buf[:n], T1_EEXEC_KEY) + f.priv = buf + priv_body = n >= 4 ? buf[4:n] : buf[n:n] + case encrypted: + f.priv = t1_copy(body, allocator) + t1_decipher(f.priv, T1_EEXEC_KEY) + priv_body = len(f.priv) >= 4 ? f.priv[4:] : f.priv[len(f.priv):] + case: + f.priv = t1_copy(body, allocator) + priv_body = f.priv + } + + ctx := T1_Parse_Ctx{ + f = f, + subrs = make([dynamic][]byte, allocator), + names = make([dynamic]string, allocator), + chars = make([dynamic][]byte, allocator), + } + t1_parse_private(&ctx, priv_body) + + if len(ctx.chars) == 0 { + delete(ctx.subrs) + delete(ctx.names) + delete(ctx.chars) + delete(f.priv, allocator) + if raw_owned { + delete(raw, allocator) + } + free(f, allocator) + return nil, .Bad_Type1 + } + + f.subrs = ctx.subrs[:] + f.names = ctx.names[:] + f.chars = ctx.chars[:] + + n := len(f.names) + f.by_name = make([]T1_Name_Entry, n, allocator) + for i in 0 ..< n { + f.by_name[i] = T1_Name_Entry{name = f.names[i], gid = u16(i)} + } + slice.sort_by(f.by_name, proc(a, b: T1_Name_Entry) -> bool { + return a.name < b.name + }) + + f.advances = make([]f64, n, allocator) + f.adv_known = make([]bool, n, allocator) + + return f, .None +} + +// t1_glyph_count is the number of charstrings the font holds. Glyph indices +// are positions in the /CharStrings dictionary, in the order it was written. +@(private) +t1_glyph_count :: proc(f: ^Type1_Font) -> int { + if f == nil { + return 0 + } + return len(f.chars) +} + +// t1_font_matrix maps glyph space to text space. Outlines from t1_glyph_path +// are in glyph space and have not been through it. +@(private) +t1_font_matrix :: proc(f: ^Type1_Font) -> Matrix { + if f == nil { + return Matrix{0.001, 0, 0, 0.001, 0, 0} + } + return f.mtx +} + +// t1_builtin_encoding is the font program's own encoding, or nil when it +// declares none -- which the caller must read as "use StandardEncoding", +// not as "this font has no glyphs at those codes". +@(private) +t1_builtin_encoding :: proc(f: ^Type1_Font) -> ^[256]string { + if f == nil || !f.has_encoding { + return nil + } + return &f.encoding +} + +// t1_gid_for_name resolves a glyph name against the /CharStrings dictionary. +@(private) +t1_gid_for_name :: proc(f: ^Type1_Font, name: string) -> (u16, bool) { + if f == nil || len(name) == 0 { + return 0, false + } + lo, hi := 0, len(f.by_name) + for lo < hi { + mid := lo + (hi - lo) / 2 + if f.by_name[mid].name < name { + lo = mid + 1 + } else { + hi = mid + } + } + if lo < len(f.by_name) && f.by_name[lo].name == name { + return f.by_name[lo].gid, true + } + return 0, false +} + +// t1_glyph_advance is the width `hsbw` or `sbw` declares, in glyph space. +// Zero for a glyph that declares none, which is what a viewer advances by. +@(private) +t1_glyph_advance :: proc(f: ^Type1_Font, gid: u16) -> f64 { + if f == nil || int(gid) >= len(f.chars) { + return 0 + } + i := int(gid) + if i < len(f.adv_known) && f.adv_known[i] { + return f.advances[i] + } + + ip := T1_Interp{width_only = true} + t1_run(f, i, nil, Point{0, 0}, &ip) + w := ip.have_width ? ip.width : 0 + if !is_finite(w) { + w = 0 + } + if i < len(f.adv_known) { + f.advances[i] = w + f.adv_known[i] = true + } + return w +} + +// t1_glyph_path appends one glyph's outline, in glyph space, to `out`. +// +// A glyph that cannot be interpreted appends whatever it managed to draw and +// still returns .None: a broken glyph is a blank, never a failed page. Only an +// index that names no glyph is an error. +@(private) +t1_glyph_path :: proc(f: ^Type1_Font, gid: u16, out: ^Path) -> Error { + if f == nil || out == nil { + return .Invalid_Argument + } + if int(gid) >= len(f.chars) { + return .No_Glyph + } + + ip := T1_Interp{allow_seac = true} + t1_run(f, int(gid), out, Point{0, 0}, &ip) + if !ip.seac { + return .None + } + + // seac composes an accented glyph out of two others named by their + // StandardEncoding codes -- the one place a font program refers to an + // encoding it does not carry. The table is font_standard.odin's, which is + // the same table the format's Annex D defines. + asb := ip.seac_args[0] + adx := ip.seac_args[1] + ady := ip.seac_args[2] + if !is_finite(asb) || !is_finite(adx) || !is_finite(ady) { + return .None + } + bcode := int(ip.seac_args[3]) + acode := int(ip.seac_args[4]) + std := encoding_standard() + + if bcode >= 0 && bcode < 256 { + if bgid, ok := t1_gid_for_name(f, std[bcode]); ok { + base := T1_Interp{} + t1_run(f, int(bgid), out, Point{0, 0}, &base) + } + } + if acode >= 0 && acode < 256 { + if agid, ok := t1_gid_for_name(f, std[acode]); ok { + // adx/ady position the accent's left sidebearing point relative to + // the COMPOSITE's left sidebearing point, not its origin -- so the + // composite's own sidebearing, which `ip.sbx` holds, is part of the + // offset, and `asb` (the accent's sidebearing, which the accent's + // own hsbw is about to apply again) is taken back out. + // + // Getting this wrong is invisible in the many fonts whose accented + // glyphs have a zero sidebearing and glaring in the ones that do + // not: dropping `ip.sbx` moves the dieresis off the I by the width + // of the sidebearing in 104 of the 448 seac composites in the + // Bitstream and URW faces this was checked against. + acc := T1_Interp{} + t1_run(f, int(agid), out, Point{ip.sbx + adx - asb, ady}, &acc) + } + } + return .None +} + +// --------------------------------------------------------------------------- +// Container: PFB segments, eexec, and the two ciphers +// --------------------------------------------------------------------------- + +@(private = "file") +t1_copy :: proc(src: []byte, allocator: mem.Allocator) -> []byte { + if len(src) == 0 { + return nil + } + out := make([]byte, len(src), allocator) + if len(out) != len(src) { + return nil + } + copy(out, src) + return out +} + +// t1_unwrap_pfb strips .pfb segment headers and concatenates the payloads. +// Returns the input untouched, and owned = false, for anything else. +@(private = "file") +t1_unwrap_pfb :: proc(data: []byte, allocator: mem.Allocator) -> (out: []byte, owned: bool) { + if len(data) < 6 || data[0] != 0x80 { + return data, false + } + + // Two passes so the join is exactly sized: a declared length is a file + // value and must not be believed as an allocation size. + total := 0 + for pass in 0 ..< 2 { + pos := 0 + n := 0 + for pos + 6 <= len(data) { + if data[pos] != 0x80 { + break + } + kind := data[pos + 1] + // 3 is the end-of-file marker; anything else is not a segment. + if kind != 1 && kind != 2 { + break + } + seg := int(u32(data[pos + 2]) | + u32(data[pos + 3]) << 8 | + u32(data[pos + 4]) << 16 | + u32(data[pos + 5]) << 24) + pos += 6 + if seg > len(data) - pos { + seg = len(data) - pos + } + if seg <= 0 { + continue + } + if pass == 0 { + total += seg + } else { + copy(out[n:], data[pos:pos + seg]) + n += seg + } + pos += seg + } + if pass == 0 { + if total == 0 { + return data, false + } + out = make([]byte, total, allocator) + if len(out) != total { + return data, false + } + } + } + return out, true +} + +// t1_decipher runs the eexec cipher over `buf` in place (Type 1 spec, 7.3). +@(private = "file") +t1_decipher :: proc(buf: []byte, key: u16) { + r := u32(key) + for i in 0 ..< len(buf) { + c := u32(buf[i]) + buf[i] = u8(c) ~ u8(r >> 8) + r = ((c + r) * 52845 + 22719) & 0xFFFF + } +} + +// t1_is_hex_eexec tells the .pfa form from the binary one. Adobe requires the +// random padding to be chosen so that the first four ciphertext bytes are not +// all hex digits, which is what makes this test safe rather than a guess. +@(private = "file") +t1_is_hex_eexec :: proc(body: []byte) -> bool { + if len(body) < 4 { + return false + } + for i in 0 ..< 4 { + if _, ok := hex_value(body[i]); !ok { + return false + } + } + return true +} + +// t1_hex_decode decodes the hex-ASCII eexec form into `dst`, returning how many +// bytes it wrote. Decoding stops at the first character that is neither hex nor +// whitespace, which in a .pfa is the `cleartomark` trailer. +@(private = "file") +t1_hex_decode :: proc(dst, src: []byte) -> int { + n := 0 + hi: u8 + have_hi := false + for c in src { + if t1_is_space(c) { + continue + } + v, ok := hex_value(c) + if !ok { + break + } + if !have_hi { + hi = v + have_hi = true + continue + } + if n >= len(dst) { + break + } + dst[n] = hi << 4 | v + n += 1 + have_hi = false + } + return n +} + +// t1_decrypt_charstring deciphers one charstring in place and drops its /lenIV +// bytes of padding. A charstring shorter than the padding yields an empty +// glyph, which draws nothing -- the alternative is a slice out of range. +@(private = "file") +t1_decrypt_charstring :: proc(buf: []byte, len_iv: int) -> []byte { + if len_iv < 0 { + return buf + } + t1_decipher(buf, T1_CHARSTRING_KEY) + if len(buf) <= len_iv { + return buf[len(buf):] + } + return buf[len_iv:] +} + +// --------------------------------------------------------------------------- +// A PostScript-shaped tokeniser +// +// Only enough of the syntax to find the handful of constructs that matter. +// Strings and hex strings come back whole so that their contents can never be +// mistaken for keywords: a /Notice is allowed to contain the word "eexec". +// --------------------------------------------------------------------------- + +@(private = "file") +T1_Lexer :: struct { + src: []byte, + pos: int, +} + +@(private = "file") +t1_is_space :: proc "contextless" (c: byte) -> bool { + return c == 0 || c == 9 || c == 10 || c == 12 || c == 13 || c == 32 +} + +@(private = "file") +t1_is_special :: proc "contextless" (c: byte) -> bool { + switch c { + case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%': + return true + } + return false +} + +@(private = "file") +t1_next :: proc(lx: ^T1_Lexer) -> (tok: []byte, ok: bool) { + for { + for lx.pos < len(lx.src) && t1_is_space(lx.src[lx.pos]) { + lx.pos += 1 + } + if lx.pos >= len(lx.src) { + return nil, false + } + if lx.src[lx.pos] != '%' { + break + } + for lx.pos < len(lx.src) && lx.src[lx.pos] != '\n' && lx.src[lx.pos] != '\r' { + lx.pos += 1 + } + } + + start := lx.pos + switch lx.src[lx.pos] { + case '/': + lx.pos += 1 + for lx.pos < len(lx.src) && + !t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) { + lx.pos += 1 + } + case '(': + depth := 0 + for lx.pos < len(lx.src) { + c := lx.src[lx.pos] + lx.pos += 1 + switch c { + case '\\': + lx.pos += 1 + case '(': + depth += 1 + case ')': + depth -= 1 + } + if depth == 0 { + break + } + } + if lx.pos > len(lx.src) { + lx.pos = len(lx.src) + } + case '<': + lx.pos += 1 + for lx.pos < len(lx.src) && lx.src[lx.pos] != '>' { + lx.pos += 1 + } + if lx.pos < len(lx.src) { + lx.pos += 1 + } + case ')', '>', '[', ']', '{', '}': + lx.pos += 1 + case: + for lx.pos < len(lx.src) && + !t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) { + lx.pos += 1 + } + } + if lx.pos == start { + // Nothing consumed would stall the caller's loop; a delimiter with no + // body is still one byte of progress. + lx.pos += 1 + } + return lx.src[start:lx.pos], true +} + +@(private = "file") +t1_eq :: proc "contextless" (tok: []byte, s: string) -> bool { + return string(tok) == s +} + +// t1_tok_int reads a plain decimal integer, which is all the constructs here +// ever need. Values far past anything meaningful are rejected rather than +// wrapped, because every one of them is a length or a count. +@(private = "file") +t1_tok_int :: proc "contextless" (tok: []byte) -> (int, bool) { + if len(tok) == 0 { + return 0, false + } + i := 0 + neg := false + if tok[0] == '+' || tok[0] == '-' { + neg = tok[0] == '-' + i = 1 + } + if i >= len(tok) { + return 0, false + } + v := 0 + for ; i < len(tok); i += 1 { + c := tok[i] + if c < '0' || c > '9' { + return 0, false + } + if v > 1 << 30 { + return 0, false + } + v = v * 10 + int(c - '0') + } + return neg ? -v : v, true +} + +@(private = "file") +t1_next_int :: proc(lx: ^T1_Lexer) -> (int, bool) { + tok, ok := t1_next(lx) + if !ok { + return 0, false + } + return t1_tok_int(tok) +} + +// t1_find_token locates a whitespace-delimited keyword. Delimiting matters: +// `/Subrs` is not to be found inside `/SubrsCount`, and a bare substring search +// finds `eexec` inside the middle of a word. +@(private = "file") +t1_find_token :: proc(hay: []byte, needle: string) -> int { + if len(needle) == 0 || len(hay) < len(needle) { + return -1 + } + for i in 0 ..= len(hay) - len(needle) { + if hay[i] != needle[0] { + continue + } + if string(hay[i:i + len(needle)]) != needle { + continue + } + if i > 0 { + p := hay[i - 1] + if !t1_is_space(p) && p != ')' && p != ']' && p != '}' && p != '>' { + continue + } + } + j := i + len(needle) + if j < len(hay) { + c := hay[j] + if !t1_is_space(c) && !t1_is_special(c) { + continue + } + } + return i + } + return -1 +} + +// --------------------------------------------------------------------------- +// The cleartext portion +// --------------------------------------------------------------------------- + +@(private = "file") +t1_parse_cleartext :: proc(f: ^Type1_Font, src: []byte) { + lx := T1_Lexer{src = src} + for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 { + tok, ok := t1_next(&lx) + if !ok { + break + } + switch { + case t1_eq(tok, "/FontMatrix"): + t1_read_matrix(f, &lx) + case t1_eq(tok, "/Encoding"): + t1_read_encoding(f, &lx) + case t1_eq(tok, "/FontName"): + if name, nok := t1_next(&lx); nok && len(name) > 1 && name[0] == '/' { + f.name = string(name[1:]) + } + } + } +} + +// t1_read_matrix reads `[a b c d e f]`. A matrix that is not invertible, or +// that carries a NaN, is discarded in favour of the 1000-per-em default: it +// would otherwise collapse every glyph in the font to a point. +@(private = "file") +t1_read_matrix :: proc(f: ^Type1_Font, lx: ^T1_Lexer) { + open, ok := t1_next(lx) + if !ok || (!t1_eq(open, "[") && !t1_eq(open, "{")) { + return + } + vals: [6]f64 + n := 0 + for n < 6 { + tok, tok_ok := t1_next(lx) + if !tok_ok || t1_eq(tok, "]") || t1_eq(tok, "}") { + return + } + num, num_ok := parse_number(string(tok)) + v, v_ok := to_number(num) + if !num_ok || !v_ok { + return + } + vals[n] = v + n += 1 + } + m := Matrix{vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]} + if !matrix_is_finite(m) || matrix_determinant(m) == 0 { + return + } + f.mtx = m +} + +// t1_read_encoding reads both spellings the format allows: the +// `StandardEncoding` shorthand, and the `256 array ... dup / put` +// form that every subsetter emits. +@(private = "file") +t1_read_encoding :: proc(f: ^Type1_Font, lx: ^T1_Lexer) { + tok, ok := t1_next(lx) + if !ok { + return + } + if t1_eq(tok, "StandardEncoding") { + f.encoding = encoding_standard()^ + f.has_encoding = true + return + } + + depth := 0 + for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 { + t, tok_ok := t1_next(lx) + if !tok_ok { + return + } + switch { + case t1_eq(t, "{"): + depth += 1 + continue + case t1_eq(t, "}"): + depth -= 1 + continue + } + if depth <= 0 && (t1_eq(t, "def") || t1_eq(t, "readonly") || t1_eq(t, "eexec")) { + return + } + if depth != 0 || !t1_eq(t, "dup") { + continue + } + + // `dup / put`. The three tokens are read together, and + // anything that does not fit the shape is stepped over rather than + // consumed: a font is allowed to `dup` for other reasons. + save := lx.pos + code, code_ok := t1_next_int(lx) + name, name_ok := t1_next(lx) + put, put_ok := t1_next(lx) + if !code_ok || !name_ok || !put_ok || + code < 0 || code > 255 || len(name) < 2 || name[0] != '/' || + !t1_eq(put, "put") { + lx.pos = save + continue + } + f.encoding[code] = string(name[1:]) + f.has_encoding = true + } +} + +// --------------------------------------------------------------------------- +// The Private dictionary +// --------------------------------------------------------------------------- + +@(private = "file") +T1_Parse_Ctx :: struct { + f: ^Type1_Font, + subrs: [dynamic][]byte, + names: [dynamic]string, + chars: [dynamic][]byte, +} + +@(private = "file") +t1_parse_private :: proc(ctx: ^T1_Parse_Ctx, src: []byte) { + ctx.f.len_iv = t1_scan_len_iv(src) + + lx := T1_Lexer{src = src} + subrs_open := false + fuel := MAX_T1_TOKENS + for fuel > 0 { + fuel -= 1 + tok, ok := t1_next(&lx) + if !ok { + return + } + switch { + case t1_eq(tok, "/CharStrings"): + t1_skip_to_begin(&lx) + t1_read_charstrings(ctx, &lx, &fuel) + return + case t1_eq(tok, "/Subrs"): + // The count that follows is not read at all: the array grows to fit + // the entries that turn up, which is what a subsetter that dropped + // some without rewriting the count needs, and what stops a count of + // a billion from being an allocation. + subrs_open = true + case subrs_open && t1_eq(tok, "dup"): + t1_read_subr(ctx, &lx) + } + } +} + +// t1_scan_len_iv finds /lenIV before any binary data can be reached, so that a +// charstring byte pattern cannot be mistaken for the key. +@(private = "file") +t1_scan_len_iv :: proc(src: []byte) -> int { + limit := len(src) + if p := t1_find_token(src, "/Subrs"); p >= 0 { + limit = min(limit, p) + } + if p := t1_find_token(src, "/CharStrings"); p >= 0 { + limit = min(limit, p) + } + at := t1_find_token(src[:limit], "/lenIV") + if at < 0 { + return 4 + } + lx := T1_Lexer{src = src[:limit], pos = at} + if _, ok := t1_next(&lx); !ok { + return 4 + } + v, ok := t1_next_int(&lx) + // 0..16 is the padding a font can mean. A negative value is how the tools + // that strip the charstring cipher record that they did. + if !ok || v > 16 || v < -1 { + return 4 + } + return v +} + +// t1_skip_to_begin steps over the ` dict dup begin` that introduces the +// /CharStrings dictionary. A font that writes it differently is not followed +// off a cliff: the lexer is put back where it was and the entries are read from +// there. +@(private = "file") +t1_skip_to_begin :: proc(lx: ^T1_Lexer) { + save := lx.pos + for _ in 0 ..< 32 { + tok, ok := t1_next(lx) + if !ok { + break + } + if t1_eq(tok, "begin") { + return + } + if len(tok) > 0 && tok[0] == '/' { + break + } + } + lx.pos = save +} + +// t1_read_binary takes the `` bytes that follow an RD token. Exactly one +// delimiter separates the two, and the byte after it is data even when it looks +// like whitespace. +// +// A length that runs past the end of the input is clamped rather than rejected: +// that is a truncated eexec block, and the partial charstring it yields draws +// what it can. The clamp always leaves the lexer at the end of the input, so +// the truncation is recorded by the token read that follows, in +// t1_read_charstrings. +@(private = "file") +t1_read_binary :: proc(lx: ^T1_Lexer, n: int) -> ([]byte, bool) { + if lx.pos >= len(lx.src) || n < 0 { + return nil, false + } + lx.pos += 1 + take := min(n, len(lx.src) - lx.pos) + out := lx.src[lx.pos:lx.pos + take] + lx.pos += take + return out, true +} + +// t1_read_subr reads one `dup RD NP`. The RD token's name +// is whatever the font defined it as -- RD, -|, or anything else -- so it is +// read and discarded rather than matched. +@(private = "file") +t1_read_subr :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer) { + save := lx.pos + idx, idx_ok := t1_next_int(lx) + n, n_ok := t1_next_int(lx) + if !idx_ok || !n_ok || idx < 0 || n < 0 || idx >= MAX_T1_SUBRS { + lx.pos = save + return + } + if _, ok := t1_next(lx); !ok { + lx.pos = save + return + } + blob, blob_ok := t1_read_binary(lx, n) + if !blob_ok { + lx.pos = save + return + } + if idx >= len(ctx.subrs) { + resize(&ctx.subrs, idx + 1) + } + ctx.subrs[idx] = t1_decrypt_charstring(blob, ctx.f.len_iv) +} + +// t1_read_charstrings reads `/ RD ND` until `end`. +@(private = "file") +t1_read_charstrings :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer, fuel: ^int) { + for fuel^ > 0 { + fuel^ -= 1 + tok, ok := t1_next(lx) + if !ok { + ctx.f.truncated = true + return + } + if t1_eq(tok, "end") { + return + } + if len(tok) < 2 || tok[0] != '/' { + continue + } + + save := lx.pos + n, n_ok := t1_next_int(lx) + if !n_ok || n < 0 { + lx.pos = save + continue + } + // Both of these mean the dictionary stopped in the middle of an + // entry, which is what a truncated eexec block looks like from here. + if _, rd_ok := t1_next(lx); !rd_ok { + ctx.f.truncated = true + return + } + blob, blob_ok := t1_read_binary(lx, n) + if !blob_ok { + ctx.f.truncated = true + return + } + if len(ctx.names) >= MAX_T1_GLYPHS { + return + } + append(&ctx.names, string(tok[1:])) + append(&ctx.chars, t1_decrypt_charstring(blob, ctx.f.len_iv)) + } +} + +// --------------------------------------------------------------------------- +// The charstring interpreter (Type 1 spec, chapter 6) +// --------------------------------------------------------------------------- + +@(private = "file") +T1_Interp :: struct { + f: ^Type1_Font, + out: ^Path, + + stack: [T1_STACK_MAX]f64, + sp: int, + // Values an OtherSubr left behind for the `pop`s that follow it. The + // format models this as the PostScript operand stack, which a charstring + // can only reach through `pop`. + res: [T1_PS_MAX]f64, + nres: int, + ires: int, + + // Where the glyph is being drawn. `origin` is non-zero only for the accent + // half of a seac, and translates the whole outline because every Type 1 + // path operator is relative. + cur: Point, + origin: Point, + + sbx: f64, + width: f64, + have_width: bool, + // A subpath has been begun and not yet closed. + open: bool, + + in_flex: bool, + // Where the pen stood when the FLEX began; nothing is emitted while its + // points are collected, so this is still the subpath's live end. + flex_start: Point, + flex: [T1_FLEX_POINTS]Point, + nflex: int, + + fuel: int, + stop: bool, + + allow_seac: bool, + seac: bool, + seac_args: [5]f64, + width_only: bool, +} + +@(private = "file") +t1_run :: proc(f: ^Type1_Font, gid: int, out: ^Path, origin: Point, ip: ^T1_Interp) { + if gid < 0 || gid >= len(f.chars) { + return + } + ip.f = f + ip.out = out + ip.origin = origin + ip.cur = origin + ip.fuel = T1_MAX_FUEL + t1_exec(ip, f.chars[gid], 0) + // Glyph outlines are closed shapes, so whatever the charstring left open -- + // whether it ended at `endchar`, ran out of bytes, or hit a bound -- is + // closed here. + t1_close_subpath(ip) +} + +@(private = "file") +t1_push :: proc(ip: ^T1_Interp, v: f64) { + if ip.sp >= len(ip.stack) { + // The format allows 24 operands. Past our margin the charstring is + // corrupt, and carrying on would mis-align every operator after it. + ip.stop = true + return + } + ip.stack[ip.sp] = is_finite(v) ? v : 0 + ip.sp += 1 +} + +@(private = "file") +t1_args :: proc(ip: ^T1_Interp, n: int) -> ([]f64, bool) { + if ip.sp < n { + return nil, false + } + return ip.stack[ip.sp - n:ip.sp], true +} + +@(private = "file") +t1_ensure_open :: proc(ip: ^T1_Interp) { + if ip.open { + return + } + if ip.out != nil { + path_move_to(ip.out, ip.cur.x, ip.cur.y) + } + ip.open = true +} + +// t1_close_subpath ends the current subpath. Type 1 `closepath` leaves the +// current point where it was, unlike the content stream's `h`, so `cur` is +// untouched here. +@(private = "file") +t1_close_subpath :: proc(ip: ^T1_Interp) { + if !ip.open { + return + } + if ip.out != nil { + path_close(ip.out) + } + ip.open = false +} + +@(private = "file") +t1_rmoveto :: proc(ip: ^T1_Interp, dx, dy: f64) { + nx := ip.cur.x + dx + ny := ip.cur.y + dy + if !is_finite(nx) || !is_finite(ny) { + ip.stop = true + return + } + ip.cur = Point{nx, ny} + + if ip.in_flex { + // Inside a FLEX the seven movetos are control points being collected, + // not seven separate subpaths. A reader that draws them as movetos + // loses the curve entirely and the glyph goes visibly lumpy. + if ip.nflex < len(ip.flex) { + ip.flex[ip.nflex] = ip.cur + } + ip.nflex += 1 + return + } + + // Glyph outlines are always closed, and fonts that leave the `closepath` + // out are common enough that closing here rather than trusting them is the + // only way a stroked text render mode looks right. + t1_close_subpath(ip) + if ip.out != nil { + path_move_to(ip.out, nx, ny) + } + ip.open = true +} + +@(private = "file") +t1_rlineto :: proc(ip: ^T1_Interp, dx, dy: f64) { + nx := ip.cur.x + dx + ny := ip.cur.y + dy + if !is_finite(nx) || !is_finite(ny) { + ip.stop = true + return + } + t1_ensure_open(ip) + if ip.out != nil { + path_line_to(ip.out, nx, ny) + } + ip.cur = Point{nx, ny} +} + +@(private = "file") +t1_rrcurveto :: proc(ip: ^T1_Interp, dx1, dy1, dx2, dy2, dx3, dy3: f64) { + x1 := ip.cur.x + dx1 + y1 := ip.cur.y + dy1 + x2 := x1 + dx2 + y2 := y1 + dy2 + x3 := x2 + dx3 + y3 := y2 + dy3 + t1_curve_abs(ip, Point{x1, y1}, Point{x2, y2}, Point{x3, y3}) +} + +@(private = "file") +t1_curve_abs :: proc(ip: ^T1_Interp, c1, c2, e: Point) { + if !is_finite(c1.x) || !is_finite(c1.y) || !is_finite(c2.x) || + !is_finite(c2.y) || !is_finite(e.x) || !is_finite(e.y) { + ip.stop = true + return + } + t1_ensure_open(ip) + if ip.out != nil { + path_curve_to(ip.out, c1.x, c1.y, c2.x, c2.y, e.x, e.y) + } + ip.cur = e +} + +@(private = "file") +t1_exec :: proc(ip: ^T1_Interp, code: []byte, depth: int) { + if depth > T1_MAX_CALL_DEPTH { + ip.stop = true + return + } + + i := 0 + for i < len(code) { + if ip.stop { + return + } + ip.fuel -= 1 + if ip.fuel <= 0 { + ip.stop = true + return + } + + b := code[i] + i += 1 + + // 32..255 begin a number; 0..31 are the operators (Type 1 spec, 6.2). + if b >= 32 { + v: f64 + switch { + case b <= 246: + v = f64(int(b) - 139) + case b <= 250: + if i >= len(code) { + ip.stop = true + return + } + v = f64((int(b) - 247) * 256 + int(code[i]) + 108) + i += 1 + case b <= 254: + if i >= len(code) { + ip.stop = true + return + } + v = f64(-(int(b) - 251) * 256 - int(code[i]) - 108) + i += 1 + case: + if i + 4 > len(code) { + ip.stop = true + return + } + u := u32(code[i]) << 24 | u32(code[i + 1]) << 16 | + u32(code[i + 2]) << 8 | u32(code[i + 3]) + v = f64(i32(u)) + i += 4 + } + t1_push(ip, v) + continue + } + + switch b { + case 1, 3: + // hstem, vstem: hints, which this reader does not use. + ip.sp = 0 + + case 4: // vmoveto + if a, ok := t1_args(ip, 1); ok { + t1_rmoveto(ip, 0, a[0]) + } + ip.sp = 0 + + case 5: // rlineto + if a, ok := t1_args(ip, 2); ok { + t1_rlineto(ip, a[0], a[1]) + } + ip.sp = 0 + + case 6: // hlineto + if a, ok := t1_args(ip, 1); ok { + t1_rlineto(ip, a[0], 0) + } + ip.sp = 0 + + case 7: // vlineto + if a, ok := t1_args(ip, 1); ok { + t1_rlineto(ip, 0, a[0]) + } + ip.sp = 0 + + case 8: // rrcurveto + if a, ok := t1_args(ip, 6); ok { + t1_rrcurveto(ip, a[0], a[1], a[2], a[3], a[4], a[5]) + } + ip.sp = 0 + + case 9: // closepath + t1_close_subpath(ip) + ip.sp = 0 + + case 10: // callsubr + if ip.sp < 1 { + ip.sp = 0 + break + } + ip.sp -= 1 + idx := int(ip.stack[ip.sp]) + if idx < 0 || idx >= len(ip.f.subrs) || len(ip.f.subrs[idx]) == 0 { + break + } + t1_exec(ip, ip.f.subrs[idx], depth + 1) + + case 11: // return + return + + case 13: // hsbw + if a, ok := t1_args(ip, 2); ok { + ip.sbx = a[0] + ip.width = a[1] + ip.have_width = true + // The left sidebearing is where the outline starts, so setting + // the current point here is what translates the whole glyph. + ip.cur = Point{ip.origin.x + a[0], ip.origin.y} + } + ip.sp = 0 + if ip.width_only && ip.have_width { + ip.stop = true + return + } + + case 14: // endchar + // The open subpath is closed by t1_run rather than here, because a + // charstring that simply runs out of bytes has to be closed too. + ip.sp = 0 + ip.stop = true + return + + case 21: // rmoveto + if a, ok := t1_args(ip, 2); ok { + t1_rmoveto(ip, a[0], a[1]) + } + ip.sp = 0 + + case 22: // hmoveto + if a, ok := t1_args(ip, 1); ok { + t1_rmoveto(ip, a[0], 0) + } + ip.sp = 0 + + case 30: // vhcurveto + if a, ok := t1_args(ip, 4); ok { + t1_rrcurveto(ip, 0, a[0], a[1], a[2], a[3], 0) + } + ip.sp = 0 + + case 31: // hvcurveto + if a, ok := t1_args(ip, 4); ok { + t1_rrcurveto(ip, a[0], 0, a[1], a[2], 0, a[3]) + } + ip.sp = 0 + + case 12: + if i >= len(code) { + ip.stop = true + return + } + b2 := code[i] + i += 1 + if t1_escape(ip, b2) { + return + } + + case: + // An operator this reader does not know is a byte of corruption, + // not a reason to abandon the glyph: clear the operands it would + // have taken and carry on. + ip.sp = 0 + } + } +} + +// t1_escape runs the two-byte operators. Returns true when the caller must +// stop executing this charstring. +@(private = "file") +t1_escape :: proc(ip: ^T1_Interp, op: byte) -> bool { + switch op { + case 0, 1, 2: + // dotsection, vstem3, hstem3: hints. + ip.sp = 0 + + case 6: // seac + if a, ok := t1_args(ip, 5); ok && ip.allow_seac { + ip.seac = true + for k in 0 ..< 5 { + ip.seac_args[k] = a[k] + } + } + ip.sp = 0 + ip.stop = true + return true + + case 7: // sbw + if a, ok := t1_args(ip, 4); ok { + ip.sbx = a[0] + ip.width = a[2] + ip.have_width = true + ip.cur = Point{ip.origin.x + a[0], ip.origin.y + a[1]} + } + ip.sp = 0 + if ip.width_only && ip.have_width { + ip.stop = true + return true + } + + case 12: // div + if ip.sp < 2 { + ip.sp = 0 + break + } + den := ip.stack[ip.sp - 1] + num := ip.stack[ip.sp - 2] + ip.sp -= 2 + // A zero denominator gives an infinity or a NaN, which t1_push turns + // into a zero: this is the only arithmetic in the interpreter, so it + // is the only place a non-finite number can enter the operand stack. + t1_push(ip, num / den) + + case 16: // callothersubr + t1_othersubr(ip) + + case 17: // pop + v := 0.0 + if ip.ires < ip.nres { + v = ip.res[ip.ires] + ip.ires += 1 + } + t1_push(ip, v) + + case 33: // setcurrentpoint + // The operands restate a current point the interpreter already holds: + // the FLEX sequence ends `pop pop setcurrentpoint`, handing back the + // two values OtherSubr 0 left. Honouring them could only move the pen + // somewhere the outline never went, so they are dropped. + ip.in_flex = false + ip.sp = 0 + + case: + ip.sp = 0 + } + return false +} + +// t1_othersubr implements `callothersubr`, which is where the font program +// calls out to PostScript procedures the interpreter is expected to know. +// +// Skipping it is not an option. OtherSubrs 0-2 are FLEX, which every serif face +// uses on the shallow curves of stems and bowls; a reader that treats the seven +// collected movetos as movetos draws those curves as a stack of disconnected +// points. OtherSubr 3 is hint replacement, whose one argument has to come back +// out through the `pop` that follows so the `callsubr` after it reaches the +// right subroutine. +@(private = "file") +t1_othersubr :: proc(ip: ^T1_Interp) { + if ip.sp < 2 { + ip.sp = 0 + return + } + othersubr := int(ip.stack[ip.sp - 1]) + n := int(ip.stack[ip.sp - 2]) + ip.sp -= 2 + if n < 0 { + n = 0 + } + if n > ip.sp { + n = ip.sp + } + args := ip.stack[ip.sp - n:ip.sp] + ip.sp -= n + + ip.nres = 0 + ip.ires = 0 + + switch othersubr { + case 0: + t1_end_flex(ip) + case 1: + ip.in_flex = true + ip.nflex = 0 + ip.flex_start = ip.cur + case 2: + // Collect one FLEX point. The rmoveto that precedes it has already + // recorded it, so there is nothing left to do. + case 3: + // Hint replacement: the subroutine number is handed straight back. A + // reader that answered 3 instead -- the value the non-interpreting + // fallback pushes -- would run the do-nothing subroutine every font + // ships as Subrs[3] and lose the replacement. + ip.nres = 1 + ip.res[0] = len(args) > 0 ? args[0] : 3 + case: + // An OtherSubr this reader does not know: leave its arguments for the + // pops that follow, which is what the PostScript procedure it stands in + // for would have left on the stack. + cnt := min(len(args), len(ip.res)) + for k in 0 ..< cnt { + ip.res[k] = args[k] + } + ip.nres = cnt + } +} + +// t1_end_flex turns the seven collected points into the two curves they +// describe. The first is a reference point the hinting machinery uses and the +// outline ignores; the other six are two cubics' control and end points. +@(private = "file") +t1_end_flex :: proc(ip: ^T1_Interp) { + if !ip.in_flex { + return + } + ip.in_flex = false + + // Nothing was emitted while the points were collected, so the subpath is + // still sitting where the FLEX began. + if !ip.open { + if ip.out != nil { + path_move_to(ip.out, ip.flex_start.x, ip.flex_start.y) + } + ip.open = true + } + + if ip.nflex >= T1_FLEX_POINTS { + t1_curve_abs(ip, ip.flex[1], ip.flex[2], ip.flex[3]) + t1_curve_abs(ip, ip.flex[4], ip.flex[5], ip.flex[6]) + } else if ip.nflex > 0 { + // A FLEX that collected the wrong number of points is malformed, but + // the pen did move: joining the ends with a straight segment keeps the + // outline in one piece instead of leaving a gap across the glyph. + if ip.out != nil { + path_line_to(ip.out, ip.cur.x, ip.cur.y) + } + } + + // The two values `pop pop setcurrentpoint` is about to consume. + ip.res[0] = ip.cur.x + ip.res[1] = ip.cur.y + ip.nres = 2 + ip.ires = 0 +} diff --git a/font_type1_test.odin b/font_type1_test.odin new file mode 100644 index 0000000..a3bbd45 --- /dev/null +++ b/font_type1_test.odin @@ -0,0 +1,1427 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:testing" + +// Tests for font_type1.odin. +// +// THE FIXTURES +// +// T1_FONT_A and T1_FONT_B below are two complete Type 1 font programs, emitted +// as byte literals by a throwaway Python script. The script assembled the +// PostScript by hand, encoded each charstring with the Type 1 number and +// operator encoding, enciphered the charstrings at r = 4330 and the Private +// dictionary at r = 55665, and chose the eexec random padding so that the first +// four ciphertext bytes are neither whitespace nor all hex digits -- the +// condition that lets a reader tell the binary form from the .pfa one. +// +// Font A carries, in /CharStrings order: +// +// 0 .notdef 0 500 hsbw endchar +// 1 A sidebearing 50, a 100x200 box +// 2 B one FLEX, driven through the standard Subrs 0/1/2 +// 3 C hint replacement: OtherSubr 3 must hand 4 back to the callsubr +// 4 D A's outline with a zero sidebearing +// 5 Aacute seac of A (65) and acute (194), asb = acute's own sidebearing +// 6 acute sidebearing 20, a triangle +// 7 E rrcurveto, hvcurveto, vhcurveto and a div +// +// and Subrs 0-2 as every Type 1 font defines them, Subrs[3] the do-nothing +// subroutine hint replacement falls back to, and Subrs[4] a distinctive +// `0 100 rlineto return` so that a test can tell 4 from 3. Its /Encoding uses +// the `dup / put` form. +// +// Font B has no eexec layer at all -- its Private dictionary is plain text, +// with the charstrings still enciphered -- the `/Encoding StandardEncoding def` +// shorthand, and a /FontMatrix that is not the default. +// +// The PFB, hex-eexec and truncated forms are derived from Font A in the tests +// below rather than being three more literals; T1A_CLEAR_LEN and T1A_BIN_LEN +// are where its three parts meet. + +// --------------------------------------------------------------------------- +// Fixture +// +// Every font under test is parsed into a local arena, destroyed on the way out +// whatever happened, so a failing assertion cannot leak. +// --------------------------------------------------------------------------- + +@(private = "file") +T1_Fixture :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private = "file") +t1f_init :: proc(f: ^T1_Fixture) { + _ = virtual.arena_init_growing(&f.arena) + f.alloc = virtual.arena_allocator(&f.arena) +} + +@(private = "file") +t1f_destroy :: proc(f: ^T1_Fixture) { + virtual.arena_destroy(&f.arena) +} + +// t1f_font parses a font and fails the test if it did not come back. +@(private = "file") +t1f_font :: proc(t: ^testing.T, f: ^T1_Fixture, data: []byte, label: string) -> ^Type1_Font { + font, err := t1_parse(data, f.alloc) + if !testing.expectf(t, err == .None && font != nil, "%s: t1_parse gave %v", label, err) { + return nil + } + return font +} + +// t1f_glyph runs one named glyph and returns its outline. +@(private = "file") +t1f_glyph :: proc(t: ^testing.T, f: ^T1_Fixture, font: ^Type1_Font, name: string) -> Path { + p := path_make(f.alloc) + gid, ok := t1_gid_for_name(font, name) + if !testing.expectf(t, ok, "glyph /%s is missing from the font", name) { + return p + } + err := t1_glyph_path(font, gid, &p) + testing.expectf(t, err == .None, "/%s: t1_glyph_path gave %v", name, err) + return p +} + +// t1f_expect_path compares an outline element by element. Both arrays are +// checked, so a path with the right points under the wrong verbs fails. +@(private = "file") +t1f_expect_path :: proc(t: ^testing.T, p: ^Path, verbs: []Path_Verb, pts: []Point, label: string) { + ok := len(p.verbs) == len(verbs) && len(p.points) == len(pts) + if ok { + for v, i in verbs { + if p.verbs[i] != v { + ok = false + break + } + } + } + if ok { + for q, i in pts { + if abs(p.points[i].x - q.x) > 1e-9 || abs(p.points[i].y - q.y) > 1e-9 { + ok = false + break + } + } + } + testing.expectf(t, ok, "%s:\n got %v %v\n want %v %v", + label, p.verbs[:], p.points[:], verbs, pts) +} + +@(private = "file", rodata) +T1_FONT_A := [1723]u8{ + 0x25, 0x21, 0x46, 0x6f, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x31, 0x2d, + 0x31, 0x2e, 0x30, 0x3a, 0x20, 0x4f, 0x64, 0x69, 0x6e, 0x54, 0x65, 0x73, + 0x74, 0x20, 0x30, 0x30, 0x31, 0x2e, 0x30, 0x30, 0x31, 0x0a, 0x2f, 0x46, + 0x6f, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x2f, 0x4f, 0x64, 0x69, + 0x6e, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x46, + 0x6f, 0x6e, 0x74, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x20, 0x5b, 0x30, + 0x2e, 0x30, 0x30, 0x31, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2e, 0x30, + 0x30, 0x31, 0x20, 0x30, 0x20, 0x30, 0x5d, 0x20, 0x72, 0x65, 0x61, 0x64, + 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x45, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x32, 0x35, 0x36, 0x20, 0x61, + 0x72, 0x72, 0x61, 0x79, 0x0a, 0x30, 0x20, 0x31, 0x20, 0x32, 0x35, 0x35, + 0x20, 0x7b, 0x31, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x65, 0x78, + 0x63, 0x68, 0x20, 0x2f, 0x2e, 0x6e, 0x6f, 0x74, 0x64, 0x65, 0x66, 0x20, + 0x70, 0x75, 0x74, 0x7d, 0x20, 0x66, 0x6f, 0x72, 0x0a, 0x64, 0x75, 0x70, + 0x20, 0x36, 0x35, 0x20, 0x2f, 0x41, 0x20, 0x70, 0x75, 0x74, 0x0a, 0x64, + 0x75, 0x70, 0x20, 0x36, 0x36, 0x20, 0x2f, 0x42, 0x20, 0x70, 0x75, 0x74, + 0x0a, 0x64, 0x75, 0x70, 0x20, 0x36, 0x37, 0x20, 0x2f, 0x43, 0x20, 0x70, + 0x75, 0x74, 0x0a, 0x64, 0x75, 0x70, 0x20, 0x36, 0x38, 0x20, 0x2f, 0x44, + 0x20, 0x70, 0x75, 0x74, 0x0a, 0x64, 0x75, 0x70, 0x20, 0x31, 0x39, 0x33, + 0x20, 0x2f, 0x41, 0x61, 0x63, 0x75, 0x74, 0x65, 0x20, 0x70, 0x75, 0x74, + 0x0a, 0x64, 0x75, 0x70, 0x20, 0x31, 0x39, 0x34, 0x20, 0x2f, 0x61, 0x63, + 0x75, 0x74, 0x65, 0x20, 0x70, 0x75, 0x74, 0x0a, 0x72, 0x65, 0x61, 0x64, + 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x50, 0x61, + 0x69, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x20, 0x30, 0x20, 0x64, 0x65, + 0x66, 0x0a, 0x2f, 0x46, 0x6f, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x20, + 0x31, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x46, 0x6f, 0x6e, 0x74, 0x42, + 0x42, 0x6f, 0x78, 0x20, 0x7b, 0x30, 0x20, 0x30, 0x20, 0x31, 0x30, 0x30, + 0x30, 0x20, 0x31, 0x30, 0x30, 0x30, 0x7d, 0x20, 0x72, 0x65, 0x61, 0x64, + 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x64, 0x69, 0x63, 0x74, 0x20, 0x65, 0x6e, 0x64, + 0x0a, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x66, 0x69, 0x6c, 0x65, + 0x20, 0x65, 0x65, 0x78, 0x65, 0x63, 0x0a, 0xbe, 0x77, 0xeb, 0xcb, 0x37, + 0xf8, 0xbf, 0x92, 0x8f, 0x7e, 0xc3, 0x33, 0x57, 0x7b, 0x74, 0xb0, 0xdd, + 0xd4, 0x81, 0xda, 0x47, 0x92, 0x94, 0xae, 0x29, 0x54, 0x75, 0xfe, 0x04, + 0xc3, 0xf4, 0xc2, 0x73, 0x76, 0xeb, 0x6e, 0x25, 0xbc, 0xe3, 0x4e, 0x14, + 0xca, 0xb0, 0xc6, 0x40, 0x8a, 0x7e, 0xd7, 0x9b, 0x7d, 0x61, 0x3d, 0x1e, + 0xe3, 0xf4, 0xfc, 0x7d, 0xa5, 0xd9, 0x52, 0x83, 0x3a, 0xe2, 0xd5, 0x6c, + 0x88, 0xd0, 0xf6, 0xe5, 0x90, 0x76, 0x79, 0x6e, 0x2c, 0x49, 0x2d, 0x62, + 0xa3, 0x35, 0x18, 0x36, 0x1e, 0x20, 0x9e, 0x8a, 0x3d, 0x49, 0xe7, 0x0b, + 0x84, 0xc6, 0x5d, 0xbb, 0x6b, 0xf1, 0xf9, 0x15, 0xbf, 0x48, 0xf6, 0xcc, + 0x2d, 0xbe, 0xc9, 0x38, 0x04, 0xa9, 0x3b, 0x2c, 0x22, 0xaa, 0x60, 0xc8, + 0x56, 0x48, 0xe0, 0x38, 0xa9, 0xa5, 0xb3, 0xae, 0x94, 0xaa, 0x21, 0xdd, + 0xdc, 0x32, 0x4a, 0x8f, 0x95, 0x7a, 0xde, 0x87, 0x37, 0x9c, 0x1d, 0x8f, + 0x26, 0x2a, 0x8e, 0xa0, 0x48, 0x4f, 0x16, 0x8c, 0x61, 0x67, 0xb6, 0x3b, + 0xe5, 0xf5, 0x35, 0x8d, 0xab, 0x9f, 0x3e, 0x9a, 0xe6, 0x86, 0xd1, 0xce, + 0xd1, 0x6f, 0x95, 0x5f, 0xc8, 0xb9, 0x0e, 0x16, 0x3c, 0xcb, 0x6b, 0xfb, + 0x17, 0x46, 0x81, 0x41, 0x8e, 0x63, 0x89, 0x4d, 0x3d, 0xdd, 0x38, 0x51, + 0x0c, 0x6a, 0x23, 0x44, 0xee, 0xe8, 0x4d, 0x2d, 0xc5, 0xeb, 0xb4, 0xe3, + 0x1e, 0x60, 0x1b, 0xe2, 0x24, 0x6a, 0xaa, 0x60, 0xad, 0x04, 0x65, 0x85, + 0x8a, 0xcf, 0x6b, 0x36, 0x11, 0xf7, 0xbd, 0x53, 0xab, 0xbb, 0xb1, 0xb0, + 0x46, 0x35, 0xd4, 0x3b, 0x68, 0x2f, 0xef, 0x72, 0x5b, 0xc0, 0x86, 0x69, + 0xeb, 0x5c, 0x53, 0x0c, 0xa9, 0x5b, 0xcd, 0x7d, 0x6a, 0x74, 0x7c, 0x3b, + 0x3f, 0xd3, 0xef, 0x11, 0x88, 0xf0, 0xfb, 0x70, 0x42, 0xb8, 0x0b, 0x0e, + 0x77, 0x2f, 0x33, 0x21, 0x2d, 0xf5, 0xd1, 0xaf, 0xb5, 0x2b, 0x82, 0x76, + 0x3d, 0x3c, 0x81, 0x45, 0xf3, 0x6f, 0xe2, 0x87, 0xdf, 0x76, 0xe5, 0x7e, + 0x0a, 0x60, 0xe1, 0xed, 0x9e, 0xf6, 0x8f, 0x3e, 0x2d, 0x9a, 0x74, 0x75, + 0x1e, 0x27, 0x8e, 0xad, 0x60, 0x04, 0x78, 0x21, 0xd5, 0xe3, 0x84, 0xbf, + 0xd0, 0xa3, 0x60, 0x6d, 0x42, 0xef, 0xb6, 0x21, 0xcd, 0x07, 0x34, 0x1e, + 0xab, 0x62, 0x2a, 0x75, 0x5e, 0x26, 0xe9, 0x26, 0xf0, 0x3e, 0x2a, 0xf8, + 0x32, 0x87, 0x91, 0xa2, 0xf0, 0xd0, 0x39, 0xd0, 0x84, 0xa2, 0xb5, 0x90, + 0x19, 0xcb, 0x76, 0x41, 0x7a, 0x23, 0x7c, 0x9d, 0x19, 0xea, 0x35, 0x27, + 0x36, 0xf2, 0xbe, 0x8a, 0x9e, 0x8e, 0x3a, 0xfa, 0xe6, 0x21, 0x20, 0xcd, + 0x2f, 0x1f, 0xe6, 0x0c, 0x48, 0x6a, 0xd7, 0x61, 0x38, 0x02, 0xc1, 0xf9, + 0xfa, 0xe1, 0x6b, 0xc5, 0x28, 0x34, 0xa5, 0xf7, 0xcb, 0x05, 0x10, 0xa3, + 0x13, 0xd5, 0x75, 0xc5, 0xdf, 0x9e, 0x4a, 0xde, 0xa8, 0x5d, 0x9f, 0x8c, + 0x7c, 0x4e, 0xd1, 0xea, 0x15, 0x35, 0x8f, 0x98, 0x18, 0xf5, 0x87, 0x7c, + 0x39, 0xcd, 0xcd, 0x9f, 0x0a, 0x33, 0xfe, 0x83, 0xc9, 0x73, 0xe6, 0x52, + 0xd3, 0xda, 0x97, 0x2c, 0xe1, 0x1d, 0xc1, 0x56, 0x7a, 0x3b, 0x37, 0x6c, + 0x2a, 0xcf, 0x5b, 0x6e, 0xf2, 0xe1, 0x61, 0x49, 0x11, 0x0f, 0x00, 0x2e, + 0x35, 0xbe, 0xa4, 0x35, 0xcf, 0x43, 0x10, 0xf4, 0x98, 0xfa, 0xcc, 0x56, + 0x52, 0xb1, 0x4b, 0xe9, 0x8a, 0xfd, 0x7f, 0x99, 0xe8, 0xa0, 0x7c, 0x97, + 0x94, 0x95, 0xf7, 0xa5, 0xeb, 0x92, 0x8e, 0xc6, 0xe8, 0x01, 0x66, 0x96, + 0x68, 0x4d, 0x2e, 0x61, 0x42, 0xd0, 0x2d, 0x32, 0x44, 0x91, 0xea, 0x57, + 0x6f, 0x6c, 0xc0, 0x4f, 0x82, 0x56, 0x9c, 0x0c, 0x04, 0xe7, 0xad, 0xed, + 0x53, 0x76, 0xdb, 0x5e, 0x5c, 0x80, 0x57, 0x00, 0x08, 0xab, 0x0d, 0x74, + 0xca, 0x44, 0xd8, 0x0b, 0xd9, 0x96, 0x57, 0xed, 0x7f, 0x84, 0xf0, 0xdb, + 0xd5, 0x13, 0xc8, 0xe7, 0xbb, 0x97, 0xfa, 0x53, 0x5a, 0x18, 0x64, 0xfa, + 0x9c, 0xb6, 0x94, 0xf7, 0x36, 0xb8, 0x3e, 0xfb, 0x09, 0xd1, 0xd5, 0x91, + 0x2d, 0xdb, 0x3d, 0xa7, 0x2d, 0xe6, 0xc0, 0x83, 0xe4, 0xfd, 0xff, 0xa0, + 0x20, 0x4a, 0x4e, 0xbf, 0xbb, 0x8b, 0x6f, 0xad, 0xab, 0xf4, 0xa5, 0xc6, + 0xf4, 0x43, 0xd0, 0x5e, 0x89, 0x33, 0xa0, 0x56, 0x84, 0xb9, 0xff, 0x63, + 0xbf, 0xd1, 0x40, 0x3c, 0xa1, 0x03, 0xd7, 0x3f, 0x4d, 0xfb, 0xf1, 0x00, + 0x52, 0x3c, 0xda, 0xdb, 0x02, 0xdc, 0x1c, 0xc0, 0x35, 0x4c, 0xce, 0xce, + 0x84, 0x8d, 0x94, 0x23, 0xc4, 0x06, 0x00, 0x64, 0x60, 0x6c, 0xb6, 0xcb, + 0x71, 0x0f, 0xd0, 0xd3, 0xa4, 0x06, 0xba, 0x3a, 0x70, 0x5e, 0x8b, 0x12, + 0x7e, 0x81, 0x94, 0x58, 0x6e, 0xbb, 0x08, 0x41, 0x92, 0x39, 0xde, 0xae, + 0x86, 0x1f, 0xdd, 0xa4, 0xd0, 0xad, 0xce, 0x44, 0xa0, 0x67, 0x0e, 0x10, + 0x0a, 0x4b, 0x1f, 0x2c, 0x00, 0xc5, 0x37, 0xa7, 0x9f, 0xf7, 0xda, 0x8d, + 0x9d, 0xe0, 0x13, 0xcb, 0xbf, 0x53, 0x75, 0xb1, 0xf6, 0x74, 0x59, 0x41, + 0x87, 0x9b, 0xe4, 0xab, 0xb5, 0xfd, 0x05, 0x44, 0x0f, 0x61, 0x86, 0x29, + 0x5a, 0x30, 0x03, 0x80, 0x68, 0xa1, 0x34, 0x39, 0x65, 0x2d, 0x8c, 0xdd, + 0x4b, 0x57, 0x8a, 0x94, 0xd8, 0x36, 0xd1, 0xdd, 0xfc, 0x73, 0xe4, 0xef, + 0x45, 0xd5, 0xa0, 0x42, 0x35, 0x01, 0x38, 0xfa, 0x89, 0xd8, 0x92, 0xa2, + 0xd2, 0xfe, 0x3b, 0x4d, 0x7e, 0x87, 0x9a, 0x18, 0x05, 0xe1, 0x46, 0x4b, + 0xa1, 0x58, 0x51, 0x5f, 0x0d, 0x38, 0xd8, 0x63, 0xb2, 0x0c, 0x3f, 0x34, + 0xcc, 0x87, 0xa5, 0x7d, 0xd9, 0xce, 0x40, 0xfd, 0x05, 0x1d, 0x7d, 0x45, + 0x89, 0x11, 0xa7, 0x8f, 0xab, 0x68, 0x81, 0xc5, 0x84, 0x04, 0xfc, 0x68, + 0x93, 0x2b, 0x49, 0x1e, 0xa6, 0xf3, 0x99, 0x23, 0x98, 0x0a, 0x1f, 0x84, + 0x3e, 0x13, 0x70, 0x62, 0xdc, 0xc8, 0xd7, 0x27, 0x39, 0xa2, 0xdf, 0x8f, + 0xfe, 0x52, 0x0a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x0a, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x0a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x0a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x0a, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x0a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x0a, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x0a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x0a, 0x63, 0x6c, 0x65, 0x61, 0x72, + 0x74, 0x6f, 0x6d, 0x61, 0x72, 0x6b, 0x0a, +} + +// Byte offsets inside T1_FONT_A: the cleartext runs [0, 379), the +// eexec ciphertext [379, 1190), and the trailer the rest. +@(private = "file") +T1A_CLEAR_LEN :: 379 +@(private = "file") +T1A_BIN_LEN :: 811 + +@(private = "file", rodata) +T1_FONT_B := [393]u8{ + 0x25, 0x21, 0x46, 0x6f, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x31, 0x2d, + 0x31, 0x2e, 0x30, 0x3a, 0x20, 0x4f, 0x64, 0x69, 0x6e, 0x50, 0x6c, 0x61, + 0x69, 0x6e, 0x20, 0x30, 0x30, 0x31, 0x2e, 0x30, 0x30, 0x31, 0x0a, 0x2f, + 0x46, 0x6f, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x2f, 0x4f, 0x64, + 0x69, 0x6e, 0x50, 0x6c, 0x61, 0x69, 0x6e, 0x20, 0x64, 0x65, 0x66, 0x0a, + 0x2f, 0x46, 0x6f, 0x6e, 0x74, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x20, + 0x5b, 0x30, 0x2e, 0x30, 0x30, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, + 0x2e, 0x30, 0x30, 0x32, 0x20, 0x30, 0x20, 0x30, 0x5d, 0x20, 0x72, 0x65, + 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, + 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x53, 0x74, 0x61, + 0x6e, 0x64, 0x61, 0x72, 0x64, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x46, 0x6f, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x20, 0x31, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x64, 0x69, 0x63, 0x74, 0x20, 0x65, 0x6e, + 0x64, 0x0a, 0x2f, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x20, 0x35, + 0x20, 0x64, 0x69, 0x63, 0x74, 0x20, 0x64, 0x75, 0x70, 0x20, 0x62, 0x65, + 0x67, 0x69, 0x6e, 0x0a, 0x2f, 0x52, 0x44, 0x20, 0x7b, 0x73, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x66, + 0x69, 0x6c, 0x65, 0x20, 0x65, 0x78, 0x63, 0x68, 0x20, 0x72, 0x65, 0x61, + 0x64, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x70, 0x6f, 0x70, 0x7d, + 0x20, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x6f, 0x6e, 0x6c, 0x79, + 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x6c, 0x65, 0x6e, 0x49, 0x56, 0x20, + 0x34, 0x20, 0x64, 0x65, 0x66, 0x0a, 0x2f, 0x53, 0x75, 0x62, 0x72, 0x73, + 0x20, 0x30, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x20, 0x4e, 0x44, 0x0a, + 0x32, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2f, 0x43, 0x68, 0x61, + 0x72, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x20, 0x32, 0x20, 0x64, + 0x69, 0x63, 0x74, 0x20, 0x64, 0x75, 0x70, 0x20, 0x62, 0x65, 0x67, 0x69, + 0x6e, 0x0a, 0x2f, 0x2e, 0x6e, 0x6f, 0x74, 0x64, 0x65, 0x66, 0x20, 0x39, + 0x20, 0x52, 0x44, 0x20, 0x10, 0xbf, 0x31, 0x70, 0x79, 0xc8, 0x2f, 0x06, + 0x80, 0x20, 0x4e, 0x44, 0x0a, 0x2f, 0x41, 0x20, 0x31, 0x39, 0x20, 0x52, + 0x44, 0x20, 0x10, 0xbf, 0x31, 0x70, 0x67, 0x54, 0x36, 0x79, 0x9a, 0x79, + 0xaa, 0x38, 0xca, 0xcd, 0xa1, 0x72, 0x86, 0xcd, 0xc9, 0x20, 0x4e, 0x44, + 0x0a, 0x65, 0x6e, 0x64, 0x0a, 0x65, 0x6e, 0x64, 0x0a, +} +// 0 500 hsbw endchar +@(private = "file", rodata) +T1T_NOTDEF := [?]u8{ + 0x8b, 0xf8, 0x88, 0x0d, 0x0e, +} + +// return +@(private = "file", rodata) +T1T_RETURN := [?]u8{ + 0x0b, +} + +// 0 400 hsbw 0 0 rmoveto 100 hlineto closepath endchar +@(private = "file", rodata) +T1T_LINE := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xef, 0x06, 0x09, 0x0e, +} + +// 0 400 hsbw 0 100 400 128 130 seac +@(private = "file", rodata) +T1T_SEAC_UNDEF := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0xef, 0xf8, 0x24, 0xf7, 0x14, 0xf7, 0x16, 0x0c, 0x06, +} + +// 0 400 hsbw 0 100 400 -5 999 seac +@(private = "file", rodata) +T1T_SEAC_OOR := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0xef, 0xf8, 0x24, 0x86, 0xfa, 0x7b, 0x0c, 0x06, +} + +// 0 400 hsbw 0 0 rmoveto 1 callsubr closepath endchar +@(private = "file", rodata) +T1T_VIA_SUBR := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0x8c, 0x0a, 0x09, 0x0e, +} + +// 100 hlineto 100 vlineto return +@(private = "file", rodata) +T1T_SUBR_BOX := [?]u8{ + 0xef, 0x06, 0xef, 0x07, 0x0b, +} + +// 0 400 hsbw 0 0 rmoveto 0 callsubr closepath endchar +@(private = "file", rodata) +T1T_DEEP := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0x8b, 0x0a, 0x09, 0x0e, +} + +// 0 callsubr return +@(private = "file", rodata) +T1T_SELFCALL := [?]u8{ + 0x8b, 0x0a, 0x0b, +} + +// 0 400 hsbw rlineto rrcurveto 0 0 rmoveto 100 hlineto closepath endchar +@(private = "file", rodata) +T1T_STARVED := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x05, 0x08, 0x8b, 0x8b, 0x15, 0xef, 0x06, 0x09, 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto raw:15 100 hlineto closepath endchar +@(private = "file", rodata) +T1T_UNKNOWN_OP := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0x0f, 0xef, 0x06, 0x09, 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto 0 1 callothersubr 10 10 rmoveto 10 10 rmoveto 10 10 rmoveto 0 0 0 3 0 callothersubr pop pop setcurrentpoint closepath endchar +@(private = "file", rodata) +T1T_HALF_FLEX := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0x8b, 0x8c, 0x0c, 0x10, 0x95, 0x95, 0x15, + 0x95, 0x95, 0x15, 0x95, 0x95, 0x15, 0x8b, 0x8b, 0x8b, 0x8e, 0x8b, 0x0c, 0x10, 0x0c, + 0x11, 0x0c, 0x11, 0x0c, 0x21, 0x09, 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto raw:255 raw:0 raw:0 +@(private = "file", rodata) +T1T_TRUNC_NUM := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xff, 0x00, 0x00, +} + +// 0 400 hsbw 0 0 rmoveto 99 callsubr -1 callsubr 100 hlineto closepath endchar +@(private = "file", rodata) +T1T_BAD_SUBR := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xee, 0x0a, 0x8a, 0x0a, 0xef, 0x06, 0x09, + 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto 100 20 2 14 callothersubr pop pop rlineto closepath endchar +@(private = "file", rodata) +T1T_OTHERSUBR := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xef, 0x9f, 0x8d, 0x99, 0x0c, 0x10, 0x0c, + 0x11, 0x0c, 0x11, 0x05, 0x09, 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto 100 pop rlineto closepath endchar +@(private = "file", rodata) +T1T_BARE_POP := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xef, 0x0c, 0x11, 0x05, 0x09, 0x0e, +} + +// 0 400 hsbw 0 0 rmoveto 100 0 div 20 rlineto closepath endchar +@(private = "file", rodata) +T1T_DIV_ZERO := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0x8b, 0x15, 0xef, 0x8b, 0x0c, 0x0c, 0x9f, 0x05, 0x09, + 0x0e, +} + +// 10 480 hsbw 0 0 rmoveto 40 0 rlineto 0 60 rlineto closepath endchar +@(private = "file", rodata) +T1T_FONTB_A := [?]u8{ + 0x95, 0xf8, 0x74, 0x0d, 0x8b, 0x8b, 0x15, 0xb3, 0x8b, 0x05, 0x8b, 0xc7, 0x05, 0x09, + 0x0e, +} + +// endchar +@(private = "file", rodata) +T1T_ENDCHAR := [?]u8{ + 0x0e, +} + +// 0 700 hsbw 0 0 rmoveto 107 -107 rlineto 108 -108 rlineto 1131 -1131 rlineto 32768 -32768 rlineto closepath endchar +@(private = "file", rodata) +T1T_NUMBERS := [?]u8{ + 0x8b, 0xf9, 0x50, 0x0d, 0x8b, 0x8b, 0x15, 0xf6, 0x20, 0x05, 0xf7, 0x00, 0xfb, 0x00, + 0x05, 0xfa, 0xff, 0xfe, 0xff, 0x05, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x05, 0x09, 0x0e, +} + +// 0 600 hsbw 0 0 rmoveto 100 hlineto 0 200 rmoveto 100 hlineto endchar +@(private = "file", rodata) +T1T_TWO_SUBS := [?]u8{ + 0x8b, 0xf8, 0xec, 0x0d, 0x8b, 0x8b, 0x15, 0xef, 0x06, 0x8b, 0xf7, 0x5c, 0x15, 0xef, + 0x06, 0x0e, +} + +// 0 400 hsbw 0 100 400 999 -5 seac +@(private = "file", rodata) +T1T_SEAC_HIGH := [?]u8{ + 0x8b, 0xf8, 0x24, 0x0d, 0x8b, 0xef, 0xf8, 0x24, 0xfa, 0x7b, 0x86, 0x0c, 0x06, +} + +// --------------------------------------------------------------------------- +// Container and dictionary parsing +// --------------------------------------------------------------------------- + +@(test) +test_t1_parse_raw :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + + testing.expectf(t, t1_glyph_count(font) == 8, "glyph count %d, want 8", t1_glyph_count(font)) + testing.expectf(t, font.name == "OdinTest", "/FontName %q", font.name) + // Font A declares /lenIV 5 -- a value the format allows and no font uses + // by accident -- and puts it in the first line of the encrypted private + // dictionary, right against the four bytes of random eexec padding. A + // reader that forgets to discard the padding finds no /lenIV, falls back to + // the default 4, and mis-decodes every charstring in the font. + testing.expectf(t, font.len_iv == 5, "/lenIV %d, want 5", font.len_iv) + testing.expect(t, !font.truncated, "an intact font reported truncation") + + m := t1_font_matrix(font) + testing.expectf(t, m == Matrix{0.001, 0, 0, 0.001, 0, 0}, "/FontMatrix %v", m) + + // /CharStrings order is the order the dictionary was written in. + want := [?]string{".notdef", "A", "B", "C", "D", "Aacute", "acute", "E"} + for name, i in want { + gid, ok := t1_gid_for_name(font, name) + testing.expectf(t, ok && int(gid) == i, "/%s resolved to %d,%v; want %d", name, gid, ok, i) + } + if _, ok := t1_gid_for_name(font, "Zcaron"); ok { + testing.expect(t, false, "a glyph the font does not have resolved anyway") + } + if _, ok := t1_gid_for_name(font, ""); ok { + testing.expect(t, false, "the empty name resolved to a glyph") + } + + // Five Subrs, and the two the flex and hint tests lean on are non-empty. + testing.expectf(t, len(font.subrs) == 5, "Subrs count %d, want 5", len(font.subrs)) + if len(font.subrs) == 5 { + testing.expect(t, len(font.subrs[0]) > 0, "Subrs[0] decoded empty") + testing.expect(t, len(font.subrs[4]) > 0, "Subrs[4] decoded empty") + } +} + +@(test) +test_t1_encoding_array_form :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + enc := t1_builtin_encoding(font) + if !testing.expect(t, enc != nil, "font A declares an /Encoding but none came back") { + return + } + testing.expectf(t, enc[65] == "A", "code 65 is %q", enc[65]) + testing.expectf(t, enc[66] == "B", "code 66 is %q", enc[66]) + testing.expectf(t, enc[68] == "D", "code 68 is %q", enc[68]) + testing.expectf(t, enc[193] == "Aacute", "code 193 is %q", enc[193]) + testing.expectf(t, enc[194] == "acute", "code 194 is %q", enc[194]) + // The `0 1 255 {1 index exch /.notdef put} for` loop is not executed, and + // its `/.notdef put` must not be mistaken for a `dup / put`. + testing.expectf(t, enc[32] == "", "code 32 is %q, want undefined", enc[32]) + testing.expectf(t, enc[0] == "", "code 0 is %q, want undefined", enc[0]) +} + +// Font B exercises three things at once: a font shipped with the eexec layer +// already stripped, the /Encoding StandardEncoding shorthand, and a +// /FontMatrix that is not the default. +@(test) +test_t1_no_eexec_and_standard_encoding :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_B[:], "font B") + if font == nil { + return + } + testing.expectf(t, t1_glyph_count(font) == 2, "glyph count %d, want 2", t1_glyph_count(font)) + testing.expectf(t, font.name == "OdinPlain", "/FontName %q", font.name) + + m := t1_font_matrix(font) + testing.expectf(t, m == Matrix{0.002, 0, 0, 0.002, 0, 0}, "/FontMatrix %v", m) + + enc := t1_builtin_encoding(font) + if !testing.expect(t, enc != nil, "/Encoding StandardEncoding produced no encoding") { + return + } + testing.expectf(t, enc[65] == "A", "code 65 is %q", enc[65]) + testing.expectf(t, enc[194] == "acute", "code 194 is %q", enc[194]) + testing.expectf(t, enc[32] == "space", "code 32 is %q", enc[32]) + testing.expectf(t, enc[127] == "", "code 127 is %q, want undefined", enc[127]) + + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, + {.Move, .Line, .Line, .Close}, + {{10, 0}, {50, 0}, {50, 60}}, + "font B /A") + testing.expectf(t, t1_glyph_advance(font, 1) == 480, "advance %v", t1_glyph_advance(font, 1)) +} + +// A font with no /Encoding at all must say so rather than claiming an empty +// one: the caller has to know to fall back to StandardEncoding. +@(test) +test_t1_no_encoding_reports_none :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_LINE[:]}, + no_encoding = true, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "no /Encoding") + if font == nil { + return + } + testing.expect(t, t1_builtin_encoding(font) == nil, + "a font with no /Encoding claimed to have one") + // ... and still has its glyphs. + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, {.Move, .Line, .Close}, {{0, 0}, {100, 0}}, "/A") +} + +@(test) +test_t1_pfb_segments :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + raw := t1f_font(t, &f, T1_FONT_A[:], "font A raw") + pfb := t1f_font(t, &f, t1t_pfb(T1_FONT_A[:], f.alloc), "font A as PFB") + if raw == nil || pfb == nil { + return + } + t1f_expect_same(t, raw, pfb, &f, "PFB") + + // A segment header that claims more bytes than the file holds must be + // clamped to what is there. The eexec cipher is a stream, so the glyphs + // before the join still decode and the font comes out whole. + lying := t1t_pfb(T1_FONT_A[:], f.alloc) + at := 6 + T1A_CLEAR_LEN + 2 + lying[at] = 0xFF + lying[at + 1] = 0xFF + lying[at + 2] = 0xFF + lying[at + 3] = 0x7F + clamped := t1f_font(t, &f, lying, "font A as PFB with a lying segment length") + if clamped != nil { + t1f_expect_same(t, raw, clamped, &f, "clamped PFB") + } +} + +@(test) +test_t1_hex_eexec :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + raw := t1f_font(t, &f, T1_FONT_A[:], "font A raw") + hex := t1f_font(t, &f, t1t_hex_form(T1_FONT_A[:], f.alloc), "font A as PFA") + if raw == nil || hex == nil { + return + } + t1f_expect_same(t, raw, hex, &f, "hex eexec") +} + +// --------------------------------------------------------------------------- +// The charstring interpreter +// --------------------------------------------------------------------------- + +@(test) +test_t1_glyph_outline :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + + a := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &a, + {.Move, .Line, .Line, .Line, .Close}, + {{50, 0}, {150, 0}, {150, 200}, {50, 200}}, + "/A") + + // /E covers rrcurveto and the two mixed curve operators, and a div. + e := t1f_glyph(t, &f, font, "E") + t1f_expect_path(t, &e, + {.Move, .Curve, .Curve, .Curve, .Line, .Close}, + { + {0, 0}, + {10, 20}, {40, 60}, {90, 120}, // rrcurveto + {100, 120}, {120, 150}, {120, 190}, // hvcurveto + {120, 200}, {140, 230}, {180, 230}, // vhcurveto + {230, 230}, // 200 4 div 0 rlineto + }, + "/E") +} + +// hsbw does two jobs, and the second one is easy to drop: the sidebearing it +// declares translates the whole outline. /A and /D are the same drawing +// commands under sidebearings of 50 and 0. +@(test) +test_t1_hsbw_sidebearing_translates :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + + a := t1f_glyph(t, &f, font, "A") + d := t1f_glyph(t, &f, font, "D") + t1f_expect_path(t, &d, + {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 200}, {0, 200}}, + "/D") + + if !testing.expectf(t, len(a.points) == len(d.points), + "/A has %d points, /D has %d", len(a.points), len(d.points)) { + return + } + for pt, i in d.points { + shifted := Point{pt.x + 50, pt.y} + testing.expectf(t, a.points[i] == shifted, + "/A point %d is %v; want /D's %v moved by the sidebearing to %v", + i, a.points[i], pt, shifted) + } + // Both widths come from the same hsbw, and the sidebearing is not it. + testing.expectf(t, t1_glyph_advance(font, 1) == 600, "/A advance %v", t1_glyph_advance(font, 1)) + testing.expectf(t, t1_glyph_advance(font, 4) == 600, "/D advance %v", t1_glyph_advance(font, 4)) +} + +// The seven rmovetos a FLEX collects are control points, not seven subpaths. +// A reader that draws them as movetos loses the curve. +@(test) +test_t1_flex_becomes_two_curves :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + + b := t1f_glyph(t, &f, font, "B") + t1f_expect_path(t, &b, + {.Move, .Curve, .Curve, .Close}, + { + {0, 0}, + {20, 30}, {40, 60}, {60, 60}, + {80, 60}, {100, 30}, {120, 0}, + }, + "/B") + + // The reference point, the first of the seven, is not on the outline. + for pt in b.points { + testing.expectf(t, pt != Point{50, 50}, + "the FLEX reference point (50,50) reached the outline") + } +} + +// OtherSubr 3 hands the subroutine number it was given back to the `pop` and +// `callsubr` that follow. Answering 3 instead -- what the non-interpreting +// fallback pushes -- runs the do-nothing Subrs[3] every font carries and +// silently drops the replacement, which is why /C's outline is the assertion. +@(test) +test_t1_hint_replacement_calls_the_named_subr :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + c := t1f_glyph(t, &f, font, "C") + t1f_expect_path(t, &c, + {.Move, .Line, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}, {0, 100}}, + "/C") +} + +// seac draws two other glyphs, named by their StandardEncoding codes. The +// accent's offset is the composite's own sidebearing plus adx, less the +// accent's sidebearing asb -- adx measures from sidebearing point to +// sidebearing point, not origin to origin. The three terms here are 50, 100 +// and 20, so every plausible mis-reading of that formula (adx alone, adx-asb, +// sb+adx) lands somewhere else. +@(test) +test_t1_seac :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + + acc := t1f_glyph(t, &f, font, "acute") + t1f_expect_path(t, &acc, + {.Move, .Line, .Line, .Close}, + {{20, 0}, {50, 0}, {35, 40}}, + "/acute standing alone") + + // The composite's hsbw sidebearing is 50, asb is 20, adx is 100 and ady is + // 400, so the accent moves by (50 + 100 - 20, 400) = (130, 400). + comp := t1f_glyph(t, &f, font, "Aacute") + t1f_expect_path(t, &comp, + {.Move, .Line, .Line, .Line, .Close, .Move, .Line, .Line, .Close}, + { + {50, 0}, {150, 0}, {150, 200}, {50, 200}, + {150, 400}, {180, 400}, {165, 440}, + }, + "/Aacute") + + // The composite's own hsbw still gives the width. + gid, _ := t1_gid_for_name(font, "Aacute") + testing.expectf(t, t1_glyph_advance(font, gid) == 600, + "/Aacute advance %v, want the composite's own 600", t1_glyph_advance(font, gid)) +} + +// A seac naming codes StandardEncoding leaves undefined, or codes outside it +// altogether, must draw nothing rather than read past the end of the table. +@(test) +test_t1_seac_unknown_codes :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A", "B", "C"}, + chars = {T1T_NOTDEF[:], T1T_SEAC_UNDEF[:], T1T_SEAC_OOR[:], T1T_SEAC_HIGH[:]}, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "seac with unknown codes") + if font == nil { + return + } + for name in ([?]string{"A", "B", "C"}) { + p := t1f_glyph(t, &f, font, name) + testing.expectf(t, len(p.verbs) == 0, "/%s: a seac to nowhere drew %d elements", + name, len(p.verbs)) + } +} + +@(test) +test_t1_advances :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + font := t1f_font(t, &f, T1_FONT_A[:], "font A") + if font == nil { + return + } + want := [?]struct { + name: string, + w: f64, + }{ + {".notdef", 500}, {"A", 600}, {"B", 500}, {"C", 400}, + {"D", 600}, {"Aacute", 600}, {"acute", 300}, {"E", 700}, + } + for c in want { + gid, ok := t1_gid_for_name(font, c.name) + if !testing.expectf(t, ok, "/%s missing", c.name) { + continue + } + got := t1_glyph_advance(font, gid) + testing.expectf(t, got == c.w, "/%s advance %v, want %v", c.name, got, c.w) + // Asked twice, answered from the memo the second time. + testing.expectf(t, t1_glyph_advance(font, gid) == c.w, + "/%s advance changed when asked again", c.name) + } + // An index past the end is 0, not a read out of bounds. + testing.expect(t, t1_glyph_advance(font, 900) == 0, "advance for a missing gid was not 0") + + p := path_make(f.alloc) + testing.expect(t, t1_glyph_path(font, 900, &p) == .No_Glyph, + "drawing a missing gid did not report .No_Glyph") + testing.expect(t, t1_glyph_path(font, 0, nil) == .Invalid_Argument, + "drawing into a nil path did not report .Invalid_Argument") +} + +// --------------------------------------------------------------------------- +// Damage +// --------------------------------------------------------------------------- + +// A truncated eexec block yields the glyphs that were decoded before the cut. +@(test) +test_t1_truncated_eexec :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + full := t1f_font(t, &f, T1_FONT_A[:], "font A") + if full == nil { + return + } + + saw_partial := false + for cut in ([?]int{40, 120, 200, 280, 360, 440}) { + end := T1A_CLEAR_LEN + T1A_BIN_LEN - cut + font, err := t1_parse(T1_FONT_A[:end], f.alloc) + if err != .None || font == nil { + continue + } + n := t1_glyph_count(font) + testing.expectf(t, n <= t1_glyph_count(full), + "cut %d: %d glyphs, more than the intact font's %d", cut, n, t1_glyph_count(full)) + if n >= 1 && n < t1_glyph_count(full) { + saw_partial = true + testing.expectf(t, font.truncated, "cut %d lost glyphs without recording truncation", cut) + } + // Every glyph but the last one it kept -- which may itself have been + // cut in half -- must decode exactly as it does in the intact font. + for gid in 0 ..< max(n - 1, 0) { + name := font.names[gid] + ref, ok := t1_gid_for_name(full, name) + if !ok { + continue + } + got := path_make(f.alloc) + reference := path_make(f.alloc) + _ = t1_glyph_path(font, u16(gid), &got) + _ = t1_glyph_path(full, ref, &reference) + t1f_expect_path(t, &got, reference.verbs[:], reference.points[:], name) + } + } + testing.expect(t, saw_partial, + "no truncation lost a glyph, so this test proves nothing about truncation") +} + +// Truncating anywhere at all must give an error or a usable font, never a +// crash, a hang, or an out-of-bounds read while drawing what survived. +@(test) +test_t1_truncation_is_never_fatal :: proc(t: ^testing.T) { + scratch: virtual.Arena + if virtual.arena_init_growing(&scratch) != nil { + return + } + defer virtual.arena_destroy(&scratch) + alloc := virtual.arena_allocator(&scratch) + + for end := 1; end <= len(T1_FONT_A); end += 3 { + font, err := t1_parse(T1_FONT_A[:end], alloc) + if err == .None && font != nil { + for gid in 0 ..< t1_glyph_count(font) { + p := path_make(alloc) + _ = t1_glyph_path(font, u16(gid), &p) + _ = t1_glyph_advance(font, u16(gid)) + } + } + virtual.arena_free_all(&scratch) + } + testing.expect(t, true, "reached the end without crashing or hanging") +} + +// A byte flipped anywhere must be survivable too: this is what a subsetter's +// off-by-one length does to a /FontFile stream. +@(test) +test_t1_bit_flips_are_never_fatal :: proc(t: ^testing.T) { + scratch: virtual.Arena + if virtual.arena_init_growing(&scratch) != nil { + return + } + defer virtual.arena_destroy(&scratch) + alloc := virtual.arena_allocator(&scratch) + + buf := make([]byte, len(T1_FONT_A), alloc) + for at := 0; at < len(T1_FONT_A); at += 7 { + copy(buf, T1_FONT_A[:]) + buf[at] ~= 0xFF + font, err := t1_parse(buf, alloc) + if err == .None && font != nil { + for gid in 0 ..< t1_glyph_count(font) { + p := path_make(alloc) + _ = t1_glyph_path(font, u16(gid), &p) + _ = t1_glyph_advance(font, u16(gid)) + } + } + } + testing.expect(t, true, "reached the end without crashing or hanging") +} + +@(test) +test_t1_rejects_junk :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + cases := [?][]byte{ + {}, + transmute([]byte)string("not a font at all"), + transmute([]byte)string("%!FontType1-1.0\ncurrentfile eexec\n"), + // A PFB header and nothing else. + {0x80, 0x01, 0x10, 0x00, 0x00, 0x00}, + {0x80, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0x41, 0x42}, + // /CharStrings with no entries. + transmute([]byte)string("/CharStrings 4 dict dup begin\nend\n"), + } + for c, i in cases { + font, err := t1_parse(c, f.alloc) + testing.expectf(t, font == nil && err == .Bad_Type1, + "case %d gave %v, %v; want nil and .Bad_Type1", i, font, err) + } +} + +// A /Subrs or /CharStrings count that disagrees with the entries that follow +// is ignored in favour of the entries. +@(test) +test_t1_counts_that_lie :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + // /A draws through Subrs[1], so a Subrs array cut to the declared count + // would lose the outline rather than merely a hint. + for declared in ([?][2]int{{99, 40}, {1, 1}}) { + spec := T1t_Spec{ + subrs = {T1T_RETURN[:], T1T_SUBR_BOX[:]}, + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_VIA_SUBR[:]}, + declared_subrs = declared[0], + declared_chars = declared[1], + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "lying counts") + if font == nil { + continue + } + testing.expectf(t, t1_glyph_count(font) == 2, + "declared %d glyphs, wrote 2, got %d", declared[1], t1_glyph_count(font)) + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, + {.Move, .Line, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 100}}, + "/A through Subrs[1]") + } +} + +// A charstring shorter than /lenIV has no plaintext left once the padding is +// dropped. It must come back as an empty glyph, not a slice out of range. +@(test) +test_t1_charstring_shorter_than_leniv :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A", "B"}, + chars = {T1T_NOTDEF[:], nil, T1T_ENDCHAR[:]}, + // Two padding bytes written where /lenIV says four, so /A (nothing but + // padding) and /B (padding plus one byte) are both too short. + pad = 2, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "short charstrings") + if font == nil { + return + } + testing.expectf(t, t1_glyph_count(font) == 3, "glyph count %d", t1_glyph_count(font)) + for name in ([?]string{"A", "B"}) { + p := t1f_glyph(t, &f, font, name) + testing.expectf(t, len(p.verbs) == 0, "/%s drew %d elements", name, len(p.verbs)) + gid, _ := t1_gid_for_name(font, name) + testing.expectf(t, t1_glyph_advance(font, gid) == 0, "/%s advance was not 0", name) + } +} + +// A negative /lenIV is how the tools that strip the charstring cipher record +// that they did; the charstrings are then plain text. +@(test) +test_t1_negative_leniv_is_plaintext :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_LINE[:]}, + len_iv = -1, + no_encipher = true, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "/lenIV -1") + if font == nil { + return + } + testing.expectf(t, font.len_iv == -1, "/lenIV read as %d", font.len_iv) + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, {.Move, .Line, .Close}, {{0, 0}, {100, 0}}, + "/A with plaintext charstrings") +} + +// --------------------------------------------------------------------------- +// Interpreter bounds +// --------------------------------------------------------------------------- + +@(test) +test_t1_interpreter_bounds :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + // 200 numbers pushed with no operator to consume them: well past the + // operand stack, and the run has to stop rather than write off the end. + flood := make([dynamic]byte, f.alloc) + append(&flood, ..T1T_LINE[:4]) // 0 400 hsbw + append(&flood, 0x8b, 0x8b, 0x15) // 0 0 rmoveto + for _ in 0 ..< 200 { + append(&flood, 0x8b) + } + append(&flood, 0xef, 0x06, 0x09, 0x0e) // 100 hlineto closepath endchar + + spec := T1t_Spec{ + // Subrs[0] calls itself; only the depth cap ends it. + subrs = {T1T_SELFCALL[:]}, + names = {".notdef", "deep", "flood", "starved", "unknown", "halfflex", "truncnum"}, + chars = { + T1T_NOTDEF[:], T1T_DEEP[:], flood[:], T1T_STARVED[:], + T1T_UNKNOWN_OP[:], T1T_HALF_FLEX[:], T1T_TRUNC_NUM[:], + }, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "bounds") + if font == nil { + return + } + testing.expectf(t, t1_glyph_count(font) == 7, "glyph count %d", t1_glyph_count(font)) + + for gid in 0 ..< t1_glyph_count(font) { + p := path_make(f.alloc) + err := t1_glyph_path(font, u16(gid), &p) + testing.expectf(t, err == .None, "/%s gave %v", font.names[gid], err) + } + + // Stepping over one bad operator must not abandon the rest of the glyph: + // both of these draw the line that follows the damage. + for name in ([?]string{"starved", "unknown"}) { + p := t1f_glyph(t, &f, font, name) + t1f_expect_path(t, &p, {.Move, .Line, .Close}, {{0, 0}, {100, 0}}, name) + } + // A FLEX abandoned after three of its seven points joins its ends rather + // than leaving a gap across the glyph. + half := t1f_glyph(t, &f, font, "halfflex") + t1f_expect_path(t, &half, {.Move, .Line, .Close}, {{0, 0}, {30, 30}}, "halfflex") +} + +// Every branch of the Type 1 number encoding, at the boundaries between them: +// one byte for -107..107, two for the two 108..1131 forms, and a 255 prefix +// with a 32-bit two's complement integer behind it -- not the 16.16 fixed +// point that the same prefix means in a Type 2 charstring. +@(test) +test_t1_number_encoding :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_NUMBERS[:]}, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "number encoding") + if font == nil { + return + } + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, + {.Move, .Line, .Line, .Line, .Line, .Close}, + {{0, 0}, {107, -107}, {215, -215}, {1346, -1346}, {34114, -34114}}, + "/A") + testing.expectf(t, t1_glyph_advance(font, 1) == 700, "advance %v", t1_glyph_advance(font, 1)) +} + +// Glyph outlines are always closed, and fonts that leave the closepath out +// before a moveto are common enough that closing at the moveto rather than +// trusting them is the only way a stroked text render mode looks right. +@(test) +test_t1_moveto_closes_the_previous_subpath :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_TWO_SUBS[:]}, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "two subpaths, no closepath") + if font == nil { + return + } + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, + {.Move, .Line, .Close, .Move, .Line, .Close}, + {{0, 0}, {100, 0}, {100, 200}, {200, 200}}, + "/A") +} + +// A `callsubr` naming a subroutine that is not there must be stepped over. +@(test) +test_t1_callsubr_out_of_range :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + subrs = {T1T_RETURN[:]}, + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_BAD_SUBR[:]}, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "callsubr out of range") + if font == nil { + return + } + p := t1f_glyph(t, &f, font, "A") + t1f_expect_path(t, &p, {.Move, .Line, .Close}, {{0, 0}, {100, 0}}, "/A") +} + +// The three ways `callothersubr` and `pop` are asked for something this reader +// has no procedure for. +@(test) +test_t1_othersubr_edges :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + spec := T1t_Spec{ + names = {".notdef", "unknown", "barepop", "divzero"}, + chars = {T1T_NOTDEF[:], T1T_OTHERSUBR[:], T1T_BARE_POP[:], T1T_DIV_ZERO[:]}, + } + font := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "othersubr edges") + if font == nil { + return + } + + // An OtherSubr this reader does not implement leaves its two arguments for + // the two pops, exactly as the PostScript procedure would have. + unknown := t1f_glyph(t, &f, font, "unknown") + t1f_expect_path(t, &unknown, {.Move, .Line, .Close}, {{0, 0}, {100, 20}}, "unknown othersubr") + + // `pop` with nothing waiting pushes a zero rather than reading past the + // end of the results. + bare := t1f_glyph(t, &f, font, "barepop") + t1f_expect_path(t, &bare, {.Move, .Line, .Close}, {{0, 0}, {100, 0}}, "bare pop") + + // div by zero yields zero: a NaN reaching the rasteriser turns a fill into + // an unbounded scanline walk. + div := t1f_glyph(t, &f, font, "divzero") + t1f_expect_path(t, &div, {.Move, .Line, .Close}, {{0, 0}, {0, 20}}, "div by zero") +} + +// --------------------------------------------------------------------------- +// Test-side font builder +// +// The two golden fonts cover the container; these cover the damaged shapes +// that are awkward to freeze as a literal. The builder writes a font with no +// eexec layer, so the only cipher it needs is the charstring one -- and +// test_t1_builder_agrees_with_the_golden_font ties its output to font B. +// --------------------------------------------------------------------------- + +@(private = "file") +T1t_Spec :: struct { + subrs: [][]byte, + names: []string, + chars: [][]byte, + // 0 means "write the real count". + declared_subrs: int, + declared_chars: int, + // 0 means the format's default of 4. + len_iv: int, + // Padding bytes actually written; 0 means "as many as len_iv says". + pad: int, + no_encipher: bool, + no_encoding: bool, +} + +@(private = "file") +t1t_int :: proc(out: ^[dynamic]byte, v: int) { + if v < 0 { + append(out, '-') + t1t_int(out, -v) + return + } + if v >= 10 { + t1t_int(out, v / 10) + } + append(out, byte('0' + v % 10)) +} + +@(private = "file") +t1t_str :: proc(out: ^[dynamic]byte, s: string) { + append(out, ..transmute([]byte)s) +} + +// t1t_encipher is the Type 1 cipher run forwards (Type 1 spec, 7.3). +@(private = "file") +t1t_encipher :: proc(out: ^[dynamic]byte, plain: []byte, key: u16, pad: int) { + r := u32(key) + emit :: proc(out: ^[dynamic]byte, r: ^u32, p: byte) { + c := byte(u32(p) ~ (r^ >> 8)) + append(out, c) + r^ = ((u32(c) + r^) * 52845 + 22719) & 0xFFFF + } + for _ in 0 ..< pad { + emit(out, &r, 0) + } + for p in plain { + emit(out, &r, p) + } +} + +@(private = "file") +t1t_entry :: proc(out: ^[dynamic]byte, spec: ^T1t_Spec, code: []byte, allocator: mem.Allocator) { + body := make([dynamic]byte, allocator) + if spec.no_encipher { + append(&body, ..code) + } else { + pad := spec.pad + if pad == 0 { + pad = spec.len_iv == 0 ? 4 : spec.len_iv + } + t1t_encipher(&body, code, T1_CHARSTRING_KEY, pad) + } + t1t_int(out, len(body)) + t1t_str(out, " RD ") + append(out, ..body[:]) + t1t_str(out, " ND\n") +} + +// t1t_assemble writes a complete Type 1 program with a plaintext Private dict. +@(private = "file") +t1t_assemble :: proc(spec: ^T1t_Spec, allocator: mem.Allocator) -> []byte { + out := make([dynamic]byte, allocator) + + t1t_str(&out, "%!FontType1-1.0: OdinBuilt 001.001\n") + t1t_str(&out, "/FontName /OdinBuilt def\n") + t1t_str(&out, "/FontMatrix [0.001 0 0 0.001 0 0] readonly def\n") + if !spec.no_encoding { + t1t_str(&out, "/Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n") + t1t_str(&out, "dup 65 /A put\nreadonly def\n") + } + t1t_str(&out, "/FontType 1 def\ncurrentdict end\n") + + t1t_str(&out, "/Private 8 dict dup begin\n") + t1t_str(&out, "/RD {string currentfile exch readstring pop} executeonly def\n") + t1t_str(&out, "/lenIV ") + t1t_int(&out, spec.len_iv == 0 ? 4 : spec.len_iv) + t1t_str(&out, " def\n") + + t1t_str(&out, "/Subrs ") + t1t_int(&out, spec.declared_subrs != 0 ? spec.declared_subrs : len(spec.subrs)) + t1t_str(&out, " array\n") + for code, i in spec.subrs { + t1t_str(&out, "dup ") + t1t_int(&out, i) + t1t_str(&out, " ") + t1t_entry(&out, spec, code, allocator) + } + t1t_str(&out, "ND\n") + + t1t_str(&out, "2 index /CharStrings ") + t1t_int(&out, spec.declared_chars != 0 ? spec.declared_chars : len(spec.names)) + t1t_str(&out, " dict dup begin\n") + for name, i in spec.names { + append(&out, '/') + t1t_str(&out, name) + append(&out, ' ') + t1t_entry(&out, spec, i < len(spec.chars) ? spec.chars[i] : nil, allocator) + } + t1t_str(&out, "end\nend\n") + return out[:] +} + +// The builder and the golden font B must agree on the same glyph. If the +// cipher in the builder were wrong, this is where it would show. +@(test) +test_t1_builder_agrees_with_the_golden_font :: proc(t: ^testing.T) { + f := T1_Fixture{} + t1f_init(&f) + defer t1f_destroy(&f) + + golden := t1f_font(t, &f, T1_FONT_B[:], "font B") + if golden == nil { + return + } + // T1T_FONTB_A is the same charstring font B carries for /A. + spec := T1t_Spec{ + names = {".notdef", "A"}, + chars = {T1T_NOTDEF[:], T1T_FONTB_A[:]}, + } + built := t1f_font(t, &f, t1t_assemble(&spec, f.alloc), "builder") + if built == nil { + return + } + + a := t1f_glyph(t, &f, golden, "A") + b := t1f_glyph(t, &f, built, "A") + testing.expect(t, len(a.verbs) == 4, "font B's /A did not decode, so the comparison is vacuous") + t1f_expect_path(t, &b, a.verbs[:], a.points[:], "the builder's /A against font B's") +} + +// --------------------------------------------------------------------------- +// Deriving the PFB and .pfa forms from font A +// --------------------------------------------------------------------------- + +@(private = "file") +t1t_pfb :: proc(src: []byte, allocator: mem.Allocator) -> []byte { + out := make([dynamic]byte, 0, len(src) + 32, allocator) + hdr :: proc(out: ^[dynamic]byte, kind: byte, n: int) { + append(out, 0x80, kind, byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)) + } + seg1 := src[:T1A_CLEAR_LEN] + seg2 := src[T1A_CLEAR_LEN:T1A_CLEAR_LEN + T1A_BIN_LEN] + seg3 := src[T1A_CLEAR_LEN + T1A_BIN_LEN:] + hdr(&out, 1, len(seg1)) + append(&out, ..seg1) + hdr(&out, 2, len(seg2)) + append(&out, ..seg2) + hdr(&out, 1, len(seg3)) + append(&out, ..seg3) + append(&out, 0x80, 3) + return out[:] +} + +// t1t_hex_form rewrites the eexec ciphertext as the hex ASCII a .pfa carries, +// wrapped in lines the way every real one is. +@(private = "file") +t1t_hex_form :: proc(src: []byte, allocator: mem.Allocator) -> []byte { + hex := "0123456789abcdef" + out := make([dynamic]byte, 0, len(src) * 2 + 64, allocator) + append(&out, ..src[:T1A_CLEAR_LEN]) + for b, i in src[T1A_CLEAR_LEN:T1A_CLEAR_LEN + T1A_BIN_LEN] { + append(&out, hex[b >> 4]) + append(&out, hex[b & 15]) + if i % 32 == 31 { + append(&out, '\n') + } + } + append(&out, '\n') + append(&out, ..src[T1A_CLEAR_LEN + T1A_BIN_LEN:]) + return out[:] +} + +// t1f_expect_same asserts two parses of the same font agree glyph for glyph. +@(private = "file") +t1f_expect_same :: proc(t: ^testing.T, a, b: ^Type1_Font, f: ^T1_Fixture, label: string) { + if !testing.expectf(t, t1_glyph_count(a) == t1_glyph_count(b), + "%s: %d glyphs against %d", label, t1_glyph_count(a), t1_glyph_count(b)) { + return + } + testing.expectf(t, t1_glyph_count(a) == 8, + "%s: only %d glyphs, so the comparison is thin", label, t1_glyph_count(a)) + for gid in 0 ..< t1_glyph_count(a) { + pa := path_make(f.alloc) + pb := path_make(f.alloc) + _ = t1_glyph_path(a, u16(gid), &pa) + _ = t1_glyph_path(b, u16(gid), &pb) + t1f_expect_path(t, &pb, pa.verbs[:], pa.points[:], label) + testing.expectf(t, t1_glyph_advance(a, u16(gid)) == t1_glyph_advance(b, u16(gid)), + "%s: /%s advance differs", label, a.names[gid]) + } + ea := t1_builtin_encoding(a) + eb := t1_builtin_encoding(b) + if ea != nil && eb != nil { + testing.expectf(t, ea^ == eb^, "%s: encodings differ", label) + } else { + testing.expectf(t, ea == eb, "%s: one form has an encoding and the other does not", label) + } +} diff --git a/function.odin b/function.odin new file mode 100644 index 0000000..24a0b7c --- /dev/null +++ b/function.odin @@ -0,0 +1,1168 @@ +package pdf + +import "core:math" + +// PDF functions (ISO 32000-2, 7.10). Four types share one struct because +// callers only ever evaluate them, and a union would put a switch at every +// call site for no gain. + +Function_Kind :: enum u8 { + Sampled = 0, // type 0 + Exponential = 2, // type 2 + Stitching = 3, // type 3 + PostScript = 4, // type 4 +} + +// The most inputs and outputs the library will accept. The format sets no +// limit, but a shading with 33 outputs is a malformed file, not a design. +MAX_FUNCTION_INPUTS :: 32 +MAX_FUNCTION_OUTPUTS :: 32 + +Function :: struct { + kind: Function_Kind, + n_in: int, + n_out: int, + // Always 2*n_in long. Inputs are clamped to it before evaluation. + domain: []f64, + // 2*n_out long, or empty for a type 2 or 3 function that omits it. + // Outputs are clamped to it when present. + range: []f64, + + // Type 0: a sampled table. + size: []int, // n_in entries + bits_per_sample: int, + encode: []f64, // 2*n_in + decode: []f64, // 2*n_out + samples: []u8, // raw, packed at bits_per_sample + order: int, // 1 linear, 3 cubic (cubic falls back to linear) + + // Type 2: C0 + x^N * (C1 - C0). + c0: []f64, + c1: []f64, + exponent: f64, + + // Type 3: k sub-functions stitched across the domain. + functions: []^Function, + bounds: []f64, + + // Type 4: a compiled PostScript calculator program. + program: []Ps_Op, +} + +// Ps_Op is one instruction of a type 4 function, already parsed. Branches carry +// absolute jump targets so evaluation is a flat loop rather than a tree walk, +// which keeps the interpreter's step budget easy to enforce. +Ps_Op :: struct { + kind: Ps_Op_Kind, + value: f64, + // For Jump and Jump_If_False: the index to continue at. + target: int, +} + +Ps_Op_Kind :: enum u8 { + Push, + // Arithmetic + Add, Sub, Mul, Div, Idiv, Mod, Neg, Abs, Ceiling, Floor, Round, Truncate, + Sqrt, Sin, Cos, Atan, Exp, Ln, Log, Cvi, Cvr, + // Relational and boolean + Eq, Ne, Gt, Ge, Lt, Le, And, Or, Xor, Not, Bitshift, True, False, + // Stack + Pop, Exch, Dup, Copy, Index, Roll, + // Control + Jump, Jump_If_False, +} + +// --------------------------------------------------------------------------- +// Limits +// +// Everything a file controls -- how many sub-functions stitch together, how +// long a calculator program is, how many samples a table claims -- is bounded +// here, so that a hostile or merely broken function costs a fixed amount of +// time and memory instead of an unbounded one. +// --------------------------------------------------------------------------- + +// How deeply type 3 functions may nest. This is also what stops a stitching +// function that lists itself among its own /Functions: the nested load stops +// here, and because a function is only entered into the cache once it is +// completely built, the graph that results is always acyclic. +MAX_FUNCTION_DEPTH :: 8 +// Entries read from any one numeric array (/Domain, /Bounds, /Encode ...). +MAX_FUNCTION_ARRAY :: 4096 +// Sub-functions of one type 3 function. +MAX_SUB_FUNCTIONS :: 1024 +// Samples one type 0 table may hold, across every input and output. 64 Mi +// samples is far beyond any real gradient and keeps size * n_out in range. +MAX_FUNCTION_SAMPLES :: 1 << 26 +// Inputs a type 0 function is interpolated over. Multilinear interpolation +// visits 2^n_in corners per evaluation, so beyond this the nearest sample is +// used instead. Real sampled functions have one or two inputs; a shading's +// has exactly one. +MAX_INTERPOLATED_INPUTS :: 8 + +// Operands a type 4 program may stack. ISO 32000-2, 7.10.5 fixes this at 100. +MAX_PS_STACK :: 100 +// Instructions in a compiled type 4 program. +MAX_PS_OPS :: 1 << 16 +// Nesting of { } in a type 4 program. +MAX_PS_DEPTH :: 32 +// Instructions one evaluation may execute. A type 4 function has no loop +// construct, so a real program always finishes in fewer steps than it has +// instructions; this only ever fires on a program built to spin. +MAX_PS_STEPS :: 10000 + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// function_load builds the function `obj` describes -- a dictionary for types +// 2 and 3, a stream for types 0 and 4. +// +// The result is owned by the Document and must not be freed. It is cached, +// keyed by the object it was built from, because a shading that names the same +// tint transform from every one of its patterns must build it once. +function_load :: proc(doc: ^Document, obj: Object) -> (^Function, Error) { + return fn_load(doc, obj, 0) +} + +@(private) +fn_load :: proc(doc: ^Document, obj: Object, depth: int) -> (^Function, Error) { + if doc == nil { + return nil, .Invalid_Argument + } + if depth > MAX_FUNCTION_DEPTH { + add_repair(doc, .Recursion_Limit_Hit, 0) + return nil, .Recursion_Limit + } + + o := resolve(doc, obj) + key := object_identity(o) + if key != 0 { + if cached, ok := doc.fn_cache[key]; ok { + return cached, .None + } + } + + d, is_dict := to_dict(o) + if !is_dict { + return nil, .Bad_Function + } + stm, _ := to_stream(o) + + kind: Function_Kind + switch dict_int(doc, d, "FunctionType", -1) { + case 0: kind = .Sampled + case 2: kind = .Exponential + case 3: kind = .Stitching + case 4: kind = .PostScript + case: return nil, .Bad_Function + } + + f := new(Function, doc.allocator) + f.kind = kind + f.domain = fn_num_array(doc, d, "Domain") + if len(f.domain) < 2 { + // /Domain is required, but a function that omits it is still usable: + // every type but 0 has an obvious default and a shading feeding it + // will clamp to [0 1] anyway. + f.domain = fn_make_pair(doc, 0, 1) + add_repair(doc, .Bad_Resource, 0) + } + f.n_in = min(len(f.domain) / 2, MAX_FUNCTION_INPUTS) + f.range = fn_num_array(doc, d, "Range") + f.n_out = min(len(f.range) / 2, MAX_FUNCTION_OUTPUTS) + + err: Error + switch kind { + case .Sampled: err = fn_load_sampled(doc, f, d, stm) + case .Exponential: err = fn_load_exponential(doc, f, d) + case .Stitching: err = fn_load_stitching(doc, f, d, depth) + case .PostScript: err = fn_load_postscript(doc, f, stm) + } + if err != .None { + return nil, err + } + if f.n_in < 1 || f.n_out < 1 { + return nil, .Bad_Function + } + + // Entered only now that it is complete: a half-built function in the cache + // is how a self-referential /Functions array would become a cycle. + if key != 0 { + if doc.fn_cache == nil { + doc.fn_cache = make(map[uintptr]^Function, 16, doc.allocator) + } + doc.fn_cache[key] = f + } + return f, .None +} + +@(private) +fn_num_array :: proc(doc: ^Document, d: ^Dict, key: string) -> []f64 { + a := dict_array(doc, d, key) + n := array_len(a) + if n <= 0 { + return nil + } + n = min(n, MAX_FUNCTION_ARRAY) + out := make([]f64, n, doc.allocator) + for i in 0 ..< n { + // A non-finite bound would propagate into every value the function + // produces and from there into the geometry. + out[i] = clamp_finite(array_num(doc, a, i, 0), 0) + } + return out +} + +@(private) +fn_make_pair :: proc(doc: ^Document, lo, hi: f64) -> []f64 { + out := make([]f64, 2, doc.allocator) + out[0], out[1] = lo, hi + return out +} + +@(private) +fn_make_single :: proc(doc: ^Document, v: f64) -> []f64 { + out := make([]f64, 1, doc.allocator) + out[0] = v + return out +} + +// --------------------------------------------------------------------------- +// Type 0: sampled +// --------------------------------------------------------------------------- + +@(private) +fn_load_sampled :: proc(doc: ^Document, f: ^Function, d: ^Dict, stm: ^Stream) -> Error { + if stm == nil { + return .Bad_Function + } + // /Range is what says how many outputs a sample has; without it the + // sample table cannot be read at all. + if f.n_out < 1 { + return .Bad_Function + } + + switch dict_int(doc, d, "BitsPerSample", 0) { + case 1, 2, 4, 8, 12, 16, 24, 32: + f.bits_per_sample = int(dict_int(doc, d, "BitsPerSample", 8)) + case: + return .Bad_Function + } + + sizes := dict_array(doc, d, "Size") + n := min(array_len(sizes), MAX_FUNCTION_INPUTS) + if n < 1 { + return .Bad_Function + } + // /Size and /Domain must agree on how many inputs there are. Trusting the + // shorter of the two keeps every later index in range. + f.n_in = min(f.n_in, n) + f.size = make([]int, f.n_in, doc.allocator) + total := i64(1) + for i in 0 ..< f.n_in { + s := array_int(doc, sizes, i, 0) + if s < 1 { + // A zero or negative dimension would make the table empty and + // every index into it invalid. + s = 1 + add_repair(doc, .Bad_Resource, 0) + } + // A single dimension larger than the whole table may be is not a + // table this library will read; nor is one whose dimensions multiply + // out past the limit. + if s > MAX_FUNCTION_SAMPLES { + return .Bad_Function + } + f.size[i] = int(s) + total *= s + if total > MAX_FUNCTION_SAMPLES { + return .Bad_Function + } + } + if total * i64(f.n_out) > MAX_FUNCTION_SAMPLES { + return .Bad_Function + } + + f.order = int(dict_int(doc, d, "Order", 1)) + + f.encode = fn_num_array(doc, d, "Encode") + if len(f.encode) < 2 * f.n_in { + // Default /Encode is [0 Size0-1 0 Size1-1 ...] (Table 38). + enc := make([]f64, 2 * f.n_in, doc.allocator) + for i in 0 ..< f.n_in { + enc[2 * i] = 0 + enc[2 * i + 1] = f64(f.size[i] - 1) + } + f.encode = enc + } + + f.decode = fn_num_array(doc, d, "Decode") + if len(f.decode) < 2 * f.n_out { + f.decode = f.range + } + + data, derr := stream_data(doc, stm) + if derr != .None { + return .Bad_Function + } + f.samples = data + + have := i64(len(data)) * 8 + need := total * i64(f.n_out) * i64(f.bits_per_sample) + if have < need { + // Short sample tables do occur, usually from a truncated stream. The + // missing samples read as zero rather than the whole function failing. + add_repair(doc, .Bad_Resource, 0, stm.ref) + } + return .None +} + +// fn_sample reads sample number `ord` (already including the output index) out +// of the packed table. Out-of-range ordinals read as 0, which is what makes a +// truncated table safe to evaluate. +@(private) +fn_sample :: proc(f: ^Function, ord: i64) -> f64 { + if ord < 0 { + return 0 + } + bps := f.bits_per_sample + bit := ord * i64(bps) + if bit + i64(bps) > i64(len(f.samples)) * 8 { + return 0 + } + if bps == 8 { + return f64(f.samples[bit >> 3]) + } + v := u64(0) + for k in 0 ..< bps { + b := bit + i64(k) + byte_val := f.samples[b >> 3] + bit_val := (byte_val >> (7 - u8(b & 7))) & 1 + v = v << 1 | u64(bit_val) + } + return f64(v) +} + +@(private) +fn_eval_sampled :: proc(f: ^Function, in_: []f64, out: []f64) { + if len(f.size) < f.n_in || f.n_in < 1 { + return + } + + // Position within the table, split into the sample below and the fraction + // between it and the next one. + base: [MAX_FUNCTION_INPUTS]int + frac: [MAX_FUNCTION_INPUTS]f64 + for i in 0 ..< f.n_in { + x := in_[i] if i < len(in_) else 0 + e := fn_interp(x, f.domain[2 * i], f.domain[2 * i + 1], + f.encode[2 * i], f.encode[2 * i + 1]) + e = clamp(clamp_finite(e, 0), 0, f64(f.size[i] - 1)) + lo := int(math.floor(e)) + if lo > f.size[i] - 1 { lo = f.size[i] - 1 } + if lo < 0 { lo = 0 } + base[i] = lo + frac[i] = e - f64(lo) + } + + // Strides through the flattened table: the first input varies fastest + // (ISO 32000-2, 7.10.2). + stride: [MAX_FUNCTION_INPUTS]i64 + s := i64(1) + for i in 0 ..< f.n_in { + stride[i] = s + s *= i64(f.size[i]) + } + + max_val := f64(u64(1) << u64(f.bits_per_sample)) - 1 + if f.bits_per_sample == 32 { + max_val = 4294967295 + } + + // /Order 3 asks for a cubic spline. Linear interpolation is used instead: + // the difference is invisible in the gradients these tables encode, and + // the format explicitly allows a reader to substitute a lower order. + corners := 1 << uint(f.n_in) if f.n_in <= MAX_INTERPOLATED_INPUTS else 1 + + for j in 0 ..< f.n_out { + if j >= len(out) { + break + } + acc := 0.0 + for corner in 0 ..< corners { + weight := 1.0 + idx := i64(0) + for i in 0 ..< f.n_in { + bit := (corner >> uint(i)) & 1 + coord := base[i] + if bit == 1 { + weight *= frac[i] + if coord + 1 <= f.size[i] - 1 { + coord += 1 + } + } else { + weight *= 1 - frac[i] + } + idx += i64(coord) * stride[i] + } + if weight == 0 { + continue + } + acc += weight * fn_sample(f, idx * i64(f.n_out) + i64(j)) + } + + lo, hi := 0.0, 1.0 + if 2 * j + 1 < len(f.decode) { + lo, hi = f.decode[2 * j], f.decode[2 * j + 1] + } + out[j] = lo + acc * (hi - lo) / max_val + } +} + +// --------------------------------------------------------------------------- +// Type 2: exponential interpolation +// --------------------------------------------------------------------------- + +@(private) +fn_load_exponential :: proc(doc: ^Document, f: ^Function, d: ^Dict) -> Error { + f.c0 = fn_num_array(doc, d, "C0") + f.c1 = fn_num_array(doc, d, "C1") + // C0 defaults to [0.0] and C1 to [1.0] (ISO 32000-2, Table 40). + if len(f.c0) == 0 { + f.c0 = fn_make_single(doc, 0) + } + if len(f.c1) == 0 { + f.c1 = fn_make_single(doc, 1) + } + f.exponent = clamp_finite(dict_num(doc, d, "N", 1), 1) + + n := min(len(f.c0), len(f.c1)) + n = min(n, MAX_FUNCTION_OUTPUTS) + if n < 1 { + return .Bad_Function + } + // /Range is optional here; when it is absent the number of outputs is the + // length of C0 and C1. + if f.n_out == 0 || f.n_out > n { + f.n_out = n + } + // A type 2 function takes exactly one input (7.10.3). + f.n_in = 1 + return .None +} + +@(private) +fn_eval_exponential :: proc(f: ^Function, in_: []f64, out: []f64) { + x := len(in_) > 0 ? in_[0] : 0 + p := x + if f.exponent != 1 { + p = math.pow(x, f.exponent) + // A negative base under a fractional exponent is not a real number; + // the format says the domain must exclude it, so a file that does not + // gets zero rather than a NaN travelling into the raster. + if !is_finite(p) { + p = 0 + } + } + for j in 0 ..< f.n_out { + if j >= len(out) { + break + } + a := f.c0[j] if j < len(f.c0) else 0 + b := f.c1[j] if j < len(f.c1) else 1 + out[j] = a + p * (b - a) + } +} + +// --------------------------------------------------------------------------- +// Type 3: stitching +// --------------------------------------------------------------------------- + +@(private) +fn_load_stitching :: proc(doc: ^Document, f: ^Function, d: ^Dict, depth: int) -> Error { + subs := dict_array(doc, d, "Functions") + k := min(array_len(subs), MAX_SUB_FUNCTIONS) + if k < 1 { + return .Bad_Function + } + f.functions = make([]^Function, k, doc.allocator) + n_out := 0 + for i in 0 ..< k { + sub, serr := fn_load(doc, array_get_raw(subs, i), depth + 1) + if serr != .None { + // One broken sub-function leaves a hole that evaluates to zero + // rather than discarding the whole stitch. + add_repair(doc, .Bad_Resource, 0) + continue + } + f.functions[i] = sub + if n_out == 0 { + n_out = sub.n_out + } + } + f.bounds = fn_num_array(doc, d, "Bounds") + f.encode = fn_num_array(doc, d, "Encode") + if f.n_out == 0 { + // /Range is optional for a type 3 function; the sub-functions say how + // many outputs there are. + f.n_out = min(n_out, MAX_FUNCTION_OUTPUTS) + } + // A type 3 function takes exactly one input (7.10.4). + f.n_in = 1 + return .None +} + +@(private) +fn_eval_stitching :: proc(f: ^Function, in_: []f64, out: []f64) { + k := len(f.functions) + if k == 0 { + return + } + x := len(in_) > 0 ? in_[0] : 0 + d0, d1 := f.domain[0], f.domain[1] + + // Subdomain i is [Bounds[i-1], Bounds[i]), so an input exactly on a bound + // belongs to the later sub-function (ISO 32000-2, 7.10.4). + i := 0 + for i < k - 1 { + b := f.bounds[i] if i < len(f.bounds) else d1 + if x < b { + break + } + i += 1 + } + + lo := d0 + if i > 0 && i - 1 < len(f.bounds) { + lo = f.bounds[i - 1] + } + hi := d1 + if i < k - 1 && i < len(f.bounds) { + hi = f.bounds[i] + } + + e0, e1 := 0.0, 1.0 + if 2 * i + 1 < len(f.encode) { + e0, e1 = f.encode[2 * i], f.encode[2 * i + 1] + } + + sub_in: [1]f64 + sub_in[0] = fn_interp(x, lo, hi, e0, e1) + function_eval(f.functions[i], sub_in[:], out) +} + +// --------------------------------------------------------------------------- +// Type 4: the PostScript calculator +// --------------------------------------------------------------------------- + +@(private) +fn_load_postscript :: proc(doc: ^Document, f: ^Function, stm: ^Stream) -> Error { + if stm == nil || f.n_out < 1 { + // Without /Range there is no way to know how many of the values left + // on the stack are outputs. + return .Bad_Function + } + src, derr := stream_data(doc, stm) + if derr != .None || len(src) == 0 { + return .Bad_Function + } + + // Compiled into scratch memory first: the Document's arena cannot release + // the intermediate blocks a growing array leaves behind. + ops := make([dynamic]Ps_Op, 0, 64, context.allocator) + defer delete(ops) + + p := Ps_Lexer{src = src} + if err := ps_compile(&p, &ops); err != .None { + return err + } + if len(ops) == 0 { + return .Bad_Function + } + + prog := make([]Ps_Op, len(ops), doc.allocator) + copy(prog, ops[:]) + f.program = prog + return .None +} + +@(private) +Ps_Token_Kind :: enum u8 { + End, + Number, + Lbrace, + Rbrace, + Operator, + If, + Ifelse, + Unknown, +} + +@(private) +Ps_Token :: struct { + kind: Ps_Token_Kind, + op: Ps_Op_Kind, + value: f64, +} + +@(private) +Ps_Lexer :: struct { + src: []byte, + pos: int, +} + +@(private) +ps_is_space :: proc "contextless" (c: byte) -> bool { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == 0 +} + +@(private) +ps_next :: proc(p: ^Ps_Lexer) -> Ps_Token { + for p.pos < len(p.src) { + c := p.src[p.pos] + if ps_is_space(c) { + p.pos += 1 + continue + } + // A comment runs to the end of the line, as everywhere else in PDF. + if c == '%' { + for p.pos < len(p.src) && p.src[p.pos] != '\n' && p.src[p.pos] != '\r' { + p.pos += 1 + } + continue + } + break + } + if p.pos >= len(p.src) { + return Ps_Token{kind = .End} + } + + c := p.src[p.pos] + if c == '{' { + p.pos += 1 + return Ps_Token{kind = .Lbrace} + } + if c == '}' { + p.pos += 1 + return Ps_Token{kind = .Rbrace} + } + + start := p.pos + for p.pos < len(p.src) { + b := p.src[p.pos] + if ps_is_space(b) || b == '{' || b == '}' || b == '%' { + break + } + p.pos += 1 + } + word := string(p.src[start:p.pos]) + if len(word) == 0 { + // Cannot happen -- the delimiters above are all consumed as tokens -- + // but a zero-length word would spin the parser, so it ends it. + p.pos = len(p.src) + return Ps_Token{kind = .End} + } + + if c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9') { + obj, ok := parse_number(word) + if ok { + v, _ := to_number(obj) + return Ps_Token{kind = .Number, value = clamp_finite(v, 0)} + } + return Ps_Token{kind = .Unknown} + } + return ps_operator_token(word) +} + +@(private) +ps_operator_token :: proc(word: string) -> Ps_Token { + k: Ps_Op_Kind + switch word { + case "add": k = .Add + case "sub": k = .Sub + case "mul": k = .Mul + case "div": k = .Div + case "idiv": k = .Idiv + case "mod": k = .Mod + case "neg": k = .Neg + case "abs": k = .Abs + case "ceiling": k = .Ceiling + case "floor": k = .Floor + case "round": k = .Round + case "truncate": k = .Truncate + case "sqrt": k = .Sqrt + case "sin": k = .Sin + case "cos": k = .Cos + case "atan": k = .Atan + case "exp": k = .Exp + case "ln": k = .Ln + case "log": k = .Log + case "cvi": k = .Cvi + case "cvr": k = .Cvr + case "eq": k = .Eq + case "ne": k = .Ne + case "gt": k = .Gt + case "ge": k = .Ge + case "lt": k = .Lt + case "le": k = .Le + case "and": k = .And + case "or": k = .Or + case "xor": k = .Xor + case "not": k = .Not + case "bitshift": k = .Bitshift + case "true": k = .True + case "false": k = .False + case "pop": k = .Pop + case "exch": k = .Exch + case "dup": k = .Dup + case "copy": k = .Copy + case "index": k = .Index + case "roll": k = .Roll + case "if": return Ps_Token{kind = .If} + case "ifelse": return Ps_Token{kind = .Ifelse} + case: return Ps_Token{kind = .Unknown} + } + return Ps_Token{kind = .Operator, op = k} +} + +// ps_compile reads the whole program. The outer braces are expected but not +// required: a stream that forgot them still compiles, because the alternative +// is refusing a function every other viewer runs. +@(private) +ps_compile :: proc(p: ^Ps_Lexer, ops: ^[dynamic]Ps_Op) -> Error { + save := p.pos + t := ps_next(p) + if t.kind == .Lbrace { + return ps_compile_block(p, ops, 0) + } + p.pos = save + return ps_compile_block(p, ops, 0) +} + +// ps_compile_block emits instructions until the '}' that closes the current +// block, which it consumes. Conditionals become jumps with absolute targets: +// +// {A} if -> jump_if_false end; A; end: +// {A} {B} ifelse -> jump_if_false b; A; jump end; b: B; end: +// +// so a slot for the branch is reserved before the block body is emitted and +// patched once the operator that follows the body says what it is. +@(private) +ps_compile_block :: proc(p: ^Ps_Lexer, ops: ^[dynamic]Ps_Op, depth: int) -> Error { + if depth > MAX_PS_DEPTH { + return .Bad_Function + } + for { + if len(ops) > MAX_PS_OPS { + return .Bad_Function + } + t := ps_next(p) + switch t.kind { + case .End: + // End of stream: a missing '}' is not worth rejecting a program + // that is otherwise complete. + return .None + case .Rbrace: + return .None + case .Number: + append(ops, Ps_Op{kind = .Push, value = t.value}) + case .Operator: + append(ops, Ps_Op{kind = t.op}) + case .Lbrace: + branch := len(ops) + append(ops, Ps_Op{kind = .Jump}) + ps_compile_block(p, ops, depth + 1) or_return + + save := p.pos + t2 := ps_next(p) + #partial switch t2.kind { + case .If: + ops[branch] = Ps_Op{kind = .Jump_If_False, target = len(ops)} + case .Lbrace: + skip := len(ops) + append(ops, Ps_Op{kind = .Jump}) + ps_compile_block(p, ops, depth + 1) or_return + t3 := ps_next(p) + if t3.kind != .Ifelse { + return .Bad_Function + } + ops[branch] = Ps_Op{kind = .Jump_If_False, target = skip + 1} + ops[skip] = Ps_Op{kind = .Jump, target = len(ops)} + case: + // A block that no `if` or `ifelse` consumes never runs in + // PostScript either, so it is jumped over rather than + // executed. + ops[branch] = Ps_Op{kind = .Jump, target = len(ops)} + p.pos = save + } + case .If, .Ifelse, .Unknown: + // An `if` with no block before it, or a word this calculator has + // no operator for. Skipping it keeps the rest of the program + // running, which is what a partly-understood tint transform needs. + } + } +} + +// Ps_Value is one operand. The type 4 language distinguishes booleans from +// numbers -- `and`, `or`, `xor` and `not` are logical on the first and bitwise +// on the second -- so the tag travels with the value. +@(private) +Ps_Value :: struct { + v: f64, + is_bool: bool, +} + +@(private) +Ps_Stack :: struct { + v: [MAX_PS_STACK]Ps_Value, + sp: int, + // Set when a push found the stack full. Evaluation stops, leaving whatever + // has been computed so far rather than discarding it. + overflow: bool, +} + +@(private) +ps_push :: proc(s: ^Ps_Stack, val: f64, is_bool := false) { + if s.sp >= MAX_PS_STACK { + s.overflow = true + return + } + s.v[s.sp] = Ps_Value{v = clamp_finite(val, 0), is_bool = is_bool} + s.sp += 1 +} + +// ps_pop returns zero on an empty stack. An underflowing program is malformed, +// but a defined result costs nothing and a crash costs the page. +@(private) +ps_pop :: proc(s: ^Ps_Stack) -> Ps_Value { + if s.sp <= 0 { + return Ps_Value{} + } + s.sp -= 1 + return s.v[s.sp] +} + +@(private) +ps_int :: proc(x: f64) -> i32 { + if !is_finite(x) { + return 0 + } + y := math.trunc(x) + if y > 2147483647 { + return 2147483647 + } + if y < -2147483648 { + return -2147483648 + } + return i32(y) +} + +@(private) +ps_exec :: proc(prog: []Ps_Op, s: ^Ps_Stack) { + pc := 0 + for steps := 0; steps < MAX_PS_STEPS; steps += 1 { + if pc < 0 || pc >= len(prog) || s.overflow { + return + } + op := prog[pc] + pc += 1 + + switch op.kind { + case .Push: + ps_push(s, op.value) + + case .Add: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v + b.v) + case .Sub: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v - b.v) + case .Mul: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v * b.v) + case .Div: + b, a := ps_pop(s), ps_pop(s) + // Division by zero is undefined in PostScript; zero is the value + // every other reader substitutes. + ps_push(s, b.v != 0 ? a.v / b.v : 0) + case .Idiv: + b, a := ps_pop(s), ps_pop(s) + ib, ia := ps_int(b.v), ps_int(a.v) + // -2147483648 idiv -1 overflows a two's-complement divide. + ps_push(s, ib != 0 && !(ia == -2147483648 && ib == -1) ? f64(ia / ib) : 0) + case .Mod: + b, a := ps_pop(s), ps_pop(s) + ib, ia := ps_int(b.v), ps_int(a.v) + ps_push(s, ib != 0 && !(ia == -2147483648 && ib == -1) ? f64(ia % ib) : 0) + case .Neg: + a := ps_pop(s) + ps_push(s, -a.v) + case .Abs: + a := ps_pop(s) + ps_push(s, abs(a.v)) + case .Ceiling: + a := ps_pop(s) + ps_push(s, math.ceil(a.v)) + case .Floor: + a := ps_pop(s) + ps_push(s, math.floor(a.v)) + case .Round: + a := ps_pop(s) + // PostScript rounds a halfway case towards positive infinity. + ps_push(s, math.floor(a.v + 0.5)) + case .Truncate: + a := ps_pop(s) + ps_push(s, math.trunc(a.v)) + case .Sqrt: + a := ps_pop(s) + ps_push(s, a.v > 0 ? math.sqrt(a.v) : 0) + case .Sin: + a := ps_pop(s) + ps_push(s, math.sin(a.v * math.PI / 180)) + case .Cos: + a := ps_pop(s) + ps_push(s, math.cos(a.v * math.PI / 180)) + case .Atan: + den, num := ps_pop(s), ps_pop(s) + deg := math.atan2(num.v, den.v) * 180 / math.PI + // `atan` answers in degrees between 0 and 360, never negative. + if deg < 0 { + deg += 360 + } + ps_push(s, deg) + case .Exp: + e, base := ps_pop(s), ps_pop(s) + r := math.pow(base.v, e.v) + ps_push(s, is_finite(r) ? r : 0) + case .Ln: + a := ps_pop(s) + ps_push(s, a.v > 0 ? math.ln(a.v) : 0) + case .Log: + a := ps_pop(s) + ps_push(s, a.v > 0 ? math.log10(a.v) : 0) + case .Cvi: + a := ps_pop(s) + ps_push(s, math.trunc(a.v)) + case .Cvr: + a := ps_pop(s) + ps_push(s, a.v) + + case .Eq: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v == b.v ? 1 : 0, true) + case .Ne: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v != b.v ? 1 : 0, true) + case .Gt: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v > b.v ? 1 : 0, true) + case .Ge: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v >= b.v ? 1 : 0, true) + case .Lt: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v < b.v ? 1 : 0, true) + case .Le: + b, a := ps_pop(s), ps_pop(s) + ps_push(s, a.v <= b.v ? 1 : 0, true) + case .And: + b, a := ps_pop(s), ps_pop(s) + if a.is_bool && b.is_bool { + ps_push(s, (a.v != 0 && b.v != 0) ? 1 : 0, true) + } else { + ps_push(s, f64(ps_int(a.v) & ps_int(b.v))) + } + case .Or: + b, a := ps_pop(s), ps_pop(s) + if a.is_bool && b.is_bool { + ps_push(s, (a.v != 0 || b.v != 0) ? 1 : 0, true) + } else { + ps_push(s, f64(ps_int(a.v) | ps_int(b.v))) + } + case .Xor: + b, a := ps_pop(s), ps_pop(s) + if a.is_bool && b.is_bool { + ps_push(s, ((a.v != 0) != (b.v != 0)) ? 1 : 0, true) + } else { + ps_push(s, f64(ps_int(a.v) ~ ps_int(b.v))) + } + case .Not: + a := ps_pop(s) + if a.is_bool { + ps_push(s, a.v == 0 ? 1 : 0, true) + } else { + ps_push(s, f64(~ps_int(a.v))) + } + case .Bitshift: + sh, a := ps_pop(s), ps_pop(s) + n := ps_int(sh.v) + bits := u32(ps_int(a.v)) + // A shift of 32 or more is undefined in C and in Odin; the value + // it must produce here is zero. + switch { + case n >= 32 || n <= -32: ps_push(s, 0) + case n >= 0: ps_push(s, f64(i32(bits << u32(n)))) + case: ps_push(s, f64(i32(bits >> u32(-n)))) + } + case .True: + ps_push(s, 1, true) + case .False: + ps_push(s, 0, true) + + case .Pop: + ps_pop(s) + case .Exch: + b, a := ps_pop(s), ps_pop(s) + if s.sp + 2 <= MAX_PS_STACK { + s.v[s.sp] = b + s.v[s.sp + 1] = a + s.sp += 2 + } else { + s.overflow = true + } + case .Dup: + if s.sp > 0 { + top := s.v[s.sp - 1] + ps_push(s, top.v, top.is_bool) + } else { + ps_push(s, 0) + } + case .Copy: + n := int(ps_int(ps_pop(s).v)) + if n <= 0 { + break + } + if n > s.sp || s.sp + n > MAX_PS_STACK { + s.overflow = true + break + } + src := s.sp - n + for i in 0 ..< n { + s.v[s.sp + i] = s.v[src + i] + } + s.sp += n + case .Index: + n := int(ps_int(ps_pop(s).v)) + if n < 0 || n >= s.sp { + // Indexing past the bottom of the stack is malformed; zero + // keeps the program running. + ps_push(s, 0) + break + } + val := s.v[s.sp - 1 - n] + ps_push(s, val.v, val.is_bool) + case .Roll: + j := int(ps_int(ps_pop(s).v)) + n := int(ps_int(ps_pop(s).v)) + if n <= 0 || n > s.sp { + break + } + // Rotation by three reversals, so no scratch stack is needed. + k := j %% n + if k != 0 { + lo := s.sp - n + ps_reverse(s.v[lo:lo + n - k]) + ps_reverse(s.v[lo + n - k:lo + n]) + ps_reverse(s.v[lo:lo + n]) + } + + case .Jump: + pc = op.target + case .Jump_If_False: + cond := ps_pop(s) + if cond.v == 0 { + pc = op.target + } + } + } +} + +@(private) +ps_reverse :: proc(vals: []Ps_Value) { + for i, j := 0, len(vals) - 1; i < j; i, j = i + 1, j - 1 { + vals[i], vals[j] = vals[j], vals[i] + } +} + +@(private) +fn_eval_postscript :: proc(f: ^Function, in_: []f64, out: []f64) { + s: Ps_Stack + for i in 0 ..< f.n_in { + ps_push(&s, i < len(in_) ? in_[i] : 0) + } + ps_exec(f.program, &s) + + // The outputs are the topmost n_out values, bottom-most first. A program + // that left fewer behind gets zeros for the rest. + for j in 0 ..< f.n_out { + if j >= len(out) { + break + } + idx := s.sp - f.n_out + j + out[j] = idx >= 0 && idx < s.sp ? s.v[idx].v : 0 + } +} + +// --------------------------------------------------------------------------- +// Evaluation +// --------------------------------------------------------------------------- + +// function_eval evaluates `fn` at `in_` and writes n_out values into `out`. +// +// Inputs are clamped to /Domain and outputs to /Range, both of which the +// format requires (ISO 32000-2, 7.10.1) and neither of which is optional in +// practice: a shading whose tint escapes its range paints garbage. `out` may +// be shorter than n_out, in which case the extra outputs are dropped; it is +// always fully written, so a nil or broken function reads as zeros rather than +// as whatever the caller's buffer held. +function_eval :: proc(fn: ^Function, in_: []f64, out: []f64) { + for i in 0 ..< len(out) { + out[i] = 0 + } + if fn == nil || fn.n_out < 1 { + return + } + + clamped: [MAX_FUNCTION_INPUTS]f64 + n := min(fn.n_in, MAX_FUNCTION_INPUTS) + for i in 0 ..< n { + x := i < len(in_) ? clamp_finite(in_[i], 0) : 0 + if 2 * i + 1 < len(fn.domain) { + lo, hi := fn.domain[2 * i], fn.domain[2 * i + 1] + if lo > hi { + lo, hi = hi, lo + } + x = clamp(x, lo, hi) + } + clamped[i] = x + } + + switch fn.kind { + case .Sampled: fn_eval_sampled(fn, clamped[:n], out) + case .Exponential: fn_eval_exponential(fn, clamped[:n], out) + case .Stitching: fn_eval_stitching(fn, clamped[:n], out) + case .PostScript: fn_eval_postscript(fn, clamped[:n], out) + } + + for j in 0 ..< min(fn.n_out, len(out)) { + out[j] = clamp_finite(out[j], 0) + if 2 * j + 1 < len(fn.range) { + lo, hi := fn.range[2 * j], fn.range[2 * j + 1] + if lo > hi { + lo, hi = hi, lo + } + out[j] = clamp(out[j], lo, hi) + } + } +} + +// fn_interp maps x from [xmin xmax] onto [ymin ymax]. A degenerate source +// interval -- which /Bounds and /Encode arrays out of real files do produce -- +// maps to the start of the destination rather than dividing by zero. +@(private) +fn_interp :: proc(x, xmin, xmax, ymin, ymax: f64) -> f64 { + if xmax == xmin { + return ymin + } + return ymin + (x - xmin) * (ymax - ymin) / (xmax - xmin) +} diff --git a/function_test.odin b/function_test.odin new file mode 100644 index 0000000..c896794 --- /dev/null +++ b/function_test.odin @@ -0,0 +1,873 @@ +package pdf + +import "core:math" +import "core:testing" + +// --------------------------------------------------------------------------- +// Shared fixtures for the colour and function tests +// +// Both layers need a Document to allocate from and to resolve references +// against, and a few objects that can only be reached by reference: a function +// that names itself, a colour space named from a resource dictionary. The file +// below is opened by `cf_open`; its cross-reference table is deliberately +// absent, which the reader repairs by scanning, because what these tests need +// from it is objects, not structure. +// --------------------------------------------------------------------------- + +@(private) +CF_PDF: string : "%PDF-1.7\n" + +"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" + +"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + +"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>\nendobj\n" + +// A spot colour: full tint is 100% magenta and yellow, i.e. red. +"5 0 obj\n[/Separation /Spot /DeviceCMYK 6 0 R]\nendobj\n" + +"6 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 1 1 0] /N 1 >>\nendobj\n" + +// A stitching function whose first sub-function is itself. +"7 0 obj\n<< /FunctionType 3 /Domain [0 1] /Functions [7 0 R 8 0 R] " + +"/Bounds [0.5] /Encode [0 1 0 1] >>\nendobj\n" + +"8 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>\nendobj\n" + +// A colour space that names itself through the resource dictionary. +"9 0 obj\n/Self\nendobj\n" + +// An Indexed space with a four-entry table, reached by reference. +"10 0 obj\n[/Indexed /DeviceRGB 3 ]\nendobj\n" + +"trailer\n<< /Size 11 /Root 1 0 R >>\n" + +@(private) +cf_open :: proc(t: ^testing.T) -> ^Document { + doc, err := open_bytes(transmute([]byte)CF_PDF, Open_Options{skip_page_tree = true}) + testing.expect_value(t, err, Error.None) + testing.expect(t, doc != nil, "fixture document did not open") + return doc +} + +// cf_parse parses one object out of a source literal into the Document's +// arena. The literal is static, which is what lets names and strings borrow +// from it for as long as the Document lives. +@(private) +cf_parse :: proc(doc: ^Document, src: string) -> Object { + p := parser_make(doc, transmute([]byte)src, doc.allocator) + o, _ := parse_object(&p) + return o +} + +@(private) +cf_bytes :: proc(s: string) -> []byte { + return transmute([]byte)s +} + +// cf_stream builds a stream out of a dictionary literal and a payload. The +// payload is stored, not copied, so it must be static or arena memory. +@(private) +cf_stream :: proc(doc: ^Document, dict_src: string, payload: []byte) -> ^Stream { + d, _ := to_dict(cf_parse(doc, dict_src)) + return new_stream(d, payload, doc.allocator) +} + +@(private) +cf_close :: proc(t: ^testing.T, doc: ^Document) { + close(doc) +} + +@(private = "file") +EPS :: 1e-9 + +@(private = "file") +expect_near :: proc(t: ^testing.T, got, want: f64, tol: f64, what: string, + loc := #caller_location) { + if !(abs(got - want) <= tol) { + testing.expectf(t, false, "%s: got %f, want %f", what, got, want, loc = loc) + } +} + +// eval1 evaluates a one-output function at one input. +@(private = "file") +eval1 :: proc(f: ^Function, x: f64) -> f64 { + if f == nil { + return math.QNAN_F64 + } + in_ := [1]f64{x} + out: [MAX_FUNCTION_OUTPUTS]f64 + function_eval(f, in_[:], out[:max(f.n_out, 1)]) + return out[0] +} + +@(private = "file") +load_fn :: proc(t: ^testing.T, doc: ^Document, src: string) -> ^Function { + f, err := function_load(doc, cf_parse(doc, src)) + testing.expect_value(t, err, Error.None) + return f +} + +@(private = "file") +load_sampled :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte) -> ^Function { + f, err := function_load(doc, cf_stream(doc, dict_src, payload)) + testing.expect_value(t, err, Error.None) + return f +} + +@(private = "file") +load_ps :: proc(t: ^testing.T, doc: ^Document, program: string) -> ^Function { + return load_sampled(t, doc, + "<< /FunctionType 4 /Domain [0 1] /Range [-100000 100000] >>", cf_bytes(program)) +} + +// --------------------------------------------------------------------------- +// Type 2: exponential interpolation +// --------------------------------------------------------------------------- + +@(test) +test_function_type2_defaults :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // C0 defaults to [0.0] and C1 to [1.0], so this is the identity. + f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /N 1 >>") + if f == nil { return } + testing.expect_value(t, f.n_in, 1) + testing.expect_value(t, f.n_out, 1) + expect_near(t, eval1(f, 0), 0, EPS, "f(0)") + expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)") + expect_near(t, eval1(f, 1), 1, EPS, "f(1)") +} + +@(test) +test_function_type2_exponent_and_outputs :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // C0 + x^N (C1 - C0) with N = 2: at x = 0.5 the factor is 0.25. + f := load_fn(t, doc, + "<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 0.5 0] /N 2 >>") + if f == nil { return } + testing.expect_value(t, f.n_out, 3) + + in_ := [1]f64{0.5} + out: [3]f64 + function_eval(f, in_[:], out[:]) + expect_near(t, out[0], 0.25, EPS, "out0") + expect_near(t, out[1], 0.125, EPS, "out1") + expect_near(t, out[2], 0, EPS, "out2") + + // C0 is returned exactly at the bottom of the domain whatever N is. + in_[0] = 0 + function_eval(f, in_[:], out[:]) + expect_near(t, out[0], 0, EPS, "out0 at 0") +} + +// A negative base under a fractional exponent has no real value. The domain is +// supposed to exclude it; a file whose domain does not must still produce a +// number rather than a NaN. +@(test) +test_function_type2_negative_base :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_fn(t, doc, + "<< /FunctionType 2 /Domain [-1 1] /C0 [0] /C1 [1] /N 0.5 >>") + if f == nil { return } + v := eval1(f, -1) + testing.expect(t, is_finite(v), "a negative base must not produce a NaN") +} + +// --------------------------------------------------------------------------- +// Domain and range clamping (ISO 32000-2, 7.10.1) +// --------------------------------------------------------------------------- + +@(test) +test_function_domain_clamped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>") + if f == nil { return } + // Inputs outside /Domain are clamped to it, not extrapolated. + expect_near(t, eval1(f, 5), 1, EPS, "f(5)") + expect_near(t, eval1(f, -5), 0, EPS, "f(-5)") + // And a non-finite input cannot leak through into the output. + testing.expect(t, is_finite(eval1(f, math.INF_F64)), "infinite input") + testing.expect(t, is_finite(eval1(f, math.QNAN_F64)), "NaN input") +} + +@(test) +test_function_range_clamped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The interpolation reaches 1.0 but /Range stops at 0.5. + f := load_fn(t, doc, + "<< /FunctionType 2 /Domain [0 1] /Range [0 0.5] /C0 [0] /C1 [1] /N 1 >>") + if f == nil { return } + expect_near(t, eval1(f, 1), 0.5, EPS, "f(1) clamped to /Range") + expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25) inside /Range") + + // A type 4 program is clamped the same way: without it a shading paints + // whatever the calculator happened to leave behind. + g := load_sampled(t, doc, + "<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes("{ pop 42 }")) + if g == nil { return } + expect_near(t, eval1(g, 0), 1, EPS, "42 clamped to /Range") +} + +// --------------------------------------------------------------------------- +// Type 0: sampled +// --------------------------------------------------------------------------- + +@(test) +test_function_type0_linear :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Two 8-bit samples, 0 and 255, over [0 1]. + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF}) + if f == nil { return } + testing.expect_value(t, f.n_in, 1) + testing.expect_value(t, f.n_out, 1) + expect_near(t, eval1(f, 0), 0, EPS, "f(0)") + expect_near(t, eval1(f, 1), 1, EPS, "f(1)") + // Halfway between the two samples is 127.5/255. + expect_near(t, eval1(f, 0.5), 0.5, EPS, "f(0.5)") + expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)") +} + +@(test) +test_function_type0_two_outputs :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Two samples of two outputs each, interleaved: (0,255) then (255,0). + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1 0 1] /Size [2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF, 0xFF, 0x00}) + if f == nil { return } + testing.expect_value(t, f.n_out, 2) + + in_ := [1]f64{0.5} + out: [2]f64 + function_eval(f, in_[:], out[:]) + expect_near(t, out[0], 0.5, EPS, "out0 midway") + expect_near(t, out[1], 0.5, EPS, "out1 midway") + + in_[0] = 1 + function_eval(f, in_[:], out[:]) + expect_near(t, out[0], 1, EPS, "out0 at 1") + expect_near(t, out[1], 0, EPS, "out1 at 1") +} + +// A 2x2 table: the first input varies fastest (7.10.2), so the samples are +// (0,0) (1,0) (0,1) (1,1). +@(test) +test_function_type0_bilinear :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1] /Size [2 2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF, 0xFF, 0x00}) + if f == nil { return } + testing.expect_value(t, f.n_in, 2) + + check :: proc(t: ^testing.T, f: ^Function, x, y, want: f64, what: string) { + in_ := [2]f64{x, y} + out: [1]f64 + function_eval(f, in_[:], out[:]) + if !(abs(out[0] - want) <= 1e-9) { + testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want) + } + } + check(t, f, 0, 0, 0, "corner (0,0)") + check(t, f, 1, 0, 1, "corner (1,0)") + check(t, f, 0, 1, 1, "corner (0,1)") + check(t, f, 1, 1, 0, "corner (1,1)") + // The centre averages all four corners, which is what makes this a test of + // interpolation rather than of nearest-neighbour lookup. + check(t, f, 0.5, 0.5, 0.5, "centre") + check(t, f, 0.5, 0, 0.5, "midway along x") + check(t, f, 0.25, 0, 0.25, "quarter along x") + check(t, f, 0, 0.75, 0.75, "three quarters along y") +} + +// Three inputs, eight samples: sample value 0 or 255 by parity, so every +// corner and the centre have hand-checkable values. +@(test) +test_function_type0_three_inputs :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Index = i0 + 2*i1 + 4*i2. Values: 0, 255, 0, 255, 0, 255, 0, 255 -- so + // the table depends only on the first input. + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1 0 1 0 1] /Range [0 1] /Size [2 2 2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF}) + if f == nil { return } + testing.expect_value(t, f.n_in, 3) + + check :: proc(t: ^testing.T, f: ^Function, x, y, z, want: f64, what: string) { + in_ := [3]f64{x, y, z} + out: [1]f64 + function_eval(f, in_[:], out[:]) + if !(abs(out[0] - want) <= 1e-9) { + testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want) + } + } + check(t, f, 0, 0, 0, 0, "origin") + check(t, f, 1, 0, 0, 1, "along x") + check(t, f, 0, 1, 1, 0, "y and z do not matter") + check(t, f, 0.5, 0.5, 0.5, 0.5, "centre") + check(t, f, 0.25, 1, 0.5, 0.25, "quarter along x, anywhere else") +} + +// Every bit width the format allows (Table 38). One sample of all zero bits +// and one of all one bits, so the decoded values must be exactly 0 and 1 +// whatever the width is. +@(test) +test_function_type0_bit_widths :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + Case :: struct { + dict: string, + payload: []byte, + } + cases := []Case{ + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 1 >>", + []byte{0b0100_0000}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 2 >>", + []byte{0b0011_0000}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 4 >>", + []byte{0x0F}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 12 >>", + []byte{0x00, 0x0F, 0xFF}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 16 >>", + []byte{0x00, 0x00, 0xFF, 0xFF}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 24 >>", + []byte{0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF}}, + {"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 32 >>", + []byte{0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}}, + } + for c, i in cases { + f := load_sampled(t, doc, c.dict, c.payload) + if f == nil { + testing.expectf(t, false, "case %d did not load", i) + continue + } + expect_near(t, eval1(f, 0), 0, EPS, "low sample") + expect_near(t, eval1(f, 1), 1, EPS, "high sample") + expect_near(t, eval1(f, 0.5), 0.5, EPS, "midpoint") + } + + // A width the format does not define is refused rather than guessed at. + _, err := function_load(doc, cf_stream(doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 7 >>", + []byte{0x00, 0xFF})) + testing.expect_value(t, err, Error.Bad_Function) +} + +@(test) +test_function_type0_encode_and_decode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Encode reverses the table: input 0 reads the last sample. + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 " + + "/Encode [1 0] >>", + []byte{0x00, 0xFF}) + if f == nil { return } + expect_near(t, eval1(f, 0), 1, EPS, "reversed f(0)") + expect_near(t, eval1(f, 1), 0, EPS, "reversed f(1)") + + // /Decode maps the raw samples onto a different interval than /Range does. + g := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 100] /Size [2] /BitsPerSample 8 " + + "/Decode [10 20] >>", + []byte{0x00, 0xFF}) + if g == nil { return } + expect_near(t, eval1(g, 0), 10, EPS, "decoded low") + expect_near(t, eval1(g, 1), 20, EPS, "decoded high") + expect_near(t, eval1(g, 0.5), 15, EPS, "decoded midpoint") + + // A domain that is not [0 1] is mapped onto the samples by /Encode's + // default, which is [0 Size-1]. + h := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [100 200] /Range [0 1] /Size [2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF}) + if h == nil { return } + expect_near(t, eval1(h, 100), 0, EPS, "domain low") + expect_near(t, eval1(h, 150), 0.5, EPS, "domain middle") + expect_near(t, eval1(h, 200), 1, EPS, "domain high") +} + +// /Order 3 asks for a cubic spline; linear interpolation is substituted, which +// the format allows. The function must still load and still interpolate. +@(test) +test_function_type0_cubic_falls_back :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 /Order 3 >>", + []byte{0x00, 0xFF}) + if f == nil { return } + testing.expect_value(t, f.order, 3) + expect_near(t, eval1(f, 0.5), 0.5, EPS, "cubic falls back to linear") +} + +// A sample table shorter than /Size claims is common in truncated files. The +// missing samples read as zero; nothing reads past the buffer. +@(test) +test_function_type0_truncated_samples :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [16] /BitsPerSample 8 >>", + []byte{0xFF, 0xFF}) + if f == nil { return } + expect_near(t, eval1(f, 0), 1, EPS, "the samples that are there") + expect_near(t, eval1(f, 1), 0, EPS, "the ones that are not read as zero") + testing.expect(t, doc_has_repair(doc, .Bad_Resource), "a short table is a repair") +} + +@(test) +test_function_type0_bad_sizes :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A zero dimension would make every index into the table invalid. + f := load_sampled(t, doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [0] /BitsPerSample 8 >>", + []byte{0x80}) + if f != nil { + testing.expect(t, is_finite(eval1(f, 0.5)), "a zero /Size must still evaluate") + } + + // A table with more samples than the library will hold is refused, not + // allocated. + _, err := function_load(doc, cf_stream(doc, + "<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2000000000] /BitsPerSample 8 >>", + []byte{0x00, 0xFF})) + testing.expect_value(t, err, Error.Bad_Function) + + // /Range is what says how many outputs a sample carries; without it the + // table cannot be read at all. + _, err2 := function_load(doc, cf_stream(doc, + "<< /FunctionType 0 /Domain [0 1] /Size [2] /BitsPerSample 8 >>", + []byte{0x00, 0xFF})) + testing.expect_value(t, err2, Error.Bad_Function) +} + +// --------------------------------------------------------------------------- +// Type 3: stitching +// --------------------------------------------------------------------------- + +@(test) +test_function_type3_boundary :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Two sub-functions that disagree at the bound: the first ends at 0.5, the + // second starts at 1.0. An input exactly on the bound belongs to the later + // one (7.10.4), so 0.5 must read 1.0 and not 0.5. + f := load_fn(t, doc, + "<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>") + if f == nil { return } + testing.expect_value(t, f.n_in, 1) + testing.expect_value(t, f.n_out, 1) + + // First half: [0 0.5] encodes onto [0 1], so 0.25 lands at 0.5 of a + // function that runs 0 -> 0.5. + expect_near(t, eval1(f, 0), 0, EPS, "start of the first sub-function") + expect_near(t, eval1(f, 0.25), 0.25, EPS, "middle of the first sub-function") + expect_near(t, eval1(f, 0.5), 1, EPS, "exactly on the bound is the later function") + expect_near(t, eval1(f, 0.75), 0.5, EPS, "middle of the second sub-function") + expect_near(t, eval1(f, 1), 0, EPS, "end of the domain") +} + +@(test) +test_function_type3_three_way :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Three constant sub-functions, so the value alone says which one ran. + f := load_fn(t, doc, + "<< /FunctionType 3 /Domain [0 1] /Bounds [0.25 0.75] /Encode [0 1 0 1 0 1] /Functions [" + + "<< /FunctionType 2 /Domain [0 1] /C0 [10] /C1 [10] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [20] /C1 [20] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [30] /C1 [30] /N 1 >>] >>") + if f == nil { return } + expect_near(t, eval1(f, 0), 10, EPS, "below the first bound") + expect_near(t, eval1(f, 0.24), 10, EPS, "just below the first bound") + expect_near(t, eval1(f, 0.25), 20, EPS, "on the first bound") + expect_near(t, eval1(f, 0.74), 20, EPS, "just below the second bound") + expect_near(t, eval1(f, 0.75), 30, EPS, "on the second bound") + expect_near(t, eval1(f, 1), 30, EPS, "top of the domain") +} + +// A stitching function that lists itself among its sub-functions. Loading must +// terminate, and the branch that is not self-referential must still work. +@(test) +test_function_type3_self_reference :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f, err := function_load(doc, Ref{7, 0}) + testing.expect_value(t, err, Error.None) + if f == nil { return } + + // The second sub-function is the ordinary one: [0.5 1] encodes onto [0 1], + // so 0.75 lands halfway up a ramp from 0 to 1. + expect_near(t, eval1(f, 0.75), 0.5, EPS, "the sub-function that is not a cycle") + // And the self-referential branch terminates with a defined value. + testing.expect(t, is_finite(eval1(f, 0.25)), "the cyclic branch must terminate") + testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "the cycle is a repair") +} + +@(test) +test_function_type3_broken_sub_function :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // One sub-function is nonsense. The other must still be reachable. + f := load_fn(t, doc, + "<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" + + "<< /FunctionType 9 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [7] /C1 [7] /N 1 >>] >>") + if f == nil { return } + expect_near(t, eval1(f, 0.75), 7, EPS, "the sub-function that loaded") + expect_near(t, eval1(f, 0.25), 0, EPS, "the one that did not reads as zero") +} + +// --------------------------------------------------------------------------- +// Type 4: the PostScript calculator +// --------------------------------------------------------------------------- + +@(test) +test_function_type4_operators :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + Case :: struct { + program: string, + want: f64, + } + // Every case is evaluated at x = 0, and the input stays on the stack below + // the result, so the value asserted is the top of the stack. + cases := []Case{ + // Arithmetic. + {"{ 2 3 add }", 5}, + {"{ 5 3 sub }", 2}, + {"{ 3 4 mul }", 12}, + {"{ 7 2 div }", 3.5}, + {"{ 7 2 idiv }", 3}, + {"{ -7 2 idiv }", -3}, + {"{ 7 2 mod }", 1}, + {"{ 5 neg }", -5}, + {"{ -5 abs }", 5}, + {"{ 1.2 ceiling }", 2}, + {"{ -1.2 ceiling }", -1}, + {"{ 1.8 floor }", 1}, + {"{ -1.8 floor }", -2}, + {"{ 2.5 round }", 3}, + // PostScript rounds a halfway case towards positive infinity. + {"{ -2.5 round }", -2}, + {"{ 1.9 truncate }", 1}, + {"{ -1.9 truncate }", -1}, + {"{ 9 sqrt }", 3}, + {"{ 90 sin }", 1}, + {"{ 0 sin }", 0}, + {"{ 180 cos }", -1}, + {"{ 0 cos }", 1}, + // atan takes num then den and answers in degrees, never negative. + {"{ 1 0 atan }", 90}, + {"{ 0 -1 atan }", 180}, + {"{ -1 0 atan }", 270}, + {"{ 0 1 atan }", 0}, + {"{ 2 10 exp }", 1024}, + {"{ 9 0.5 exp }", 3}, + {"{ 1 ln }", 0}, + {"{ 100 log }", 2}, + {"{ 3.9 cvi }", 3}, + {"{ -3.9 cvi }", -3}, + {"{ 3 cvr }", 3}, + + // Relational and boolean. Each result is turned back into a number by + // an ifelse so that one assertion covers both. + {"{ 1 1 eq { 10 } { 20 } ifelse }", 10}, + {"{ 1 2 eq { 10 } { 20 } ifelse }", 20}, + {"{ 1 2 ne { 10 } { 20 } ifelse }", 10}, + {"{ 2 1 gt { 10 } { 20 } ifelse }", 10}, + {"{ 1 1 gt { 10 } { 20 } ifelse }", 20}, + {"{ 2 2 ge { 10 } { 20 } ifelse }", 10}, + {"{ 1 2 lt { 10 } { 20 } ifelse }", 10}, + {"{ 2 2 le { 10 } { 20 } ifelse }", 10}, + {"{ true false and { 10 } { 20 } ifelse }", 20}, + {"{ true true and { 10 } { 20 } ifelse }", 10}, + {"{ true false or { 10 } { 20 } ifelse }", 10}, + {"{ true true xor { 10 } { 20 } ifelse }", 20}, + {"{ true false xor { 10 } { 20 } ifelse }", 10}, + {"{ false not { 10 } { 20 } ifelse }", 10}, + {"{ true not { 10 } { 20 } ifelse }", 20}, + // On integers the same four operators are bitwise, not logical. + {"{ 12 10 and }", 8}, + {"{ 12 10 or }", 14}, + {"{ 12 10 xor }", 6}, + {"{ 0 not }", -1}, + {"{ 1 3 bitshift }", 8}, + {"{ 16 -3 bitshift }", 2}, + + // Stack operators. + {"{ 1 2 pop }", 1}, + {"{ 1 2 exch }", 1}, + {"{ 7 dup add }", 14}, + {"{ 5 7 2 copy add }", 12}, + {"{ 1 2 3 2 index }", 1}, + {"{ 1 2 3 3 1 roll }", 2}, + {"{ 1 2 3 3 -1 roll }", 1}, + {"{ 1 2 3 3 0 roll }", 3}, + + // Conditionals. + {"{ true { 10 } if }", 10}, + // Nothing is pushed, so the top of the stack is still the input, 0. + {"{ false { 10 } if }", 0}, + {"{ true { 1 2 add } { 99 } ifelse }", 3}, + {"{ false { 99 } { 1 2 add } ifelse }", 3}, + // Nested conditionals, and a branch that is several operators long. + {"{ true { false { 1 } { 2 } ifelse } { 3 } ifelse }", 2}, + {"{ 3 4 lt { 3 4 add 2 mul } { 0 } ifelse }", 14}, + + // The input itself is on the stack when the program starts. + {"{ 1 add }", 1}, + // A comment runs to the end of the line. + {"{ 2 3 add % this is ignored\n }", 5}, + // A word this calculator has no operator for is stepped over rather + // than abandoning the program. + {"{ 2 3 add nosuchop }", 5}, + } + + for c, i in cases { + f := load_ps(t, doc, c.program) + if f == nil { + testing.expectf(t, false, "case %d (%s) did not load", i, c.program) + continue + } + got := eval1(f, 0) + if !(abs(got - c.want) <= 1e-9) { + testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want) + } + } +} + +// Division by zero, the square root of a negative, the logarithm of zero and +// an empty stack must all produce a defined value rather than a NaN, an +// infinity or a crash. +@(test) +test_function_type4_defined_on_bad_input :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + Case :: struct { + program: string, + want: f64, + } + cases := []Case{ + {"{ 1 0 div }", 0}, + {"{ 1 0 idiv }", 0}, + {"{ 1 0 mod }", 0}, + {"{ -4 sqrt }", 0}, + {"{ 0 ln }", 0}, + {"{ -1 ln }", 0}, + {"{ 0 log }", 0}, + // Every operand gone, then an operator that wants two. + {"{ pop add }", 0}, + {"{ pop pop pop sub }", 0}, + {"{ pop dup }", 0}, + {"{ pop exch }", 0}, + // exp overflowing to infinity must not leave an infinity behind. + {"{ 1000 1000 exp }", 0}, + // index past the bottom of the stack. + {"{ 99 index }", 0}, + // roll and copy with counts that make no sense. + {"{ 1 2 -3 1 roll }", 2}, + {"{ 1 2 99 1 roll }", 2}, + {"{ 5 -1 copy }", 5}, + // A block with no `if` after it never runs. + {"{ 5 { 99 } }", 5}, + // An `if` with no block before it is skipped. + {"{ 5 if }", 5}, + } + for c in cases { + f := load_ps(t, doc, c.program) + if f == nil { + testing.expectf(t, false, "%s did not load", c.program) + continue + } + got := eval1(f, 0) + testing.expectf(t, is_finite(got), "%s produced a non-finite value", c.program) + if !(abs(got - c.want) <= 1e-9) { + testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want) + } + } +} + +// A program that pushes more than the hundred operands the format allows +// (7.10.5) stops cleanly and still produces a value. +@(test) +test_function_type4_stack_overflow :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 200 pushes, in arena memory so that the stream can keep pointing at it. + src := make([]byte, 2 + 2 * 200 + 1, doc.allocator) + src[0], src[1] = '{', ' ' + for i in 0 ..< 200 { + src[2 + 2 * i] = '1' + src[3 + 2 * i] = ' ' + } + src[len(src) - 1] = '}' + + f := load_sampled(t, doc, "<< /FunctionType 4 /Domain [0 1] /Range [0 10] >>", src) + if f == nil { return } + got := eval1(f, 0) + testing.expect(t, is_finite(got), "an overflowing program still produces a value") + expect_near(t, got, 1, EPS, "the last value pushed before the stack filled") +} + +// A program that jumps backwards forever is not something a compiler here can +// produce, but a Function is a plain struct and the interpreter has to survive +// one anyway. The step budget is what ends it. +@(test) +test_function_type4_infinite_loop_terminates :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := new(Function, doc.allocator) + f.kind = .PostScript + f.n_in = 1 + f.n_out = 1 + f.domain = fn_make_pair(doc, 0, 1) + f.range = fn_make_pair(doc, 0, 1) + prog := make([]Ps_Op, 1, doc.allocator) + prog[0] = Ps_Op{kind = .Jump, target = 0} + f.program = prog + + // Reaching the assertion at all is the test: without a step budget this + // never returns. + expect_near(t, eval1(f, 0.25), 0.25, EPS, "the input is still on the stack") +} + +@(test) +test_function_type4_deep_nesting :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Braces nested past the compiler's depth limit are refused rather than + // recursing the native stack away. + n := MAX_PS_DEPTH + 8 + src := make([]byte, 1 + 2 * n, doc.allocator) + src[0] = '{' + for i in 0 ..< n { + src[1 + i] = '{' + } + for i in 0 ..< n { + src[1 + n + i] = '}' + } + _, err := function_load(doc, cf_stream(doc, + "<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", src)) + testing.expect_value(t, err, Error.Bad_Function) +} + +@(test) +test_function_type4_needs_range :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Without /Range there is no way to know how many of the values left on + // the stack are outputs. + _, err := function_load(doc, cf_stream(doc, + "<< /FunctionType 4 /Domain [0 1] >>", cf_bytes("{ 1 }"))) + testing.expect_value(t, err, Error.Bad_Function) + + // An empty program is not a function either. + _, err2 := function_load(doc, cf_stream(doc, + "<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes(""))) + testing.expect_value(t, err2, Error.Bad_Function) +} + +// --------------------------------------------------------------------------- +// Loading: caching, and what a broken function does +// --------------------------------------------------------------------------- + +@(test) +test_function_cached_by_object :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + a, err_a := function_load(doc, Ref{6, 0}) + b, err_b := function_load(doc, Ref{6, 0}) + testing.expect_value(t, err_a, Error.None) + testing.expect_value(t, err_b, Error.None) + testing.expect(t, a != nil && a == b, "the same object must build one function") + + // A different object is a different function. + c, _ := function_load(doc, Ref{8, 0}) + testing.expect(t, c != nil && c != a, "different objects are different functions") +} + +@(test) +test_function_rejects_nonsense :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + _, e1 := function_load(doc, nil) + testing.expect_value(t, e1, Error.Bad_Function) + + _, e2 := function_load(doc, cf_parse(doc, "42")) + testing.expect_value(t, e2, Error.Bad_Function) + + // An unknown /FunctionType. + _, e3 := function_load(doc, cf_parse(doc, "<< /FunctionType 5 /Domain [0 1] >>")) + testing.expect_value(t, e3, Error.Bad_Function) + + // A type 2 that is missing everything still loads, because C0 and C1 have + // defaults and /Domain does not need to be trusted. + f, e4 := function_load(doc, cf_parse(doc, "<< /FunctionType 2 >>")) + testing.expect_value(t, e4, Error.None) + if f != nil { + expect_near(t, eval1(f, 0.5), 0.5, EPS, "defaulted type 2") + } + + // Evaluating nothing writes zeros rather than leaving the caller's buffer + // untouched. + out := [3]f64{9, 9, 9} + in_ := [1]f64{0.5} + function_eval(nil, in_[:], out[:]) + testing.expect_value(t, out[0], 0) + testing.expect_value(t, out[1], 0) + testing.expect_value(t, out[2], 0) +} + +// A caller may ask for fewer outputs than the function produces, and must not +// have anything written past the end of its buffer. +@(test) +test_function_short_output_buffer :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + f := load_fn(t, doc, + "<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [1 1 1 1] /N 1 >>") + if f == nil { return } + testing.expect_value(t, f.n_out, 4) + + guard := [3]f64{0, 0, 77} + in_ := [1]f64{1} + function_eval(f, in_[:], guard[:2]) + expect_near(t, guard[0], 1, EPS, "first output") + expect_near(t, guard[1], 1, EPS, "second output") + testing.expect_value(t, guard[2], 77) +} diff --git a/geom.odin b/geom.odin new file mode 100644 index 0000000..bc95ce5 --- /dev/null +++ b/geom.odin @@ -0,0 +1,282 @@ +package pdf + +import "core:math" + +// Point is a coordinate pair. PDF user space is y-up with the origin at the +// bottom-left of the page; device space here is y-down with the origin at the +// top-left of the bitmap, and the flip lives in the base matrix that +// `render_page` builds, not in the geometry code. +Point :: [2]f64 + +// Matrix is a PDF transformation matrix, stored in the order the format writes +// it: `[a b c d e f]` standing for +// +// | a b 0 | +// | c d 0 | +// | e f 1 | +// +// with row vectors, so a point maps as x' = a*x + c*y + e, y' = b*x + d*y + f. +Matrix :: struct { + a, b, c, d, e, f: f64, +} + +MATRIX_IDENTITY :: Matrix{1, 0, 0, 1, 0, 0} + +// Rect is an axis-aligned rectangle. x0/y0 is not required to be the lower +// corner on construction -- `rect_normalize` sorts them -- because PDF +// rectangle arrays are explicitly allowed to give their corners in either +// order. +Rect :: struct { + x0, y0, x1, y1: f64, +} + +RECT_EMPTY :: Rect{0, 0, 0, 0} +// RECT_INFINITE is the identity for intersection: clipping against it is a +// no-op, which is what an unset clip path needs to be. +RECT_INFINITE :: Rect{-1e30, -1e30, 1e30, 1e30} + +// A4 in points, the fallback page size for a page with no usable /MediaBox. +DEFAULT_PAGE_RECT :: Rect{0, 0, 595.276, 841.89} + +// --------------------------------------------------------------------------- +// Matrices +// --------------------------------------------------------------------------- + +// matrix_mul returns m applied first, then n. This is the order the `cm` +// operator wants: `CTM = cm_operand * CTM`. +matrix_mul :: proc "contextless" (m, n: Matrix) -> Matrix { + return Matrix{ + a = m.a * n.a + m.b * n.c, + b = m.a * n.b + m.b * n.d, + c = m.c * n.a + m.d * n.c, + d = m.c * n.b + m.d * n.d, + e = m.e * n.a + m.f * n.c + n.e, + f = m.e * n.b + m.f * n.d + n.f, + } +} + +translate_matrix :: proc "contextless" (tx, ty: f64) -> Matrix { + return Matrix{1, 0, 0, 1, tx, ty} +} + +scale_matrix :: proc "contextless" (sx, sy: f64) -> Matrix { + return Matrix{sx, 0, 0, sy, 0, 0} +} + +// rotate_matrix takes degrees, matching the /Rotate page entry and the way +// callers think about page rotation. +rotate_matrix :: proc "contextless" (degrees: f64) -> Matrix { + r := degrees * math.PI / 180 + s := math.sin(r) + c := math.cos(r) + return Matrix{c, s, -s, c, 0, 0} +} + +skew_matrix :: proc "contextless" (ax, ay: f64) -> Matrix { + return Matrix{1, math.tan(ay * math.PI / 180), math.tan(ax * math.PI / 180), 1, 0, 0} +} + +matrix_determinant :: proc "contextless" (m: Matrix) -> f64 { + return m.a * m.d - m.b * m.c +} + +// matrix_invert reports ok = false for a singular matrix. Degenerate CTMs are +// legal in content streams (a zero-width scale is how some producers hide +// content) so callers must not assume success. +matrix_invert :: proc "contextless" (m: Matrix) -> (inv: Matrix, ok: bool) { + det := matrix_determinant(m) + if det == 0 || !is_finite(det) { + return MATRIX_IDENTITY, false + } + id := 1 / det + inv.a = m.d * id + inv.b = -m.b * id + inv.c = -m.c * id + inv.d = m.a * id + inv.e = (m.c * m.f - m.d * m.e) * id + inv.f = (m.b * m.e - m.a * m.f) * id + return inv, true +} + +transform_point :: proc "contextless" (m: Matrix, p: Point) -> Point { + return Point{ + m.a * p.x + m.c * p.y + m.e, + m.b * p.x + m.d * p.y + m.f, + } +} + +// transform_vector applies only the linear part, for quantities that have a +// direction and a magnitude but no position -- a dash phase, a line width. +transform_vector :: proc "contextless" (m: Matrix, p: Point) -> Point { + return Point{m.a * p.x + m.c * p.y, m.b * p.x + m.d * p.y} +} + +// transform_rect returns the bounding box of the transformed rectangle, which +// is larger than the transformed shape whenever the matrix rotates or skews. +transform_rect :: proc "contextless" (m: Matrix, r: Rect) -> Rect { + if r == RECT_INFINITE { return r } + p0 := transform_point(m, Point{r.x0, r.y0}) + p1 := transform_point(m, Point{r.x1, r.y0}) + p2 := transform_point(m, Point{r.x0, r.y1}) + p3 := transform_point(m, Point{r.x1, r.y1}) + return Rect{ + x0 = min(p0.x, p1.x, p2.x, p3.x), + y0 = min(p0.y, p1.y, p2.y, p3.y), + x1 = max(p0.x, p1.x, p2.x, p3.x), + y1 = max(p0.y, p1.y, p2.y, p3.y), + } +} + +// matrix_expansion is the square root of |determinant|: the average factor by +// which the matrix scales lengths. Used to turn a user-space line width or +// flatness tolerance into a device-space one. +matrix_expansion :: proc "contextless" (m: Matrix) -> f64 { + return math.sqrt(abs(matrix_determinant(m))) +} + +// matrix_max_expansion is the largest factor by which the matrix can stretch +// any single direction. Flattening tolerance has to use this rather than the +// average, or a curve under an anisotropic scale flattens too coarsely along +// its stretched axis. +matrix_max_expansion :: proc "contextless" (m: Matrix) -> f64 { + return max(math.sqrt(m.a * m.a + m.b * m.b), math.sqrt(m.c * m.c + m.d * m.d)) +} + +matrix_is_identity :: proc "contextless" (m: Matrix) -> bool { + return m == MATRIX_IDENTITY +} + +// matrix_is_rectilinear reports whether the matrix maps axis-aligned +// rectangles to axis-aligned rectangles. Axis-aligned clips can be applied as +// a scissor rect instead of a mask, which is much cheaper and covers most +// real content. +// +// The comparison is against a relative tolerance rather than against zero: +// `rotate_matrix(90)` computes cos(pi/2) as 6.1e-17, not 0, and a page with +// `/Rotate 90` is precisely the case this predicate exists to catch. Testing +// for exact zeros would answer "no" for every rotated page in existence. +matrix_is_rectilinear :: proc "contextless" (m: Matrix) -> bool { + scale := max(abs(m.a), abs(m.b), abs(m.c), abs(m.d)) + if scale == 0 { + // A matrix that collapses everything to a point maps a rectangle to a + // (degenerate) rectangle, so the cheap path is still correct. + return true + } + eps := scale * 1e-9 + return (abs(m.b) <= eps && abs(m.c) <= eps) || (abs(m.a) <= eps && abs(m.d) <= eps) +} + +// --------------------------------------------------------------------------- +// Rectangles +// --------------------------------------------------------------------------- + +rect_normalize :: proc "contextless" (r: Rect) -> Rect { + return Rect{min(r.x0, r.x1), min(r.y0, r.y1), max(r.x0, r.x1), max(r.y0, r.y1)} +} + +rect_width :: proc "contextless" (r: Rect) -> f64 { return r.x1 - r.x0 } +rect_height :: proc "contextless" (r: Rect) -> f64 { return r.y1 - r.y0 } + +rect_is_empty :: proc "contextless" (r: Rect) -> bool { + return r.x1 <= r.x0 || r.y1 <= r.y0 +} + +rect_contains :: proc "contextless" (r: Rect, p: Point) -> bool { + return p.x >= r.x0 && p.x <= r.x1 && p.y >= r.y0 && p.y <= r.y1 +} + +rect_intersect :: proc "contextless" (a, b: Rect) -> Rect { + if a == RECT_INFINITE { return b } + if b == RECT_INFINITE { return a } + r := Rect{max(a.x0, b.x0), max(a.y0, b.y0), min(a.x1, b.x1), min(a.y1, b.y1)} + if rect_is_empty(r) { return RECT_EMPTY } + return r +} + +rect_union :: proc "contextless" (a, b: Rect) -> Rect { + if rect_is_empty(a) { return b } + if rect_is_empty(b) { return a } + return Rect{min(a.x0, b.x0), min(a.y0, b.y0), max(a.x1, b.x1), max(a.y1, b.y1)} +} + +rect_include :: proc "contextless" (r: Rect, p: Point) -> Rect { + if rect_is_empty(r) { return Rect{p.x, p.y, p.x, p.y} } + return Rect{min(r.x0, p.x), min(r.y0, p.y), max(r.x1, p.x), max(r.y1, p.y)} +} + +rect_expand :: proc "contextless" (r: Rect, by: f64) -> Rect { + return Rect{r.x0 - by, r.y0 - by, r.x1 + by, r.y1 + by} +} + +// --------------------------------------------------------------------------- +// Reading geometry out of the file +// --------------------------------------------------------------------------- + +// object_to_rect reads a 4-number array. The corners are normalised, so a +// /MediaBox written as [612 792 0 0] -- which is legal, and does occur -- comes +// back the right way round. +object_to_rect :: proc(doc: ^Document, o: Object) -> (r: Rect, ok: bool) { + a := to_array(o) or_return + if array_len(a) < 4 { return RECT_EMPTY, false } + r = Rect{ + array_num(doc, a, 0), + array_num(doc, a, 1), + array_num(doc, a, 2), + array_num(doc, a, 3), + } + if !rect_is_finite(r) { return RECT_EMPTY, false } + return rect_normalize(r), true +} + +// object_to_matrix reads a 6-number array, as written for /Matrix on form +// XObjects, patterns and Type3 fonts. +object_to_matrix :: proc(doc: ^Document, o: Object) -> (m: Matrix, ok: bool) { + a := to_array(o) or_return + if array_len(a) < 6 { return MATRIX_IDENTITY, false } + m = Matrix{ + array_num(doc, a, 0), + array_num(doc, a, 1), + array_num(doc, a, 2), + array_num(doc, a, 3), + array_num(doc, a, 4), + array_num(doc, a, 5), + } + if !matrix_is_finite(m) { return MATRIX_IDENTITY, false } + return m, true +} + +dict_rect :: proc(doc: ^Document, d: ^Dict, key: string, default := RECT_EMPTY) -> Rect { + r, ok := object_to_rect(doc, dict_get(doc, d, key)) + return ok ? r : default +} + +dict_matrix :: proc(doc: ^Document, d: ^Dict, key: string, default := MATRIX_IDENTITY) -> Matrix { + m, ok := object_to_matrix(doc, dict_get(doc, d, key)) + return ok ? m : default +} + +// --------------------------------------------------------------------------- +// Finite-value guards +// +// A NaN or infinity reaching the rasteriser turns into an unbounded scanline +// loop or a silently blank page, so every number that comes out of a file is +// checked at the boundary rather than trusted. +// --------------------------------------------------------------------------- + +is_finite :: proc "contextless" (x: f64) -> bool { + return x == x && x != math.INF_F64 && x != math.NEG_INF_F64 +} + +rect_is_finite :: proc "contextless" (r: Rect) -> bool { + return is_finite(r.x0) && is_finite(r.y0) && is_finite(r.x1) && is_finite(r.y1) +} + +matrix_is_finite :: proc "contextless" (m: Matrix) -> bool { + return is_finite(m.a) && is_finite(m.b) && is_finite(m.c) && + is_finite(m.d) && is_finite(m.e) && is_finite(m.f) +} + +// clamp_finite replaces a non-finite value with `fallback`. +clamp_finite :: proc "contextless" (x, fallback: f64) -> f64 { + return is_finite(x) ? x : fallback +} diff --git a/geom_test.odin b/geom_test.odin new file mode 100644 index 0000000..43e728f --- /dev/null +++ b/geom_test.odin @@ -0,0 +1,282 @@ +package pdf + +import "core:math" +import "core:mem/virtual" +import "core:testing" + +// --------------------------------------------------------------------------- +// Matrices +// +// matrix_mul's operand order is the single thing every `cm` operator, form +// XObject and pattern matrix depends on. Getting it backwards transposes the +// whole page, so it is pinned here against worked examples rather than against +// the implementation's own arithmetic. +// --------------------------------------------------------------------------- + +@(test) +test_matrix_mul_order :: proc(t: ^testing.T) { + // m applied first, then n. + m := matrix_mul(scale_matrix(2, 3), translate_matrix(5, 7)) + testing.expect_value(t, m, Matrix{2, 0, 0, 3, 5, 7}) + testing.expect_value(t, transform_point(m, Point{1, 1}), Point{7, 10}) + + // The reverse order is a different matrix -- if these were equal the test + // would pass with the operands transposed. + n := matrix_mul(translate_matrix(5, 7), scale_matrix(2, 3)) + testing.expect_value(t, n, Matrix{2, 0, 0, 3, 10, 21}) + testing.expect(t, m != n, "multiplication is not commutative") + + testing.expect_value(t, matrix_mul(MATRIX_IDENTITY, m), m) + testing.expect_value(t, matrix_mul(m, MATRIX_IDENTITY), m) +} + +// The row-vector convention the format uses: x' = a*x + c*y + e. +@(test) +test_transform_point_convention :: proc(t: ^testing.T) { + m := Matrix{2, 3, 5, 7, 11, 13} + testing.expect_value(t, transform_point(m, Point{1, 0}), Point{2 + 11, 3 + 13}) + testing.expect_value(t, transform_point(m, Point{0, 1}), Point{5 + 11, 7 + 13}) + testing.expect_value(t, transform_point(m, Point{0, 0}), Point{11, 13}) + + // A vector has direction and magnitude but no position, so the translation + // must not apply. + testing.expect_value(t, transform_vector(m, Point{1, 0}), Point{2, 3}) + testing.expect_value(t, transform_vector(m, Point{0, 0}), Point{0, 0}) +} + +@(test) +test_matrix_rotate_and_skew :: proc(t: ^testing.T) { + r := rotate_matrix(90) + p := transform_point(r, Point{1, 0}) + testing.expect(t, abs(p.x) < 1e-12, "90 degrees sends x to 0") + testing.expect(t, abs(p.y - 1) < 1e-12, "90 degrees sends x to +y") + + // Four quarter turns is the identity, to floating-point tolerance. + full := matrix_mul(matrix_mul(r, r), matrix_mul(r, r)) + testing.expect(t, abs(full.a - 1) < 1e-9 && abs(full.d - 1) < 1e-9, "four quarter turns") + + s := skew_matrix(45, 0) + testing.expect(t, abs(s.c - 1) < 1e-12, "a 45 degree x-skew has c == 1") +} + +@(test) +test_matrix_invert :: proc(t: ^testing.T) { + m := Matrix{2, 1, 1, 3, 10, 20} + inv, ok := matrix_invert(m) + testing.expect(t, ok, "a non-singular matrix inverts") + + round := matrix_mul(m, inv) + testing.expect(t, abs(round.a - 1) < 1e-12 && abs(round.d - 1) < 1e-12, "m * m^-1 is identity") + testing.expect(t, abs(round.b) < 1e-12 && abs(round.c) < 1e-12, "") + testing.expect(t, abs(round.e) < 1e-12 && abs(round.f) < 1e-12, "translation cancels too") + + // Round-tripping a point is the property callers actually rely on. + p := Point{3, -4} + testing.expect(t, abs(transform_point(inv, transform_point(m, p)).x - p.x) < 1e-12, "") + testing.expect(t, abs(transform_point(inv, transform_point(m, p)).y - p.y) < 1e-12, "") +} + +// A degenerate CTM is legal content -- a zero-width scale is how some producers +// hide artwork -- so callers must be told rather than handed nonsense. +@(test) +test_matrix_invert_singular :: proc(t: ^testing.T) { + _, ok := matrix_invert(scale_matrix(0, 1)) + testing.expect(t, !ok, "a zero scale is singular") + _, ok2 := matrix_invert(Matrix{1, 2, 2, 4, 0, 0}) + testing.expect(t, !ok2, "collinear rows are singular") + _, ok3 := matrix_invert(Matrix{math.nan_f64(), 0, 0, 1, 0, 0}) + testing.expect(t, !ok3, "a NaN determinant is not invertible") + + inv, _ := matrix_invert(scale_matrix(0, 0)) + testing.expect_value(t, inv, MATRIX_IDENTITY) +} + +@(test) +test_matrix_expansion :: proc(t: ^testing.T) { + testing.expect_value(t, matrix_expansion(scale_matrix(2, 8)), 4) + testing.expect_value(t, matrix_expansion(MATRIX_IDENTITY), 1) + // Reflection scales lengths as much as rotation does; the sign is not it. + testing.expect_value(t, matrix_expansion(scale_matrix(-3, 3)), 3) + + // The maximum stretch of any single direction, which is what flattening + // tolerance must use: the average would flatten the stretched axis coarsely. + testing.expect_value(t, matrix_max_expansion(scale_matrix(2, 8)), 8) + testing.expect_value(t, matrix_max_expansion(scale_matrix(8, 2)), 8) +} + +@(test) +test_matrix_predicates :: proc(t: ^testing.T) { + testing.expect(t, matrix_is_identity(MATRIX_IDENTITY), "") + testing.expect(t, !matrix_is_identity(translate_matrix(1, 0)), "") + + testing.expect(t, matrix_is_rectilinear(scale_matrix(2, 3)), "a scale keeps axes") + testing.expect(t, matrix_is_rectilinear(rotate_matrix(90)), "a quarter turn swaps axes") + testing.expect(t, !matrix_is_rectilinear(rotate_matrix(45)), "45 degrees does not") + testing.expect(t, !matrix_is_rectilinear(skew_matrix(10, 0)), "a skew does not") + + testing.expect(t, matrix_is_finite(MATRIX_IDENTITY), "") + testing.expect(t, !matrix_is_finite(Matrix{1, 0, 0, 1, math.nan_f64(), 0}), "") + testing.expect(t, !matrix_is_finite(Matrix{math.INF_F64, 0, 0, 1, 0, 0}), "") +} + +// --------------------------------------------------------------------------- +// Rectangles +// --------------------------------------------------------------------------- + +@(test) +test_transform_rect_bounds_the_shape :: proc(t: ^testing.T) { + r := Rect{0, 0, 10, 10} + testing.expect_value(t, transform_rect(scale_matrix(2, 3), r), Rect{0, 0, 20, 30}) + + // A rotation gives the bounding box of the rotated shape, which is larger + // than the shape itself. + rot := transform_rect(rotate_matrix(45), r) + d := 10 * math.sqrt_f64(2) + testing.expect(t, abs(rect_width(rot) - d) < 1e-9, "a 45-degree box is sqrt(2) wider") + + // Clipping against the infinite rect must stay a no-op after transforming. + testing.expect_value(t, transform_rect(scale_matrix(2, 2), RECT_INFINITE), RECT_INFINITE) +} + +@(test) +test_rect_ops :: proc(t: ^testing.T) { + a := Rect{0, 0, 10, 10} + b := Rect{5, 5, 20, 20} + + testing.expect_value(t, rect_intersect(a, b), Rect{5, 5, 10, 10}) + testing.expect_value(t, rect_union(a, b), Rect{0, 0, 20, 20}) + testing.expect_value(t, rect_intersect(a, Rect{100, 100, 110, 110}), RECT_EMPTY) + + // The infinite rect is the identity for intersection, which is what an + // unset clip path needs to be. + testing.expect_value(t, rect_intersect(a, RECT_INFINITE), a) + testing.expect_value(t, rect_intersect(RECT_INFINITE, a), a) + + // The empty rect is the identity for union. + testing.expect_value(t, rect_union(a, RECT_EMPTY), a) + testing.expect_value(t, rect_union(RECT_EMPTY, a), a) + + testing.expect_value(t, rect_width(a), 10) + testing.expect_value(t, rect_height(a), 10) + testing.expect(t, rect_is_empty(Rect{5, 5, 5, 10}), "zero width is empty") + testing.expect(t, rect_is_empty(Rect{10, 0, 5, 10}), "inverted is empty") + testing.expect(t, !rect_is_empty(a), "") + + testing.expect(t, rect_contains(a, Point{5, 5}), "") + testing.expect(t, rect_contains(a, Point{0, 0}), "the boundary counts") + testing.expect(t, !rect_contains(a, Point{11, 5}), "") + + testing.expect_value(t, rect_expand(a, 2), Rect{-2, -2, 12, 12}) + testing.expect_value(t, rect_include(a, Point{-5, 20}), Rect{-5, 0, 10, 20}) + testing.expect_value(t, rect_include(RECT_EMPTY, Point{3, 4}), Rect{3, 4, 3, 4}) +} + +// A /MediaBox is explicitly allowed to give its corners in either order. +@(test) +test_rect_normalize :: proc(t: ^testing.T) { + testing.expect_value(t, rect_normalize(Rect{612, 792, 0, 0}), Rect{0, 0, 612, 792}) + testing.expect_value(t, rect_normalize(Rect{0, 0, 612, 792}), Rect{0, 0, 612, 792}) +} + +// --------------------------------------------------------------------------- +// Reading geometry out of objects +// --------------------------------------------------------------------------- + +@(test) +test_object_to_rect_and_matrix :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + box := new_array(a) + array_push(box, i64(612)) + array_push(box, i64(792)) + array_push(box, i64(0)) + array_push(box, f64(0)) + r, ok := object_to_rect(nil, box) + testing.expect(t, ok, "a four-number array is a rectangle") + testing.expect_value(t, r, Rect{0, 0, 612, 792}) + + short := new_array(a) + array_push(short, i64(1)) + _, ok2 := object_to_rect(nil, short) + testing.expect(t, !ok2, "three numbers is not a rectangle") + + _, ok3 := object_to_rect(nil, nil) + testing.expect(t, !ok3, "null is not a rectangle") + _, ok4 := object_to_rect(nil, i64(7)) + testing.expect(t, !ok4, "an integer is not a rectangle") + + // A non-finite entry must be refused, not passed on to the rasteriser. + bad := new_array(a) + for _ in 0 ..< 3 { array_push(bad, f64(0)) } + array_push(bad, math.inf_f64(1)) + _, ok5 := object_to_rect(nil, bad) + testing.expect(t, !ok5, "an infinite corner is not a rectangle") + + mtx := new_array(a) + for v in ([]f64{1, 2, 3, 4, 5, 6}) { array_push(mtx, v) } + m, ok6 := object_to_matrix(nil, mtx) + testing.expect(t, ok6, "a six-number array is a matrix") + testing.expect_value(t, m, Matrix{1, 2, 3, 4, 5, 6}) + + _, ok7 := object_to_matrix(nil, box) + testing.expect(t, !ok7, "four numbers is not a matrix") +} + +@(test) +test_dict_rect_and_matrix_defaults :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + d := new_dict(a) + testing.expect_value(t, dict_rect(nil, d, "MediaBox", DEFAULT_PAGE_RECT), DEFAULT_PAGE_RECT) + testing.expect_value(t, dict_matrix(nil, d, "Matrix"), MATRIX_IDENTITY) + + box := new_array(a) + for v in ([]i64{0, 0, 200, 100}) { array_push(box, v) } + dict_set(d, "MediaBox", box) + testing.expect_value(t, dict_rect(nil, d, "MediaBox", DEFAULT_PAGE_RECT), Rect{0, 0, 200, 100}) + + // A nil dictionary must fall back rather than crash. + testing.expect_value(t, dict_rect(nil, nil, "MediaBox", DEFAULT_PAGE_RECT), DEFAULT_PAGE_RECT) +} + +@(test) +test_finite_guards :: proc(t: ^testing.T) { + testing.expect(t, is_finite(0), "") + testing.expect(t, is_finite(-1e300), "") + testing.expect(t, !is_finite(math.nan_f64()), "") + testing.expect(t, !is_finite(math.INF_F64), "") + testing.expect(t, !is_finite(math.NEG_INF_F64), "") + + testing.expect_value(t, clamp_finite(5, 99), 5) + testing.expect_value(t, clamp_finite(math.nan_f64(), 99), 99) + testing.expect_value(t, clamp_finite(math.INF_F64, 99), 99) + + testing.expect(t, rect_is_finite(Rect{0, 0, 1, 1}), "") + testing.expect(t, !rect_is_finite(Rect{0, 0, math.nan_f64(), 1}), "") +} + +// rotate_matrix builds its entries from sin and cos, so a quarter turn carries +// a 6e-17 where an exact zero would be. Every /Rotate 90 page in existence goes +// through this path. +@(test) +test_matrix_is_rectilinear_tolerates_rotation_error :: proc(t: ^testing.T) { + for deg in ([]f64{0, 90, 180, 270, -90, 360}) { + testing.expect(t, matrix_is_rectilinear(rotate_matrix(deg)), + "a quarter turn maps axis-aligned rectangles to axis-aligned rectangles") + } + for deg in ([]f64{1, 30, 45, 89, 100}) { + testing.expect(t, !matrix_is_rectilinear(rotate_matrix(deg)), + "an off-axis rotation does not") + } + // Scale must not change the answer: the tolerance is relative. + tiny := matrix_mul(rotate_matrix(90), scale_matrix(1e-6, 1e-6)) + testing.expect(t, matrix_is_rectilinear(tiny), "a tiny rotated matrix is still rectilinear") + huge := matrix_mul(rotate_matrix(45), scale_matrix(1e9, 1e9)) + testing.expect(t, !matrix_is_rectilinear(huge), "a huge skewed matrix is still not") +} diff --git a/golden_test.odin b/golden_test.odin new file mode 100644 index 0000000..9a6c5aa --- /dev/null +++ b/golden_test.odin @@ -0,0 +1,467 @@ +package pdf + +import "core:os" +import "core:strings" +import "core:testing" +import "core:time" + +// --------------------------------------------------------------------------- +// Golden files +// +// The rest of the suite builds its PDFs in memory with the pb_* helpers, which +// is the right way to test a structure: the builder records the offsets, so a +// test never asserts a hand-counted number. But a builder is part of the same +// codebase as the reader, and three things it therefore cannot show: +// +// 1. that `open_file` works on an actual file, not just on a byte slice; +// 2. that byte offsets some *other* tool wrote are the ones we parse; +// 3. that a refactor did not quietly change what the library accepts by +// changing the builder at the same time. +// +// So testdata/ holds fourteen committed files, one per structural case, and +// this table pins down exactly what each of them must produce -- including the +// exact set of repairs, so that a file which starts opening "more cleanly" +// than it should fails here rather than passing silently. +// +// testdata/README.md says how each file was made. +// --------------------------------------------------------------------------- + +@(private = "file") +GOLDEN_DIR :: #directory + "testdata/" + +@(private = "file") +Golden :: struct { + name: string, + // Flattened page count. + pages: int, + // First page's /MediaBox, after inheritance. + width: f64, + height: f64, + // First page's /Rotate, normalised. + rotate: int, + // What the first page's decoded content stream must begin with. Every + // golden page draws the same filled rectangle and stroked diagonal behind + // a comment naming the file, so this doubles as "the stream decoded". + marker: string, + // The repair kinds this file must record -- exactly, no more and no less, + // once every page's content has been read. + repairs: bit_set[Repair_Kind], + // encryption_method_name, or "" for a file that is not encrypted. + method: string, + // The /Info /Title, which in the encrypted files is a string that only + // decrypts correctly when the file key is right. + title: string, +} + +@(private = "file") +GOLDEN := []Golden{ + // -- structure -------------------------------------------------------- + {"classic-xref.pdf", 1, 200, 200, 0, "% classic-xref", + {}, "", ""}, + {"xref-stream.pdf", 1, 200, 200, 0, "% xref-stream", + {}, "", ""}, + {"object-stream.pdf", 1, 200, 200, 0, "% object-stream", + {}, "", ""}, + // The second generation replaces the page, so 300x300 -- not the 200x200 + // the first generation wrote -- is what proves the update was applied. + {"incremental-update.pdf", 1, 300, 300, 0, "% incremental-update generation 2", + {}, "", ""}, + {"junk-before-header.pdf", 1, 200, 200, 0, "% junk-before-header", + {}, "", ""}, + + // -- damage that must be repaired, not rejected ------------------------ + {"broken-xref.pdf", 1, 200, 200, 0, "% broken-xref", + {.Bad_Startxref, .Rebuilt_Xref}, "", ""}, + {"bad-length.pdf", 1, 200, 200, 0, "% bad-length", + {.Bad_Stream_Length}, "", ""}, + {"no-xref.pdf", 1, 200, 200, 0, "% no-xref", + {.Bad_Startxref, .Rebuilt_Xref, .Missing_Root}, "", ""}, + {"page-tree-broken.pdf", 1, 200, 200, 0, "% page-tree-broken", + {.Cyclic_Page_Tree}, "", ""}, + + // -- encryption, every one with an empty user password ----------------- + {"encrypted-rc4-40.pdf", 1, 200, 200, 0, "% encrypted-rc4-40", + {}, "RC4 40-bit", "Golden File"}, + {"encrypted-rc4-128.pdf", 1, 200, 200, 0, "% encrypted-rc4-128", + {}, "RC4 128-bit", "Golden File"}, + {"encrypted-aes128.pdf", 1, 200, 200, 0, "% encrypted-aes128", + {}, "AES-128", "Golden File"}, + {"encrypted-aes256.pdf", 1, 200, 200, 0, "% encrypted-aes256", + {}, "AES-256", "Golden File"}, + + // -- page tree --------------------------------------------------------- + // /MediaBox and /Rotate come from the root, three levels up. + {"page-tree-deep.pdf", 2, 300, 400, 90, "% page-tree-deep page 1", + {}, "", ""}, +} + +// The password every encrypted golden file restricts itself with. The user +// password is empty, which is what "opens without prompting" means. +@(private = "file") +GOLDEN_OWNER_PW :: "owner" + +@(private = "file") +golden_path :: proc(name: string) -> string { + return strings.concatenate({GOLDEN_DIR, name}, context.temp_allocator) +} + +// golden_repairs collects the kinds recorded so far, ignoring how many times +// each occurred: a repair that fires twice is the same fact as one that fires +// once, and the count is an implementation detail this table should not pin. +@(private = "file") +golden_repairs :: proc(doc: ^Document) -> (out: bit_set[Repair_Kind]) { + if doc == nil { return } + for r in doc.repairs { + out += {r.kind} + } + return +} + +// golden_content decodes one page's content stream. +@(private = "file") +golden_content :: proc(doc: ^Document, page: Page) -> (string, Error) { + s := dict_stream(doc, page.dict, "Contents") + data, err := stream_data(doc, s) + return string(data), err +} + +// --------------------------------------------------------------------------- +// The table +// --------------------------------------------------------------------------- + +@(test) +test_golden_files :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g in GOLDEN { + path := golden_path(g.name) + + doc, err := open_file(path) + defer close(doc) + if !testing.expectf(t, err == .None, "%s: open_file failed: %s", g.name, error_string(err)) { + continue + } + if !testing.expectf(t, doc != nil, "%s: no document", g.name) { + continue + } + + testing.expectf(t, page_count(doc) == g.pages, + "%s: %d pages, wanted %d", g.name, page_count(doc), g.pages) + + // Every page's content must decode, not just the first: a file whose + // second page is unreadable is not a file this suite should pass. + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if !testing.expectf(t, perr == .None, "%s: page %d: %s", g.name, i, error_string(perr)) { + continue + } + body, cerr := golden_content(doc, page) + testing.expectf(t, cerr == .None, + "%s: page %d content: %s", g.name, i, error_string(cerr)) + testing.expectf(t, len(body) > 0, "%s: page %d content is empty", g.name, i) + testing.expectf(t, strings.contains(body, "160 100 re"), + "%s: page %d content does not draw the rectangle", g.name, i) + + if i != 0 { continue } + testing.expectf(t, rect_width(page.media_box) == g.width && + rect_height(page.media_box) == g.height, + "%s: page 0 is %vx%v, wanted %vx%v", g.name, + rect_width(page.media_box), rect_height(page.media_box), g.width, g.height) + testing.expectf(t, page.rotate == g.rotate, + "%s: page 0 /Rotate %d, wanted %d", g.name, page.rotate, g.rotate) + testing.expectf(t, strings.has_prefix(body, g.marker), + "%s: page 0 content starts %q, wanted %q", + g.name, body[:min(len(body), len(g.marker))], g.marker) + } + + // Checked after the content streams have been read, because a wrong + // /Length is only discovered when the stream itself is parsed. + got := golden_repairs(doc) + testing.expectf(t, got == g.repairs, + "%s: repairs %v, wanted %v", g.name, got, g.repairs) + testing.expectf(t, doc.is_damaged == (g.repairs != {}), + "%s: is_damaged %v with repairs %v", g.name, doc.is_damaged, got) + + method := document_is_encrypted(doc) ? encryption_method_name(doc) : "" + testing.expectf(t, method == g.method, + "%s: encryption %q, wanted %q", g.name, method, g.method) + + info := dict_dict(doc, doc.trailer, "Info") + title := string(dict_bytes(doc, info, "Title")) + testing.expectf(t, title == g.title, + "%s: /Info /Title %q, wanted %q", g.name, title, g.title) + } +} + +// open_bytes and open_file must agree. They share everything below the read, +// but only this says so. +@(test) +test_golden_open_bytes_agrees :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g in GOLDEN { + data, read_err := os.read_entire_file(golden_path(g.name), context.allocator) + if !testing.expectf(t, read_err == nil, "%s: cannot read: %v", g.name, read_err) { continue } + defer delete(data) + + doc, err := open_bytes(data) + defer close(doc) + if !testing.expectf(t, err == .None, "%s: open_bytes failed: %s", g.name, error_string(err)) { + continue + } + testing.expectf(t, page_count(doc) == g.pages, + "%s: open_bytes gave %d pages, wanted %d", g.name, page_count(doc), g.pages) + + page, perr := page_get(doc, 0) + testing.expectf(t, perr == .None, "%s: page 0: %s", g.name, error_string(perr)) + testing.expectf(t, rect_width(page.media_box) == g.width, + "%s: open_bytes gave width %v, wanted %v", g.name, rect_width(page.media_box), g.width) + } +} + +// --------------------------------------------------------------------------- +// Cases the table cannot express in one line +// --------------------------------------------------------------------------- + +// page-tree-deep.pdf has its /MediaBox and /Rotate on the root, three /Pages +// levels above the leaves, and its second page overrides /Rotate alone. That +// the override does not also drop the inherited box is the whole point of the +// file. +@(test) +test_golden_page_tree_inheritance :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + doc, err := open_file(golden_path("page-tree-deep.pdf")) + defer close(doc) + if !testing.expect(t, err == .None, "open failed") { return } + + p0, e0 := page_get(doc, 0) + testing.expect(t, e0 == .None, "page 0 missing") + testing.expectf(t, p0.rotate == 90, "page 0 /Rotate %d, wanted the inherited 90", p0.rotate) + + p1, e1 := page_get(doc, 1) + if !testing.expect(t, e1 == .None, "page 1 missing") { return } + testing.expectf(t, p1.rotate == 180, "page 1 /Rotate %d, wanted its own 180", p1.rotate) + testing.expectf(t, rect_width(p1.media_box) == 300 && rect_height(p1.media_box) == 400, + "page 1 is %vx%v; overriding /Rotate must not drop the inherited /MediaBox", + rect_width(p1.media_box), rect_height(p1.media_box)) + + // Inherited two levels down from the node that declares it. + testing.expect(t, p0.resources != nil, "page 0 did not inherit /Resources") + testing.expect(t, p1.resources != nil, "page 1 did not inherit /Resources") +} + +// Each encrypted file also has an owner password, so the owner branch of the +// key derivation is exercised by a real file and not only by a unit vector. +@(test) +test_golden_owner_password :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g in GOLDEN { + if g.method == "" { continue } + + doc, err := open_file(golden_path(g.name), Open_Options{password = GOLDEN_OWNER_PW}) + defer close(doc) + if !testing.expectf(t, err == .None, "%s: owner password rejected: %s", + g.name, error_string(err)) { + continue + } + testing.expectf(t, doc.encrypt != nil && doc.encrypt.authenticated_as == .Owner, + "%s: authenticated as %v, wanted Owner", g.name, + doc.encrypt != nil ? doc.encrypt.authenticated_as : Password_Kind.None) + + // The owner is not subject to the restrictions they set. + all := Permissions{.Print, .Modify, .Copy, .Annotate, .Fill_Forms, .Extract, .Assemble, .Print_High_Res} + testing.expectf(t, encryption_permissions(doc) == all, + "%s: owner permissions %v, wanted everything", g.name, encryption_permissions(doc)) + + page, perr := page_get(doc, 0) + testing.expectf(t, perr == .None, "%s: page 0: %s", g.name, error_string(perr)) + body, cerr := golden_content(doc, page) + testing.expectf(t, cerr == .None && strings.has_prefix(body, g.marker), + "%s: content did not decrypt under the owner password", g.name) + } +} + +// The files restrict what a user may do, so /P must survive the round trip. +// -44 clears the modify and annotate bits and leaves the rest set; revision 2 +// only ever defined four of them, which is why it reports two. +@(test) +test_golden_user_permissions :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + Case :: struct { + name: string, + want: Permissions, + } + r3_up := Permissions{.Print, .Copy, .Fill_Forms, .Extract, .Assemble, .Print_High_Res} + + cases := []Case{ + {"encrypted-rc4-40.pdf", {.Print, .Copy}}, + {"encrypted-rc4-128.pdf", r3_up}, + {"encrypted-aes128.pdf", r3_up}, + {"encrypted-aes256.pdf", r3_up}, + } + + for c in cases { + doc, err := open_file(golden_path(c.name)) + defer close(doc) + if !testing.expectf(t, err == .None, "%s: %s", c.name, error_string(err)) { continue } + + testing.expectf(t, encryption_permissions(doc) == c.want, + "%s: permissions %v, wanted %v", c.name, encryption_permissions(doc), c.want) + testing.expectf(t, doc.encrypt != nil && doc.encrypt.authenticated_as == .User, + "%s: the empty password must authenticate as the user", c.name) + } +} + +// The table and the directory must not drift apart. Without this, a golden file +// added to testdata/ and forgotten in GOLDEN would be tested by nothing at all, +// and a row whose file was never committed would be a silent skip. +@(test) +test_golden_directory_matches_table :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + entries, derr := os.read_directory_by_path(GOLDEN_DIR, -1, context.allocator) + if !testing.expectf(t, derr == nil, "cannot list %s: %v", GOLDEN_DIR, derr) { return } + defer os.file_info_slice_delete(entries, context.allocator) + + for e in entries { + if e.type != .Regular || !strings.has_suffix(e.name, ".pdf") { continue } + found := false + for g in GOLDEN { + if g.name == e.name { found = true; break } + } + testing.expectf(t, found, "testdata/%s is not in the GOLDEN table", e.name) + } + + for g in GOLDEN { + present := false + for e in entries { + if e.name == g.name { present = true; break } + } + testing.expectf(t, present, "GOLDEN lists %s but testdata/ does not have it", g.name) + } +} + +// A password that is neither the user's nor the owner's is refused rather than +// silently producing garbage. +@(test) +test_golden_wrong_password :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g in GOLDEN { + if g.method == "" { continue } + doc, err := open_file(golden_path(g.name), Open_Options{password = "not the password"}) + defer close(doc) + testing.expectf(t, err == .Wrong_Password, + "%s: opened with a wrong password: %s", g.name, error_string(err)) + testing.expectf(t, doc == nil, "%s: a failed open must not return a document", g.name) + } +} + +// --------------------------------------------------------------------------- +// Fuzz-shaped: truncations and bit flips +// +// Not a fuzzer -- there is no feedback and no corpus growth -- but the same +// shape, and enough to catch the class of bug that matters here: an index +// derived from a file byte that is no longer in range once that byte changes. +// The mutations are driven by a fixed seed so that a failure is reproducible; +// a clock-seeded run that fails once and never again is worse than no test. +// --------------------------------------------------------------------------- + +@(private = "file") +GOLDEN_FUZZ_TRUNCATIONS :: 40 +@(private = "file") +GOLDEN_FUZZ_FLIPS :: 200 +@(private = "file") +GOLDEN_FUZZ_SEED :: u64(0x9E3779B97F4A7C15) + +// A single mutated file that takes this long has found a loop bound that is +// too generous. These are all under 1.5 KB; nothing legitimate comes close. +@(private = "file") +GOLDEN_FUZZ_SLOW_MS :: 5000 + +@(private = "file") +golden_rand :: proc(state: ^u64) -> u64 { + state^ += 0x9E37_79B9_7F4A_7C15 + z := state^ + z = (z ~ (z >> 30)) * 0xBF58_476D_1CE4_E5B9 + z = (z ~ (z >> 27)) * 0x94D0_49BB_1331_11EB + return z ~ (z >> 31) +} + +// golden_probe opens one mutated file and drives it as far as a caller would. +// The assertion is only that this returns at all, with either a Document or an +// Error and never both: what a damaged file *means* is not defined, but +// crashing, hanging and handing back a half-built Document are all excluded. +@(private = "file") +golden_probe :: proc(t: ^testing.T, data: []byte, what: string) { + start := time.now() + + doc, err := open_bytes(data) + defer close(doc) + + if err == .None { + testing.expectf(t, doc != nil, "%s: no error but no document", what) + if doc != nil { + for i in 0 ..< page_count(doc) { + page, perr := page_get(doc, i) + if perr != .None { continue } + // Reading the stream is where a bad /Length or a corrupt Flate + // payload would be dereferenced, so it belongs inside the probe. + s := dict_stream(doc, page.dict, "Contents") + _, _ = stream_data(doc, s) + } + } + } else { + testing.expectf(t, doc == nil, "%s: error %s but a document too", what, error_string(err)) + } + + ms := time.duration_milliseconds(time.since(start)) + testing.expectf(t, ms < GOLDEN_FUZZ_SLOW_MS, "%s: took %v ms", what, ms) +} + +@(test) +test_golden_fuzz_truncated :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g in GOLDEN { + data, read_err := os.read_entire_file(golden_path(g.name), context.allocator) + if !testing.expectf(t, read_err == nil, "%s: cannot read: %v", g.name, read_err) { continue } + defer delete(data) + + // Evenly spaced, and never the whole file: 0 bytes and every prefix in + // between, which is what a download that stopped early looks like. + for i in 0 ..< GOLDEN_FUZZ_TRUNCATIONS { + n := len(data) * i / GOLDEN_FUZZ_TRUNCATIONS + golden_probe(t, data[:n], g.name) + } + } +} + +@(test) +test_golden_fuzz_bit_flips :: proc(t: ^testing.T) { + defer free_all(context.temp_allocator) + + for g, index in GOLDEN { + data, read_err := os.read_entire_file(golden_path(g.name), context.allocator) + if !testing.expectf(t, read_err == nil, "%s: cannot read: %v", g.name, read_err) { continue } + defer delete(data) + if len(data) == 0 { continue } + + scratch := make([]byte, len(data)) + defer delete(scratch) + + // Seeded from the file's position in the table, so every file gets a + // different sequence and every run gets the same one. + state := GOLDEN_FUZZ_SEED + u64(index) * 0x0100_0193 + for _ in 0 ..< GOLDEN_FUZZ_FLIPS { + copy(scratch, data) + off := int(golden_rand(&state) % u64(len(scratch))) + bit := u8(golden_rand(&state) & 7) + scratch[off] ~= 1 << bit + golden_probe(t, scratch, g.name) + } + } +} diff --git a/gstate.odin b/gstate.odin new file mode 100644 index 0000000..2b19978 --- /dev/null +++ b/gstate.odin @@ -0,0 +1,296 @@ +package pdf + +// The most colorants a colour value can have. /DeviceN is capped at 32 +// colorants by the format (ISO 32000-2, 8.6.6.5), which is the widest case. +MAX_COLOR_COMPONENTS :: 32 + +// --------------------------------------------------------------------------- +// Stroking +// --------------------------------------------------------------------------- + +Line_Cap :: enum u8 { + Butt = 0, + Round = 1, + Square = 2, +} + +Line_Join :: enum u8 { + Miter = 0, + Round = 1, + Bevel = 2, +} + +// MAX_DASH_ENTRIES bounds a /D array from the file. The format sets no limit; +// eight is the most any real design uses and the array is copied by value into +// every saved graphics state. +MAX_DASH_ENTRIES :: 16 + +Stroke_State :: struct { + line_width: f64, + line_cap: Line_Cap, + line_join: Line_Join, + miter_limit: f64, + // Dash pattern in user space, with `dash_count` valid entries. An entry of + // zero is legal and means a dot; an all-zero array means "no dashing", + // because a pattern that never advances would loop forever. + dash: [MAX_DASH_ENTRIES]f64, + dash_count: int, + dash_phase: f64, +} + +STROKE_STATE_DEFAULT :: Stroke_State{ + line_width = 1, + line_cap = .Butt, + line_join = .Miter, + miter_limit = 10, +} + +// stroke_has_dash reports whether the pattern actually breaks the line. An +// array of all zeros, or one whose entries sum to zero, must be treated as +// solid or the dasher never advances (ISO 32000-2, 8.4.3.6). +stroke_has_dash :: proc(s: ^Stroke_State) -> bool { + if s == nil || s.dash_count <= 0 { return false } + total := 0.0 + for i in 0 ..< min(s.dash_count, MAX_DASH_ENTRIES) { + if s.dash[i] < 0 { return false } + total += s.dash[i] + } + return total > 0 +} + +// --------------------------------------------------------------------------- +// Transparency +// --------------------------------------------------------------------------- + +// Blend_Mode is the set from ISO 32000-2, 11.3.5. `Compatible` is a legacy +// spelling of Normal and behaves identically. +Blend_Mode :: enum u8 { + Normal = 0, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + Color_Dodge, + Color_Burn, + Hard_Light, + Soft_Light, + Difference, + Exclusion, + // The four non-separable modes operate on all three RGB components at once. + Hue, + Saturation, + Color, + Luminosity, +} + +// blend_mode_from_name maps a /BM name. An unrecognised name is Normal, which +// the format requires (11.3.5.2) rather than treating as an error. +blend_mode_from_name :: proc(name: string) -> Blend_Mode { + switch name { + case "Normal", "Compatible": return .Normal + case "Multiply": return .Multiply + case "Screen": return .Screen + case "Overlay": return .Overlay + case "Darken": return .Darken + case "Lighten": return .Lighten + case "ColorDodge": return .Color_Dodge + case "ColorBurn": return .Color_Burn + case "HardLight": return .Hard_Light + case "SoftLight": return .Soft_Light + case "Difference": return .Difference + case "Exclusion": return .Exclusion + case "Hue": return .Hue + case "Saturation": return .Saturation + case "Color": return .Color + case "Luminosity": return .Luminosity + } + return .Normal +} + +blend_mode_is_separable :: proc(m: Blend_Mode) -> bool { + return m < .Hue +} + +// --------------------------------------------------------------------------- +// Colour +// --------------------------------------------------------------------------- + +// Color is a colour value in some colour space: `n` components, each in the +// range that space defines (which is not always 0..1 -- Lab and Indexed both +// use other ranges). +Color :: struct { + n: int, + v: [MAX_COLOR_COMPONENTS]f64, +} + +color_make :: proc(values: ..f64) -> (c: Color) { + c.n = min(len(values), MAX_COLOR_COMPONENTS) + for i in 0 ..< c.n { c.v[i] = values[i] } + return +} + +// Paint_Source is one half of the graphics state's colour: what a fill (or a +// stroke) paints with. A pattern takes precedence over the colour components, +// which for an uncoloured tiling pattern still carry the underlying colour. +Paint_Source :: struct { + cs: ^Colorspace, + color: Color, + pattern: ^Pattern, +} + +// Paint is the fully-resolved instruction handed to a Device for one drawing +// operation: everything it needs and nothing it has to look up. +Paint :: struct { + cs: ^Colorspace, + color: Color, + // The colour converted to RGB once, so a device that only wants RGB need + // not carry a colour-management path. + rgb: [3]f64, + pattern: ^Pattern, + alpha: f64, + blend: Blend_Mode, + soft_mask: ^Soft_Mask, + // Set when the operation paints a shape rather than a colour: image masks + // and text clipping. Devices that ignore colour can skip the conversion. + is_mask: bool, +} + +// --------------------------------------------------------------------------- +// Text state (ISO 32000-2, 9.3) +// --------------------------------------------------------------------------- + +Text_Render_Mode :: enum u8 { + Fill = 0, + Stroke = 1, + Fill_Stroke = 2, + Invisible = 3, + Fill_Clip = 4, + Stroke_Clip = 5, + Fill_Stroke_Clip = 6, + Clip = 7, +} + +text_render_mode_fills :: proc(m: Text_Render_Mode) -> bool { + return m == .Fill || m == .Fill_Stroke || m == .Fill_Clip || m == .Fill_Stroke_Clip +} + +text_render_mode_strokes :: proc(m: Text_Render_Mode) -> bool { + return m == .Stroke || m == .Fill_Stroke || m == .Stroke_Clip || m == .Fill_Stroke_Clip +} + +text_render_mode_clips :: proc(m: Text_Render_Mode) -> bool { + return m >= .Fill_Clip +} + +Text_State :: struct { + font: ^Font, + size: f64, + char_spacing: f64, // Tc, unscaled text-space units + word_spacing: f64, // Tw, applied only to single-byte code 32 + // Th: the horizontal scaling operator gives a percentage; this holds the + // fraction, so the default is 1, not 100. + horiz_scale: f64, + leading: f64, // TL + rise: f64, // Ts + render_mode: Text_Render_Mode, + knockout: bool, +} + +TEXT_STATE_DEFAULT :: Text_State{ + size = 0, + horiz_scale = 1, + knockout = true, +} + +// --------------------------------------------------------------------------- +// Graphics state (ISO 32000-2, 8.4) +// --------------------------------------------------------------------------- + +Graphics_State :: struct { + ctm: Matrix, + + fill: Paint_Source, + stroke: Paint_Source, + fill_alpha: f64, + stroke_alpha: f64, + + stroke_state: Stroke_State, + + blend: Blend_Mode, + soft_mask: ^Soft_Mask, + + text: Text_State, + + // Depth of the device clip stack at the time this state was saved, so `Q` + // can pop exactly the clips that `q` was holding. + clip_depth: int, + + // Parsed but not honoured by the software renderer; kept so a display-list + // consumer and any future colour-managed path can see them. + render_intent: string, + flatness: f64, + smoothness: f64, + stroke_adjust: bool, + alpha_is_shape: bool, + black_point_compensation: bool, + overprint_fill: bool, + overprint_stroke: bool, + overprint_mode: int, +} + +// gstate_default is the initial state at the start of a page or form, before +// any operator runs (8.4.1). Colour spaces are filled in by the interpreter, +// which needs the Document to resolve DeviceGray. +gstate_default :: proc(ctm: Matrix) -> Graphics_State { + return Graphics_State{ + ctm = ctm, + fill_alpha = 1, + stroke_alpha = 1, + stroke_state = STROKE_STATE_DEFAULT, + blend = .Normal, + text = TEXT_STATE_DEFAULT, + render_intent = "RelativeColorimetric", + flatness = 1, + smoothness = 0, + } +} + +// Gstate_Stack is the `q`/`Q` stack. It is depth-capped because a content +// stream can push without ever popping, and each entry is a fat value. +MAX_GSTATE_DEPTH :: 64 + +Gstate_Stack :: struct { + items: [dynamic]Graphics_State, +} + +gstate_stack_make :: proc(allocator := context.allocator) -> Gstate_Stack { + return Gstate_Stack{items = make([dynamic]Graphics_State, 0, 16, allocator)} +} + +gstate_stack_destroy :: proc(s: ^Gstate_Stack) { + if s == nil { return } + delete(s.items) + s^ = {} +} + +// gstate_push saves a copy of `gs`. Returns false when the stack is full, in +// which case the caller must not pop for this `q` -- otherwise the stack +// underflows on the matching `Q` and the rest of the page draws in the wrong +// state. +gstate_push :: proc(s: ^Gstate_Stack, gs: Graphics_State) -> bool { + if len(s.items) >= MAX_GSTATE_DEPTH { return false } + append(&s.items, gs) + return true +} + +gstate_pop :: proc(s: ^Gstate_Stack) -> (gs: Graphics_State, ok: bool) { + if len(s.items) == 0 { return {}, false } + gs = s.items[len(s.items) - 1] + pop(&s.items) + return gs, true +} + +gstate_depth :: proc(s: ^Gstate_Stack) -> int { + return len(s.items) +} diff --git a/gstate_test.odin b/gstate_test.odin new file mode 100644 index 0000000..4076b2a --- /dev/null +++ b/gstate_test.odin @@ -0,0 +1,108 @@ +package pdf + +import "core:testing" + +@(test) +test_blend_mode_names :: proc(t: ^testing.T) { + testing.expect_value(t, blend_mode_from_name("Multiply"), Blend_Mode.Multiply) + testing.expect_value(t, blend_mode_from_name("Compatible"), Blend_Mode.Normal) + testing.expect_value(t, blend_mode_from_name("Luminosity"), Blend_Mode.Luminosity) + // An unrecognised /BM is Normal, not an error (ISO 32000-2, 11.3.5.2). + testing.expect_value(t, blend_mode_from_name("NotAMode"), Blend_Mode.Normal) + testing.expect_value(t, blend_mode_from_name(""), Blend_Mode.Normal) + + testing.expect(t, blend_mode_is_separable(.Difference), "Difference is separable") + testing.expect(t, !blend_mode_is_separable(.Hue), "Hue is not separable") + testing.expect(t, !blend_mode_is_separable(.Luminosity), "Luminosity is not separable") +} + +// An all-zero dash array must read as "solid": a dasher fed a pattern that +// never advances loops forever. +@(test) +test_stroke_dash_guard :: proc(t: ^testing.T) { + s := STROKE_STATE_DEFAULT + testing.expect(t, !stroke_has_dash(&s), "no dash by default") + + s.dash_count = 2 + s.dash[0] = 0 + s.dash[1] = 0 + testing.expect(t, !stroke_has_dash(&s), "all-zero dash is solid") + + s.dash[1] = 3 + testing.expect(t, stroke_has_dash(&s), "a non-zero entry dashes") + + s.dash[0] = -1 + testing.expect(t, !stroke_has_dash(&s), "a negative entry is invalid, treat as solid") +} + +@(test) +test_text_render_mode_predicates :: proc(t: ^testing.T) { + testing.expect(t, text_render_mode_fills(.Fill), "") + testing.expect(t, text_render_mode_fills(.Fill_Stroke_Clip), "") + testing.expect(t, !text_render_mode_fills(.Stroke), "") + testing.expect(t, !text_render_mode_fills(.Invisible), "") + + testing.expect(t, text_render_mode_strokes(.Stroke_Clip), "") + testing.expect(t, !text_render_mode_strokes(.Fill_Clip), "") + + testing.expect(t, text_render_mode_clips(.Clip), "") + testing.expect(t, text_render_mode_clips(.Fill_Clip), "") + testing.expect(t, !text_render_mode_clips(.Invisible), "") +} + +@(test) +test_gstate_defaults :: proc(t: ^testing.T) { + gs := gstate_default(MATRIX_IDENTITY) + testing.expect_value(t, gs.fill_alpha, 1) + testing.expect_value(t, gs.stroke_alpha, 1) + testing.expect_value(t, gs.stroke_state.line_width, 1) + testing.expect_value(t, gs.stroke_state.miter_limit, 10) + testing.expect_value(t, gs.blend, Blend_Mode.Normal) + // Tz is a percentage in the file but a fraction here, so the default is 1. + testing.expect_value(t, gs.text.horiz_scale, 1) +} + +@(test) +test_gstate_stack :: proc(t: ^testing.T) { + s := gstate_stack_make() + defer gstate_stack_destroy(&s) + + _, ok := gstate_pop(&s) + testing.expect(t, !ok, "popping an empty stack must fail, not underflow") + + gs := gstate_default(MATRIX_IDENTITY) + gs.fill_alpha = 0.5 + testing.expect(t, gstate_push(&s, gs), "push should succeed") + testing.expect_value(t, gstate_depth(&s), 1) + + got, ok2 := gstate_pop(&s) + testing.expect(t, ok2, "pop should succeed") + testing.expect_value(t, got.fill_alpha, 0.5) + testing.expect_value(t, gstate_depth(&s), 0) +} + +// A content stream that only ever pushes must hit the cap and be told so, or +// the matching pops later underflow and the rest of the page draws wrong. +@(test) +test_gstate_stack_depth_cap :: proc(t: ^testing.T) { + s := gstate_stack_make() + defer gstate_stack_destroy(&s) + + gs := gstate_default(MATRIX_IDENTITY) + for _ in 0 ..< MAX_GSTATE_DEPTH { + testing.expect(t, gstate_push(&s, gs), "should accept up to the cap") + } + testing.expect(t, !gstate_push(&s, gs), "must refuse past the cap") + testing.expect_value(t, gstate_depth(&s), MAX_GSTATE_DEPTH) +} + +@(test) +test_color_make_clamps_count :: proc(t: ^testing.T) { + c := color_make(0.1, 0.2, 0.3) + testing.expect_value(t, c.n, 3) + testing.expect_value(t, c.v[1], 0.2) + + big: [MAX_COLOR_COMPONENTS + 8]f64 + huge := color_make(..big[:]) + testing.expect_value(t, huge.n, MAX_COLOR_COMPONENTS) +} diff --git a/image.odin b/image.odin new file mode 100644 index 0000000..864d86d --- /dev/null +++ b/image.odin @@ -0,0 +1,1301 @@ +package pdf + +import "core:mem" + +// Image XObjects and inline images (ISO 32000-2, 8.9). +// +// An Image is the image dictionary read once -- dimensions, bit depth, colour +// space, /Decode and the three masking mechanisms -- with its samples left +// undecoded. `image_decode` turns it into pixels the first time somebody +// actually paints it, and caches the result. +// +// WHAT COMES OUT +// +// * An ordinary image decodes to STRAIGHT-alpha RGBA8 (`Bitmap.n == 4`). +// Straight, not premultiplied, because that is what `blend_span_rgba8` +// documents itself as taking and what a caller uploading a texture wants. +// * An /ImageMask decodes to a single-channel coverage plane +// (`Bitmap.n == 1`), 255 where the stencil paints and 0 where it does not. +// There is no colour in it: the device paints it in the fill colour. +// +// THE PIPELINE, and where each step's classic bug lives +// +// 1. `stream_image_data` walks the byte filters and stops at the image +// filter, so a JPEG is decoded straight to pixels here instead of through +// an intermediate byte buffer. +// 2. Samples are unpacked MSB-first at 1, 2, 4, 8 or 16 bits per component, +// with each ROW padded out to a whole byte. Forgetting the row padding at +// a width that is not a multiple of 8/bpc shears the image diagonally. +// 3. /Decode maps each raw sample onto the colour space's component range. +// Indexed and Lab have defaults that are not 0..1, which is the usual +// source of an image that comes out inverted or wildly coloured; +// `colorspace_default_decode` knows them. +// 4. Alpha comes from /SMask, from a stencil /Mask, or from a colour-key +// /Mask array. A colour-key range is in SOURCE SAMPLE values, so it is +// compared before /Decode is applied, not after. +// +// EVERY FAILURE DEGRADES. A stream that will not decode, a JPEG 2000 or JBIG2 +// codestream this library does not implement, an /SMask that is itself broken: +// each of those yields a fully transparent image of the right size and a +// Repair on the Document. `image_decode` returns an Error only when the caller +// passed something nil. + +// --------------------------------------------------------------------------- +// Limits +// +// Every one of these bounds a quantity that came out of the file. +// --------------------------------------------------------------------------- + +// The widest or tallest image that will be decoded. A 600 dpi scan of a sheet +// of A0 is about 20000 pixels on its long side; 65536 is far beyond any of +// them and keeps `width * height` well inside an int on every target. +@(private) +MAX_IMAGE_DIM :: 1 << 16 +// Total pixels in one decoded image: 64 megapixels, which is 256 MiB at RGBA8. +// Deliberately tighter than MAX_BITMAP_PIXELS, which bounds the page raster -- +// a page can legitimately be huge, whereas an image that claims to be is a +// fuzzer or a corrupt /Width. +@(private) +MAX_IMAGE_PIXELS :: 1 << 26 +// Depth of /SMask and /Mask chains followed. A mask never legitimately has a +// mask of its own; this only stops a file whose /SMask points back at the +// image that owns it from recursing. +@(private) +MAX_IMAGE_NESTING :: 4 + +// How much bigger an image's samples may be than the stream bytes they come +// out of, and the size below which the ratio is not looked at at all. +// +// Nothing in ISO 32000-2 relates /Width and /Height to the sample data, so a +// hundred-byte dictionary can ask for a quarter of a gigabyte of pixels and a +// full colour conversion over every one of them; the rule this library is +// written to is that a length out of a file is checked against the file before +// it is allocated for. This is that check, and it is deliberately loose, +// because streams really do decompress enormously: a blank page under CCITT G4 +// codes a whole row in a handful of bits, and a flat image under Flate with a +// predictor is a run of zeroes, so both reach a thousandfold and neither is a +// broken file. What is left over is the shape that has no honest reading -- a +// few hundred bytes claiming tens of megabytes of samples. +// +// The floor keeps every ordinary image out of the test entirely: under a +// mebibyte of samples the decoded bitmap is at most 4 MiB whatever the stream +// says, which is not worth refusing a picture over. +@(private) +MAX_IMAGE_EXPANSION :: 4096 +@(private) +IMAGE_EXPANSION_FLOOR :: 1 << 20 + +// IMAGE_CACHE_BUDGET is the total size of decoded pixels one Document holds. +// +// 64 MiB is a handful of full-page photographs, which is what a reader +// scrolling a brochure re-paints; a 300-page catalogue that would otherwise +// end up holding every photograph at once instead evicts the pages it has +// scrolled past. Eviction is least-recently-used and never touches the image +// currently being decoded. +@(private) +IMAGE_CACHE_BUDGET :: 64 << 20 + +// --------------------------------------------------------------------------- +// The image +// --------------------------------------------------------------------------- + +Image :: struct { + width: int, + height: int, + // Bits per component: 1, 2, 4, 8 or 16. Always 1 when is_mask. + bpc: int, + cs: ^Colorspace, + + // An /ImageMask is a 1-bit stencil painted in the current fill colour + // rather than an image with colours of its own. + is_mask: bool, + interpolate: bool, + // /Decode, 2*cs.n entries, or empty for the colour space's default. + decode: []f64, + + // /SMask: a greyscale image supplying alpha. + smask: ^Image, + // /Mask as a stencil image. + mask: ^Image, + // /Mask as a colour-key range: 2*cs.n integers in source sample values. + color_key: []int, + has_color_key: bool, + + // /Matte, on an /SMask whose owner's colour was pre-blended against it + // (ISO 32000-2, 11.6.5.3). Components are in the OWNER's colour space, so + // it is converted when the owner decodes, not when the mask loads. + matte: []f64, + has_matte: bool, + + // The stream the samples come from, decoded lazily. + stream: ^Stream, + // True for a BI/ID/EI image, which is not shared and so is not entered in + // the Document's by-stream table. + is_inline: bool, + + // Cache filled in by `image_decode`: straight-alpha RGBA8, or a 1-byte + // coverage plane when is_mask. + rgba: Bitmap, + decoded: bool, + failed: bool, + // Cache clock reading at the last `image_decode`, for LRU eviction. + last_use: u64, +} + +// image_size is the decoded pixel size. It is not the size the image is drawn +// at: an image always covers the unit square of the current transform. +image_size :: proc(img: ^Image) -> (w, h: int) { + if img == nil { + return 0, 0 + } + return img.width, img.height +} + +// --------------------------------------------------------------------------- +// The decoded-image cache +// +// Two caches, with different lifetimes on purpose: +// +// * `by_stream` maps an image XObject's stream to its Image, so a logo drawn +// on all 300 pages is parsed once. Images are arena memory and cost a few +// hundred bytes each, so nothing is ever evicted from it. The key carries +// the ROLE as well as the stream, because a stream a /Mask points at is +// read as a 1-bit stencil while the same stream drawn by `Do` is read as +// a picture: two Images, and one cannot be both. +// * `live` holds the Images whose pixels are currently decoded. Those are +// megabytes each and come from a real allocator, so they are evicted +// least-recently-used once `bytes` exceeds `budget`. +// --------------------------------------------------------------------------- + +// Img_Key is a stream and the way it is being read. `stencil` is true for the +// second reading a /Mask entry forces on a stream that is not an /ImageMask. +@(private) +Img_Key :: struct { + stream: uintptr, + stencil: bool, +} + +@(private) +Image_Cache :: struct { + by_stream: map[Img_Key]^Image, + live: [dynamic]^Image, + bytes: int, + // Zero means IMAGE_CACHE_BUDGET; a caller (or a test) may set it lower. + budget: int, + clock: u64, +} + +// img_pixel_allocator is where decoded pixels come from. NOT the Document +// arena: the arena cannot free an individual allocation, so an image cached in +// it could never be evicted and the budget would be a fiction. `close` calls +// `image_cache_release`, which is what keeps this leak-free. +@(private) +img_pixel_allocator :: proc(doc: ^Document) -> mem.Allocator { + if doc != nil && doc.backing.procedure != nil { + return doc.backing + } + return context.allocator +} + +// image_cache_release frees every decoded image. Called by `close`. +@(private) +image_cache_release :: proc(doc: ^Document) { + if doc == nil { + return + } + c := &doc.img_cache + for img in c.live { + if img == nil { + continue + } + bitmap_destroy(&img.rgba) + img.decoded = false + } + delete(c.live) + c.live = nil + c.bytes = 0 +} + +@(private) +img_cache_touch :: proc(doc: ^Document, img: ^Image) { + if doc == nil || img == nil { + return + } + doc.img_cache.clock += 1 + img.last_use = doc.img_cache.clock +} + +@(private) +img_cache_admit :: proc(doc: ^Document, img: ^Image) { + c := &doc.img_cache + if c.budget <= 0 { + c.budget = IMAGE_CACHE_BUDGET + } + if c.live == nil { + c.live = make([dynamic]^Image, 0, 16, img_pixel_allocator(doc)) + } + append(&c.live, img) + c.bytes += len(img.rgba.pixels) + img_cache_touch(doc, img) + img_cache_evict(doc, img) +} + +// img_cache_evict drops least-recently-used images until the budget is met. +// `keep` is never evicted, which is what makes the Bitmap `image_decode` just +// returned safe to use: the pointer stays valid until the next decode of a +// *different* image on this Document. +@(private) +img_cache_evict :: proc(doc: ^Document, keep: ^Image) { + c := &doc.img_cache + for c.bytes > c.budget { + victim := -1 + oldest := max(u64) + for img, i in c.live { + if img == nil || img == keep { + continue + } + if img.last_use <= oldest { + oldest = img.last_use + victim = i + } + } + if victim < 0 { + // Nothing left but `keep`. A single image larger than the whole + // budget is held until the next decode rather than thrown away + // before its caller has drawn it. + return + } + v := c.live[victim] + c.bytes -= len(v.rgba.pixels) + bitmap_destroy(&v.rgba) + v.decoded = false + v.failed = false + unordered_remove(&c.live, victim) + } +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// image_load reads an image XObject's dictionary. +// +// The Image is owned by the Document and must not be freed. The same stream +// read the same way always yields the same Image, so a picture placed on every +// page is read once and -- until the cache evicts it -- decoded once. A stream +// that is also named by some other image's /Mask has a second Image for that +// reading, because a stencil and a picture are not the same pixels. +image_load :: proc(doc: ^Document, stm: ^Stream, res: ^Dict) -> (^Image, Error) { + return img_load(doc, stm, res, 0) +} + +// image_load_inline reads a BI/ID/EI inline image (ISO 32000-2, 8.9.7). +// +// `d` is the dictionary between BI and ID, which uses the abbreviated keys +// /W /H /BPC /CS /F /D /DP /IM, and `data` is the still-encoded bytes between +// ID and EI. `data` is copied, so it need not outlive the call. `res` is the +// page's /Resources, where a colour space name that is not one of the +// abbreviations is looked up. +// +// The Image is owned by the Document. Inline images are not shared, so unlike +// an XObject they are not cached by stream: each BI builds a new one. +image_load_inline :: proc(doc: ^Document, d: ^Dict, data: []byte, res: ^Dict) -> (img: ^Image, err: Error) { + if doc == nil || d == nil { + return nil, .Invalid_Argument + } + // Copied rather than borrowed: the caller's buffer is a window onto a + // content stream that a per-page arena may reset, and an inline image is + // required to be small (8.9.7 recommends 4 KiB), so the copy is cheap. + payload: []byte + if len(data) > 0 { + buf, aerr := make([]byte, len(data), doc.allocator) + if aerr != nil { + return nil, .Out_Of_Memory + } + copy(buf, data) + payload = buf + } + stm := new_stream(d, payload, doc.allocator) + img = img_build(doc, stm, res, 0) or_return + img.is_inline = true + return img, .None +} + +@(private) +img_load :: proc(doc: ^Document, stm: ^Stream, res: ^Dict, depth: int) -> (img: ^Image, err: Error) { + if doc == nil || stm == nil { + return nil, .Invalid_Argument + } + if depth > MAX_IMAGE_NESTING { + add_repair(doc, .Recursion_Limit_Hit, 0, stm.ref) + return nil, .Recursion_Limit + } + + key := Img_Key{uintptr(rawptr(stm)), false} + if cached, ok := doc.img_cache.by_stream[key]; ok { + return cached, .None + } + + img = img_build(doc, stm, res, depth) or_return + img_cache_put(doc, key, img) + return img, .None +} + +@(private) +img_cache_put :: proc(doc: ^Document, key: Img_Key, img: ^Image) { + if doc.img_cache.by_stream == nil { + doc.img_cache.by_stream = make(map[Img_Key]^Image, 16, doc.allocator) + } + doc.img_cache.by_stream[key] = img +} + +// img_load_stencil reads a stream the way a /Mask entry means it: as a 1-bit +// stencil. 8.9.6.4 requires that stream to be an /ImageMask, and when it is +// this is exactly img_load. +// +// Producers that write a 1-bit (or 8-bit) /DeviceGray image instead mean the +// same thing -- the sample values are identical -- so it is read as a stencil +// anyway. What it must NOT do is write that decision back onto the Image the +// by-stream cache holds: that Image is what every other reference to the same +// stream gets, including a plain `Do` of it as a picture, and setting is_mask +// on it turns the picture into a one-channel stencil too. So the stencil +// reading is a second Image over the same stream, under its own key. +@(private) +img_load_stencil :: proc(doc: ^Document, stm: ^Stream, res: ^Dict, depth: int) -> + (img: ^Image, err: Error) { + m := img_load(doc, stm, res, depth) or_return + if m.is_mask { + return m, .None + } + key := Img_Key{uintptr(rawptr(stm)), true} + if cached, ok := doc.img_cache.by_stream[key]; ok { + return cached, .None + } + // Recorded once, where the second reading is built, and not again for + // every image that goes on to reference the same stencil. + add_repair(doc, .Bad_Resource, 0, stm.ref) + s := img_build(doc, stm, res, depth) or_return + s.is_mask = true + s.bpc = 1 + img_cache_put(doc, key, s) + return s, .None +} + +// img_samples_are_plausible reports whether a stream could hold the samples the +// dictionary claims. See MAX_IMAGE_EXPANSION. `n` and `bpc` are the component +// count and depth the samples will be read at; understating them only makes +// the test more permissive, which is the safe direction for a colour space +// that would not load. +@(private) +img_samples_are_plausible :: proc(stm: ^Stream, w, h, n, bpc: int) -> bool { + row := (w * max(n, 1) * max(bpc, 1) + 7) / 8 + if row <= 0 || h <= 0 { + return false + } + need := row * h + if need <= IMAGE_EXPANSION_FLOOR { + return true + } + return need / MAX_IMAGE_EXPANSION <= len(stm.raw) +} + +@(private) +img_build :: proc(doc: ^Document, stm: ^Stream, res: ^Dict, depth: int) -> (^Image, Error) { + d := stm.dict + if d == nil { + return nil, .Bad_Image + } + + w := img_dim(doc, d, "Width", "W") + h := img_dim(doc, d, "Height", "H") + if w <= 0 || h <= 0 || w > MAX_IMAGE_DIM || h > MAX_IMAGE_DIM || w > MAX_IMAGE_PIXELS / h { + // Nothing here can be rendered and nothing can be guessed: an image + // with no dimensions is not a repairable image, it is not an image. + add_repair(doc, .Bad_Resource, 0, stm.ref) + return nil, .Bad_Image + } + + img := new(Image, doc.allocator) + img.width = w + img.height = h + img.stream = stm + img.interpolate = img_flag(doc, d, "Interpolate", "I", false) + img.is_mask = img_flag(doc, d, "ImageMask", "IM", false) + + if img.is_mask { + // 8.9.6.2: a stencil mask has no colour space and /BitsPerComponent, + // if present at all, shall be 1. Files write 8 anyway; the format is + // unambiguous, so the dictionary loses. + img.bpc = 1 + if !img_samples_are_plausible(stm, w, h, 1, 1) { + add_repair(doc, .Bad_Resource, 0, stm.ref) + return nil, .Bad_Image + } + img.decode = img_decode_array(doc, d, 1, stm.ref) + return img, .None + } + + img.bpc = int(img_number(doc, d, "BitsPerComponent", "BPC", 8)) + switch img.bpc { + case 1, 2, 4, 8, 16: + case: + // A DCT or JPX image often omits /BitsPerComponent entirely, and 8 is + // the only depth either can hand back here. + img.bpc = 8 + } + + cs, cs_err := colorspace_load(doc, dict_get_alt(doc, d, "ColorSpace", "CS"), res) + if cs_err != .None || cs == nil { + // Left nil deliberately. A DCTDecode image whose /ColorSpace is broken + // still says how many components it has in its own frame header, and + // `img_fetch` picks the device space of that width; anything else + // falls back to the width implied by the sample count. + if dict_has(d, "ColorSpace") || dict_has(d, "CS") { + add_repair(doc, .Bad_Resource, 0, stm.ref) + } + } else { + img.cs = cs + } + + n := img.cs != nil ? min(img.cs.n, MAX_COLOR_COMPONENTS) : 0 + if !img_samples_are_plausible(stm, w, h, n, img.bpc) { + add_repair(doc, .Bad_Resource, 0, stm.ref) + return nil, .Bad_Image + } + img.decode = img_decode_array(doc, d, n, stm.ref) + + img_load_smask(doc, img, d, res, depth) + img_load_mask(doc, img, d, res, depth, n) + return img, .None +} + +@(private) +img_load_smask :: proc(doc: ^Document, img: ^Image, d: ^Dict, res: ^Dict, depth: int) { + sm := dict_stream(doc, d, "SMask") + if sm == nil { + return + } + m, err := img_load(doc, sm, res, depth + 1) + if err != .None || m == nil || m == img { + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + return + } + // /Matte lives in the soft mask's dictionary but its components are in the + // *owner's* colour space (11.6.5.3), which is why it is read here. + if mt := dict_array(doc, sm.dict, "Matte"); mt != nil && img.cs != nil { + want := min(img.cs.n, MAX_COLOR_COMPONENTS) + if array_len(mt) >= want && want > 0 { + vals := array_numbers(doc, mt, doc.allocator) + if len(vals) >= want && img_all_finite(vals[:want]) { + m.matte = vals[:want] + m.has_matte = true + } + } + } + img.smask = m +} + +@(private) +img_load_mask :: proc(doc: ^Document, img: ^Image, d: ^Dict, res: ^Dict, depth, n: int) { + o := dict_get(doc, d, "Mask") + #partial switch v in o { + case ^Stream: + m, err := img_load_stencil(doc, v, res, depth + 1) + if err != .None || m == nil || m == img { + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + return + } + img.mask = m + + case ^Array: + // 2*n integers, and they are SOURCE SAMPLE values -- indices into the + // raw sample range, not colours. Comparing them after /Decode has been + // applied is the classic way to get colour-key masking wrong. + want := 2 * n + if want <= 0 || array_len(v) < want { + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + return + } + keys, aerr := make([]int, want, doc.allocator) + if aerr != nil { + return + } + maxv := (int(1) << uint(clamp(img.bpc, 1, 16))) - 1 + for i in 0 ..< want { + keys[i] = clamp(int(array_int(doc, v, i, 0)), 0, maxv) + } + img.color_key = keys + img.has_color_key = true + } +} + +// img_decode_array reads /Decode (or its inline /D spelling). A wrong-length +// or non-finite array is dropped rather than honoured: the colour space's +// default is always a sane array, and half an applied /Decode is not. +@(private) +img_decode_array :: proc(doc: ^Document, d: ^Dict, n: int, ref: Ref) -> []f64 { + a, is_array := to_array(dict_get_alt(doc, d, "Decode", "D")) + if !is_array || a == nil { + return nil + } + if n <= 0 || array_len(a) < 2 * n { + add_repair(doc, .Bad_Resource, 0, ref) + return nil + } + vals := array_numbers(doc, a, doc.allocator) + if len(vals) < 2 * n || !img_all_finite(vals[:2 * n]) { + add_repair(doc, .Bad_Resource, 0, ref) + return nil + } + return vals[:2 * n] +} + +@(private) +img_all_finite :: proc(vals: []f64) -> bool { + for v in vals { + if !is_finite(v) { + return false + } + } + return true +} + +// img_dim reads /Width or /Height, which real files write as reals often +// enough ("/Width 612.0") that a plain integer read would reject them. +@(private) +img_dim :: proc(doc: ^Document, d: ^Dict, key, alt: string) -> int { + return int(img_number(doc, d, key, alt, 0)) +} + +@(private) +img_number :: proc(doc: ^Document, d: ^Dict, key, alt: string, def: f64) -> f64 { + v, ok := to_number(dict_get_alt(doc, d, key, alt)) + if !ok || !is_finite(v) { + return def + } + // Clamped before it is ever narrowed to an int: converting a f64 far + // outside the integer range is undefined, and /Width 1e300 is exactly the + // kind of thing a fuzzer writes. + return clamp(v, -1e9, 1e9) +} + +@(private) +img_flag :: proc(doc: ^Document, d: ^Dict, key, alt: string, def: bool) -> bool { + if v, ok := to_bool(dict_get_alt(doc, d, key, alt)); ok { + return v + } + return def +} + +// --------------------------------------------------------------------------- +// Samples +// --------------------------------------------------------------------------- + +// Image_Samples is a rectangle of unpacked-but-not-yet-interpreted samples. +// `row_bytes` is carried separately because it is not always +// `(w*n*bpc + 7)/8`: CCITTFaxDecode lays its rows out at /Columns, which is +// allowed to disagree with /Width. +@(private) +Image_Samples :: struct { + data: []byte, + w, h: int, + n: int, + bpc: int, + row_bytes: int, +} + +// Image_Data is everything one decode needs, plus the scratch buffers it must +// give back. Release it with `img_release`. +@(private) +Image_Data :: struct { + s: Image_Samples, + cs: ^Colorspace, + // Effective /Decode: the dictionary's if it was usable, the colour space's + // default otherwise. Always 2*s.n entries. + dec: []f64, + raw: []byte, // from stream_image_data + conv: []byte, // from jpeg_decode or ccitt_decode + decbuf: []f64, // from colorspace_default_decode +} + +@(private) +img_release :: proc(id: ^Image_Data, scratch: mem.Allocator) { + delete(id.raw, scratch) + delete(id.conv, scratch) + delete(id.decbuf, scratch) + id^ = {} +} + +@(private) +img_row :: proc(s: Image_Samples, y: int) -> []byte { + if s.row_bytes <= 0 || y < 0 { + return nil + } + start := y * s.row_bytes + if start >= len(s.data) { + return nil + } + return s.data[start:min(start + s.row_bytes, len(s.data))] +} + +// img_sample reads component `index` of a row, MSB first. A row shorter than +// the image claims reads as zeroes rather than as a fault: a truncated image +// stream is common and must still draw what arrived. +@(private) +img_sample :: proc(row: []byte, index, bpc: int) -> u32 { + switch bpc { + case 8: + return index < len(row) ? u32(row[index]) : 0 + case 16: + i := index * 2 + hi := i < len(row) ? u32(row[i]) : 0 + lo := i + 1 < len(row) ? u32(row[i + 1]) : 0 + return hi << 8 | lo + case 1, 2, 4: + bit := index * bpc + byi := bit >> 3 + if byi >= len(row) { + return 0 + } + shift := uint(8 - bpc - (bit & 7)) + return (u32(row[byi]) >> shift) & u32((1 << uint(bpc)) - 1) + } + return 0 +} + +// img_fetch runs the filter chain and hands back samples ready to unpack. +@(private) +img_fetch :: proc(doc: ^Document, img: ^Image, scratch: mem.Allocator) -> (id: Image_Data, err: Error) { + data, fname, parms, derr := stream_image_data(doc, img.stream, scratch) + if derr != .None { + delete(data, scratch) + add_repair(doc, .Bad_Filter_Data, 0, img.stream.ref) + return {}, derr + } + id.raw = data + + switch fname { + case "": + img_fetch_plain(doc, img, &id) + case "DCTDecode": + img_fetch_dct(doc, img, &id, scratch) or_return + case "CCITTFaxDecode": + img_fetch_ccitt(doc, img, &id, parms, scratch) or_return + case "JPXDecode", "JBIG2Decode": + // Neither codec is implemented. This is not an error that may kill a + // page: the caller turns it into a transparent placeholder. + return id, .Unsupported_Image + case: + return id, .Unsupported_Image + } + + id.dec = img_effective_decode(img, id.cs, id.s.bpc, id.s.n, scratch, &id.decbuf) + return id, .None +} + +@(private) +img_fetch_plain :: proc(doc: ^Document, img: ^Image, id: ^Image_Data) { + n := img.is_mask ? 1 : img_component_count(doc, img, len(id.raw)) + bpc := img.is_mask ? 1 : img.bpc + id.cs = img.is_mask ? nil : img_effective_cs(img, n) + id.s = Image_Samples{ + data = id.raw, + w = img.width, + h = img.height, + n = n, + bpc = bpc, + row_bytes = (img.width * n * bpc + 7) / 8, + } +} + +@(private) +img_fetch_dct :: proc(doc: ^Document, img: ^Image, id: ^Image_Data, scratch: mem.Allocator) -> Error { + j, jerr := jpeg_decode(id.raw, scratch) + if jerr != .None { + add_repair(doc, .Bad_Filter_Data, 0, img.stream.ref) + return jerr + } + id.conv = j.data + if j.width <= 0 || j.height <= 0 || j.components <= 0 || + j.width > MAX_IMAGE_DIM || j.height > MAX_IMAGE_DIM || + j.width > MAX_IMAGE_PIXELS / j.height { + return .Bad_Image + } + if j.width != img.width || j.height != img.height { + // The frame header is the authority: it is what the samples are laid + // out to. An image always covers the unit square of the CTM, so taking + // the JPEG's size changes the resolution drawn, never the placement. + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + img.width = j.width + img.height = j.height + } + + // The Adobe APP14 transform (`j.adobe_transform`) has already been undone + // by the decoder: a 3-component YCbCr scan arrives as RGB and a + // 4-component YCCK scan as CMYK. What is deliberately NOT undone there is + // the sample inversion Adobe applies to the CMYK JPEGs it embeds in PDFs. + // That inversion is expressed in this dictionary's /Decode array -- + // [1 0 1 0 1 0 1 0] -- and is applied below like any other /Decode. + // Inverting here as well would double-apply it and turn every photograph + // into its own negative. + id.cs = img.cs + if id.cs == nil || min(id.cs.n, MAX_COLOR_COMPONENTS) != j.components { + id.cs = colorspace_device_n(j.components) + } + id.s = Image_Samples{ + data = j.data, + w = j.width, + h = j.height, + n = j.components, + bpc = 8, + row_bytes = j.width * j.components, + } + return .None +} + +@(private) +img_fetch_ccitt :: proc(doc: ^Document, img: ^Image, id: ^Image_Data, parms: ^Dict, + scratch: mem.Allocator) -> Error { + p := filter_ccitt_params(doc, parms) + if !dict_has(parms, "Columns") { + // /Columns defaults to 1728 because that is the width of a fax page, + // but inside a PDF the two are required to agree and a producer that + // omits it meant /Width. Taking 1728 for a 2544-pixel scan would shear + // every row. + p.columns = img.width + } else if p.columns != ccitt_columns(p) { + // Present, but not a width `ccitt_decode` will honour as written: it + // substitutes 1728 for a non-positive /Columns and clamps anything + // past MAX_CCITT_COLUMNS, and would then lay its rows out at that + // stride while this side read them back at another. /Width is the only + // other statement of the width in the file, and using it for both + // decodes the picture instead of merely agreeing about where the + // garbage starts. + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + p.columns = img.width + } + if p.rows <= 0 { + p.rows = img.height + } + if p.columns != img.width { + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + } + + out, cerr := ccitt_decode(id.raw, p, scratch) + if cerr != .None { + delete(out, scratch) + add_repair(doc, .Bad_Filter_Data, 0, img.stream.ref) + return cerr + } + id.conv = out + + // ccitt_decode emits exactly the 1-bit sample array an image expects: with + // /BlackIs1 false (the default) a black pixel is a 0 bit, which DeviceGray + // maps to 0.0, i.e. black. /BlackIs1 true exchanges the two in the sample + // data, and it is then the image's /Decode -- not this filter -- that says + // what the samples mean. So nothing is inverted here. + // CCITT is bi-level, so there is exactly one component whatever the + // dictionary's colour space claims. + id.cs = img.is_mask ? nil : img_effective_cs(img, 1) + id.s = Image_Samples{ + data = out, + w = img.width, + h = img.height, + n = 1, + bpc = 1, + // Asked of the filter rather than recomputed: `p.columns` is now a + // width it honours verbatim, and going through the same procedure it + // does is what keeps the two from ever drifting apart again. + row_bytes = ccitt_row_bytes(p), + } + return .None +} + +// img_effective_cs is the colour space to interpret `n` components in. A +// missing or unloadable /ColorSpace falls back to the device space of that +// width, which is what 8.6.5.5 says an ICCBased with no alternate does and the +// only guess available for an inline image with no /CS at all. +@(private) +img_effective_cs :: proc(img: ^Image, n: int) -> ^Colorspace { + if img.cs != nil && min(img.cs.n, MAX_COLOR_COMPONENTS) == n { + return img.cs + } + return colorspace_device_n(n) +} + +// img_component_count is how many components a row holds. Normally the colour +// space says; when it could not be loaded the sample count is divided by the +// pixel count, which recovers the common "/ColorSpace is an unresolvable +// reference" case exactly. +@(private) +img_component_count :: proc(doc: ^Document, img: ^Image, nbytes: int) -> int { + if img.cs != nil { + return max(1, min(img.cs.n, MAX_COLOR_COMPONENTS)) + } + for n in 1 ..= 4 { + need := ((img.width * n * img.bpc + 7) / 8) * img.height + if need == nbytes { + return n + } + } + return 1 +} + +// img_effective_decode returns 2*n decode entries: the dictionary's if it had +// the right shape, the colour space's default otherwise. Indexed and Lab are +// exactly why the default cannot be assumed to be [0 1 0 1 ...]. +@(private) +img_effective_decode :: proc(img: ^Image, cs: ^Colorspace, bpc, n: int, + scratch: mem.Allocator, owned: ^[]f64) -> []f64 { + if len(img.decode) >= 2 * n && n > 0 { + return img.decode[:2 * n] + } + if img.is_mask || cs == nil { + // A stencil's one component defaults to [0 1]: sample 0 paints. + buf, err := make([]f64, 2 * max(n, 1), scratch) + if err != nil { + return nil + } + for i in 0 ..< max(n, 1) { + buf[2 * i], buf[2 * i + 1] = 0, 1 + } + owned^ = buf + return buf + } + d := colorspace_default_decode(cs, bpc, scratch) + if len(d) < 2 * n { + delete(d, scratch) + return nil + } + owned^ = d + return d[:2 * n] +} + +// --------------------------------------------------------------------------- +// Decoding to pixels +// --------------------------------------------------------------------------- + +// image_decode decodes the image to pixels, caching the result on the Image. +// +// The result is straight-alpha RGBA8, or a one-channel coverage plane when +// `img.is_mask`. It belongs to the Document and must not be freed. +// +// LIFETIME: the pixels stay valid until the next `image_decode` of a +// *different* image on this Document, or until `close`. The cache evicts +// least-recently-used images to stay inside its byte budget, and never evicts +// the one it has just been asked for, so the ordinary +// "decode, draw, decode the next" loop is safe; holding one image's pixels +// across another's decode is not. The pointer itself never dangles -- it is +// into the Image, which lives as long as the Document -- so a caller that +// does hold one too long reads an empty Bitmap rather than freed memory. +// +// An image that cannot be decoded at all -- a broken stream, a JPEG 2000 or +// JBIG2 codestream -- comes back as a fully transparent image of the right +// size with a Repair on the Document. The Error return is for a nil argument. +image_decode :: proc(doc: ^Document, img: ^Image) -> (^Bitmap, Error) { + if doc == nil || img == nil { + return nil, .Invalid_Argument + } + if img.decoded { + img_cache_touch(doc, img) + return &img.rgba, .None + } + + bmp, err := img_decode_uncached(doc, img) + if err != .None { + add_repair(doc, .Bad_Filter_Data, 0, img.stream != nil ? img.stream.ref : Ref{}) + ph, perr := img_placeholder(doc, img) + if perr != .None { + return nil, perr + } + bmp = ph + img.failed = true + } + + img.rgba = bmp + img.decoded = true + img_cache_admit(doc, img) + return &img.rgba, .None +} + +// img_placeholder is a fully transparent image (or an empty stencil) of the +// right size, so a page whose picture will not decode still lays out. +@(private) +img_placeholder :: proc(doc: ^Document, img: ^Image) -> (Bitmap, Error) { + n := img.is_mask ? 1 : 4 + w := clamp(img.width, 1, MAX_IMAGE_DIM) + h := clamp(img.height, 1, MAX_IMAGE_DIM) + if w > MAX_IMAGE_PIXELS / h { + w, h = 1, 1 + } + return bitmap_make(w, h, n, img_pixel_allocator(doc)) +} + +@(private) +img_decode_uncached :: proc(doc: ^Document, img: ^Image) -> (bmp: Bitmap, err: Error) { + scratch := context.allocator + id, ferr := img_fetch(doc, img, scratch) + defer img_release(&id, scratch) + if ferr != .None { + return {}, ferr + } + if id.s.n <= 0 || len(id.dec) < 2 * id.s.n { + return {}, .Bad_Image + } + if img.width <= 0 || img.height <= 0 || img.width > MAX_IMAGE_PIXELS / img.height { + return {}, .Bad_Image + } + + if img.is_mask { + stencil := bitmap_make(img.width, img.height, 1, img_pixel_allocator(doc)) or_return + img_unpack_stencil(id.s, id.dec, &stencil) + return stencil, .None + } + + out := bitmap_make(img.width, img.height, 4, img_pixel_allocator(doc)) or_return + img_unpack_rgba(id.s, id.cs, id.dec, img.has_color_key ? img.color_key : nil, &out) + img_apply_alpha(doc, img, &out, scratch) + return out, .None +} + +// img_unpack_stencil fills a one-byte coverage plane: 255 where the stencil +// paints. Sample 0 paints under the default /Decode [0 1]; /Decode [1 0] +// exchanges the two (8.9.6.2). +@(private) +img_unpack_stencil :: proc(s: Image_Samples, dec: []f64, out: ^Bitmap) { + d0 := len(dec) >= 2 ? dec[0] : 0 + d1 := len(dec) >= 2 ? dec[1] : 1 + for y in 0 ..< out.height { + row := img_row(s, y) + dst := bitmap_row(out, y) + if dst == nil { + continue + } + for x in 0 ..< min(out.width, len(dst)) { + v := img_sample(row, x, 1) == 0 ? d0 : d1 + dst[x] = v < 0.5 ? 255 : 0 + } + } +} + +// img_cs_is_identity_rgb reports whether the space maps a three-component +// value straight onto RGB. That is true of /DeviceRGB and of most of the +// ICCBased spaces that 24% of the corpus uses, which render through a +// DeviceRGB alternate. It is answered by PROBING the space rather than by +// pattern-matching its kind, so the fast path below is taken only where it +// provably agrees with the general one. +@(private) +img_cs_is_identity_rgb :: proc(cs: ^Colorspace) -> bool { + if cs == nil || min(cs.n, MAX_COLOR_COMPONENTS) != 3 { + return false + } + if cs.kind == .Device_RGB { + return true + } + #partial switch cs.kind { + case .Cal_RGB, .ICC_Based: + case: + return false + } + probes := [?][3]f64{ + {0, 0, 0}, {1, 1, 1}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, + {0.25, 0.5, 0.75}, {0.5, 0.2, 0.9}, + } + for p in probes { + got := colorspace_to_rgb(cs, color_make(p[0], p[1], p[2])) + for i in 0 ..< 3 { + if abs(got[i] - p[i]) > 1.0 / 512.0 { + return false + } + } + } + return true +} + +@(private) +img_is_default_decode :: proc(dec: []f64, n: int) -> bool { + if len(dec) < 2 * n { + return false + } + for i in 0 ..< n { + if dec[2 * i] != 0 || dec[2 * i + 1] != 1 { + return false + } + } + return true +} + +@(private) +img_unpack_rgba :: proc(s: Image_Samples, cs: ^Colorspace, dec: []f64, color_key: []int, + out: ^Bitmap) { + n := s.n + if n <= 0 || n > MAX_COLOR_COMPONENTS { + return + } + bpc := s.bpc + maxv := f64((int(1) << uint(clamp(bpc, 1, 16))) - 1) + if maxv <= 0 { + maxv = 1 + } + + // One component and at most 8 bits: 256 entries cover every possible + // sample, so the whole colour conversion -- an Indexed palette lookup, a + // Separation tint transform, a CalGray gamma -- is done at most 256 times + // instead of once per pixel. + lut: [256][3]u8 + use_lut := n == 1 && bpc <= 8 + if use_lut { + comp: Color + comp.n = 1 + for i in 0 ..< (1 << uint(bpc)) { + comp.v[0] = dec[0] + f64(i) * (dec[1] - dec[0]) / maxv + f := colorspace_to_rgb(cs, comp) + lut[i] = {blend_quantize(f[0]), blend_quantize(f[1]), blend_quantize(f[2])} + } + } + direct_rgb := n == 3 && bpc == 8 && img_is_default_decode(dec, 3) && img_cs_is_identity_rgb(cs) + + ck := len(color_key) >= 2 * n ? color_key : nil + + comp: Color + comp.n = min(n, MAX_COLOR_COMPONENTS) + raws: [MAX_COLOR_COMPONENTS]u32 + + // A row the sample data does not reach at all reads as zeroes, so it is + // one colour from end to end -- and a declared /Height the stream comes + // nowhere near is the ordinary shape both of a truncated scan and of a + // dictionary written to make a reader work. Computing that colour once + // turns those rows into a fill instead of a colour conversion per pixel, + // which for a 32-colorant /DeviceN at 16 bits is the difference between a + // memory-speed loop and a minute of arithmetic. The bytes written are the + // same ones the general path would have written. + pad: [4]u8 + { + rgb: [3]u8 + switch { + case use_lut: + rgb = lut[0] + case direct_rgb: + case: + for c in 0 ..< comp.n { + // lo + 0 * (hi - lo) / maxv. + comp.v[c] = dec[2 * c] + } + f := colorspace_to_rgb(cs, comp) + rgb = {blend_quantize(f[0]), blend_quantize(f[1]), blend_quantize(f[2])} + } + alpha: u8 = 255 + if ck != nil { + masked := true + for c in 0 ..< n { + if ck[2 * c] > 0 || ck[2 * c + 1] < 0 { + masked = false + break + } + } + if masked { + alpha = 0 + } + } + pad = {rgb[0], rgb[1], rgb[2], alpha} + } + + for y in 0 ..< out.height { + row := img_row(s, y) + dst := bitmap_row(out, y) + if dst == nil { + continue + } + if row == nil { + for x in 0 ..< out.width { + di := x * 4 + if di + 4 > len(dst) { + break + } + copy(dst[di:di + 4], pad[:]) + } + continue + } + for x in 0 ..< out.width { + di := x * 4 + if di + 4 > len(dst) { + break + } + base := x * n + + rgb: [3]u8 + if use_lut { + v := img_sample(row, base, bpc) + raws[0] = v + rgb = lut[v & 0xff] + } else if direct_rgb { + raws[0] = img_sample(row, base, 8) + raws[1] = img_sample(row, base + 1, 8) + raws[2] = img_sample(row, base + 2, 8) + rgb = {u8(raws[0]), u8(raws[1]), u8(raws[2])} + } else { + for c in 0 ..< comp.n { + raws[c] = img_sample(row, base + c, bpc) + lo, hi := dec[2 * c], dec[2 * c + 1] + comp.v[c] = lo + f64(raws[c]) * (hi - lo) / maxv + } + f := colorspace_to_rgb(cs, comp) + rgb = {blend_quantize(f[0]), blend_quantize(f[1]), blend_quantize(f[2])} + } + + alpha: u8 = 255 + if ck != nil { + // Compared against the RAW samples, before /Decode: the ranges + // are sample values (8.9.6.4), not colour values. + masked := true + for c in 0 ..< n { + r := int(raws[c]) + if r < ck[2 * c] || r > ck[2 * c + 1] { + masked = false + break + } + } + if masked { + alpha = 0 + } + } + + dst[di] = rgb[0] + dst[di + 1] = rgb[1] + dst[di + 2] = rgb[2] + dst[di + 3] = alpha + } + } +} + +// --------------------------------------------------------------------------- +// Transparency: /SMask and a stencil /Mask +// --------------------------------------------------------------------------- + +@(private) +img_apply_alpha :: proc(doc: ^Document, img: ^Image, out: ^Bitmap, scratch: mem.Allocator) { + m := img.smask + is_stencil := false + if m == nil { + m = img.mask + is_stencil = true + } + if m == nil { + return + } + + plane, err := img_alpha_plane(doc, m, is_stencil, out.width, out.height, scratch) + defer delete(plane, scratch) + if err != .None || len(plane) < out.width * out.height { + // A broken mask must not take the image with it: an opaque picture is + // a far better answer than a blank page. + add_repair(doc, .Bad_Resource, 0, img.stream.ref) + return + } + + matte: [3]f64 + unblend := false + if !is_stencil && m.has_matte && img.cs != nil { + c := color_make() + c.n = min(len(m.matte), MAX_COLOR_COMPONENTS) + for i in 0 ..< c.n { + c.v[i] = m.matte[i] + } + matte = colorspace_to_rgb(img.cs, c) + unblend = true + } + + for y in 0 ..< out.height { + dst := bitmap_row(out, y) + prow := plane[y * out.width:] + for x in 0 ..< out.width { + di := x * 4 + if di + 4 > len(dst) || x >= len(prow) { + break + } + a := prow[x] + // Both mechanisms multiply into whatever alpha colour-key masking + // already produced, rather than replacing it. + dst[di + 3] = u8((int(dst[di + 3]) * int(a) + 127) / 255) + if unblend && a != 0 && a != 255 { + // /Matte says the colour was stored pre-blended against the + // matte: c' = m + a*(c - m). Undo it (11.6.5.3). Done in RGB + // rather than in the image's own components, which is exact + // for the device spaces and a close approximation elsewhere. + for k in 0 ..< 3 { + mk := matte[k] * 255 + v := mk + (f64(dst[di + k]) - mk) * 255 / f64(a) + dst[di + k] = blend_quantize(v / 255) + } + } + } + } +} + +// img_alpha_plane decodes a mask to one byte of alpha per pixel, resampled to +// `w` x `h`. The result is allocated with `scratch` and belongs to the caller. +// +// Resampling is nearest-neighbour. An /SMask at a different resolution to its +// image is ordinary -- a photograph at 300 dpi with a 72 dpi cut-out mask, say +// -- and interpolating the mask would soften an edge the producer drew hard. +@(private) +img_alpha_plane :: proc(doc: ^Document, m: ^Image, is_stencil: bool, w, h: int, + scratch: mem.Allocator) -> ([]u8, Error) { + if m == nil || w <= 0 || h <= 0 || w > MAX_IMAGE_PIXELS / h { + return nil, .Invalid_Argument + } + id, err := img_fetch(doc, m, scratch) + defer img_release(&id, scratch) + if err != .None || id.s.n <= 0 || len(id.dec) < 2 * id.s.n { + return nil, err == .None ? .Bad_Image : err + } + if m.width <= 0 || m.height <= 0 || m.width > MAX_IMAGE_PIXELS / m.height { + return nil, .Bad_Image + } + + src, aerr := make([]u8, m.width * m.height, scratch) + if aerr != nil { + return nil, .Out_Of_Memory + } + + maxv := f64((int(1) << uint(clamp(id.s.bpc, 1, 16))) - 1) + if maxv <= 0 { + maxv = 1 + } + stencil := is_stencil || m.is_mask + for y in 0 ..< m.height { + row := img_row(id.s, y) + for x in 0 ..< m.width { + v := f64(img_sample(row, x * id.s.n, id.s.bpc)) + v = id.dec[0] + v * (id.dec[1] - id.dec[0]) / maxv + if stencil { + // A stencil /Mask marks what is masked OUT: sample 0 (decoded + // value 0) paints the base image, 1 hides it (8.9.6.4). That + // is the same polarity an /ImageMask uses. + src[y * m.width + x] = v < 0.5 ? 255 : 0 + } else { + // An /SMask is DeviceGray by definition (11.6.5.2), so the + // decoded component is the alpha; no colour conversion. + src[y * m.width + x] = blend_quantize(v) + } + } + } + + if m.width == w && m.height == h { + return src, .None + } + defer delete(src, scratch) + + out, oerr := make([]u8, w * h, scratch) + if oerr != nil { + return nil, .Out_Of_Memory + } + for y in 0 ..< h { + sy := clamp(y * m.height / h, 0, m.height - 1) + for x in 0 ..< w { + sx := clamp(x * m.width / w, 0, m.width - 1) + out[y * w + x] = src[sy * m.width + sx] + } + } + return out, .None +} diff --git a/image_test.odin b/image_test.odin new file mode 100644 index 0000000..98b631b --- /dev/null +++ b/image_test.odin @@ -0,0 +1,1629 @@ +package pdf + +import "core:testing" + +// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` -- live in +// function_test.odin. The JPEG byte literals `JPEG_T_CMYK` and `JPEG_T_GREY8` +// live in filter_dct_test.odin; using the decoder's own vectors here is what +// makes the DCT tests below about /Decode and colour rather than about +// re-testing the JPEG decoder. + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +@(private = "file") +im_load :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, + res: ^Dict = nil, loc := #caller_location) -> ^Image { + img, err := image_load(doc, cf_stream(doc, dict_src, payload), res) + testing.expectf(t, err == .None, "image_load: %v", err, loc = loc) + return img +} + +@(private = "file") +im_pixels :: proc(t: ^testing.T, doc: ^Document, img: ^Image, loc := #caller_location) -> ^Bitmap { + testing.expect(t, img != nil, "no image to decode", loc = loc) + if img == nil { + return nil + } + bmp, err := image_decode(doc, img) + testing.expectf(t, err == .None, "image_decode: %v", err, loc = loc) + testing.expect(t, bmp != nil, "image_decode returned no bitmap", loc = loc) + return bmp +} + +// im_decode loads and decodes in one step, which is what most of these want. +@(private = "file") +im_decode :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, + res: ^Dict = nil, loc := #caller_location) -> ^Bitmap { + return im_pixels(t, doc, im_load(t, doc, dict_src, payload, res, loc), loc) +} + +@(private = "file") +im_rgba :: proc(bmp: ^Bitmap, x, y: int) -> [4]u8 { + if bmp == nil || bmp.n != 4 { + return {} + } + off, ok := bitmap_pixel_offset(bmp, x, y) + if !ok { + return {} + } + return {bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2], bmp.pixels[off + 3]} +} + +@(private = "file") +expect_rgba :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, what: string, + loc := #caller_location) { + got := im_rgba(bmp, x, y) + if got != want { + testing.expectf(t, false, "%s at (%d,%d): got %v, want %v", what, x, y, got, want, + loc = loc) + } +} + +@(private = "file") +expect_rgba_near :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, tol: int, + what: string, loc := #caller_location) { + got := im_rgba(bmp, x, y) + worst := 0 + for i in 0 ..< 4 { + worst = max(worst, abs(int(got[i]) - int(want[i]))) + } + if worst > tol { + testing.expectf(t, false, "%s at (%d,%d): got %v, want %v (+-%d)", what, x, y, got, + want, tol, loc = loc) + } +} + +// expect_gray reads a one-channel coverage plane, which is what an /ImageMask +// decodes to. +@(private = "file") +expect_cov :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: u8, what: string, + loc := #caller_location) { + if bmp == nil || bmp.n != 1 { + testing.expectf(t, false, "%s: expected a 1-channel plane, got n=%d", what, + bmp == nil ? -1 : bmp.n, loc = loc) + return + } + off, ok := bitmap_pixel_offset(bmp, x, y) + if !ok { + testing.expectf(t, false, "%s: (%d,%d) out of range", what, x, y, loc = loc) + return + } + if bmp.pixels[off] != want { + testing.expectf(t, false, "%s at (%d,%d): got %d, want %d", what, x, y, + bmp.pixels[off], want, loc = loc) + } +} + +// im_arena copies bytes into the Document's arena, so a payload built in a +// test outlives the Stream that borrows it. +@(private = "file") +im_arena :: proc(doc: ^Document, src: []byte) -> []byte { + b := make([]byte, len(src), doc.allocator) + copy(b, src) + return b +} + +@(private = "file") +im_arena_fill :: proc(doc: ^Document, n: int, fill: u8) -> []byte { + b := make([]byte, n, doc.allocator) + for &v in b { + v = fill + } + return b +} + +// --------------------------------------------------------------------------- +// Sample unpacking, one test per bit depth +// +// Expected bytes are hand-computed: a raw sample s of a /DeviceGray image maps +// to round(255 * s / (2^bpc - 1)). +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +GRAY8_DATA := [?]byte{0, 64, 128, 255, 10, 20, 30, 40} + +@(test) +test_image_bpc_8 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + GRAY8_DATA[:]) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 4) + testing.expect_value(t, bmp.height, 2) + testing.expect_value(t, bmp.n, 4) + // Straight alpha out, never premultiplied: blend_span_rgba8 says so. + testing.expect(t, !bmp.premultiplied, "image_decode must produce straight alpha") + + for x in 0 ..< 4 { + v := GRAY8_DATA[x] + expect_rgba(t, bmp, x, 0, {v, v, v, 255}, "row 0") + } + for x in 0 ..< 4 { + v := GRAY8_DATA[4 + x] + expect_rgba(t, bmp, x, 1, {v, v, v, 255}, "row 1") + } +} + +@(private = "file", rodata) +// 1 0 1 1 0 0 0 1 / 0 1 0 0 1 1 1 0 +GRAY1_DATA := [?]byte{0b1011_0001, 0b0100_1110} + +@(test) +test_image_bpc_1 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 8 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", + GRAY1_DATA[:]) + if bmp == nil { return } + + want_row0 := [8]u8{255, 0, 255, 255, 0, 0, 0, 255} + want_row1 := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} + for x in 0 ..< 8 { + expect_rgba(t, bmp, x, 0, {want_row0[x], want_row0[x], want_row0[x], 255}, "row 0") + expect_rgba(t, bmp, x, 1, {want_row1[x], want_row1[x], want_row1[x], 255}, "row 1") + } +} + +@(private = "file", rodata) +// 00 01 10 11 -> samples 0 1 2 3, and 11 10 01 00 on the second row. +GRAY2_DATA := [?]byte{0b00_01_10_11, 0b11_10_01_00} + +@(test) +test_image_bpc_2 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 2 >>", + GRAY2_DATA[:]) + if bmp == nil { return } + + // 255 * s / 3: 0, 85, 170, 255. + want := [4]u8{0, 85, 170, 255} + for x in 0 ..< 4 { + expect_rgba(t, bmp, x, 0, {want[x], want[x], want[x], 255}, "row 0") + expect_rgba(t, bmp, x, 1, {want[3 - x], want[3 - x], want[3 - x], 255}, "row 1") + } +} + +@(private = "file", rodata) +// Samples 0, 15 / 5, 10. +GRAY4_DATA := [?]byte{0x0F, 0x5A} + +@(test) +test_image_bpc_4 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 4 >>", + GRAY4_DATA[:]) + if bmp == nil { return } + + // 255 * s / 15: 0, 255, 85, 170. + expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "s=0") + expect_rgba(t, bmp, 1, 0, {255, 255, 255, 255}, "s=15") + expect_rgba(t, bmp, 0, 1, {85, 85, 85, 255}, "s=5") + expect_rgba(t, bmp, 1, 1, {170, 170, 170, 255}, "s=10") +} + +@(private = "file", rodata) +// 0x0000, 0x8000, 0xFFFF, 0x4000, 0x00FF. +GRAY16_DATA := [?]byte{0x00, 0x00, 0x80, 0x00, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0xFF} + +@(test) +test_image_bpc_16 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 5 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 16 >>", + GRAY16_DATA[:]) + if bmp == nil { return } + + // round(255 * s / 65535): 0, 128, 255, 64, 1. The last one is the sample + // that keeps this honest -- 0x00FF is 1, not the 0 that dropping the low + // byte and calling it a 16-bit decode would give. + expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "0x0000") + expect_rgba(t, bmp, 1, 0, {128, 128, 128, 255}, "0x8000") + expect_rgba(t, bmp, 2, 0, {255, 255, 255, 255}, "0xFFFF") + expect_rgba(t, bmp, 3, 0, {64, 64, 64, 255}, "0x4000") + expect_rgba(t, bmp, 4, 0, {1, 1, 1, 255}, "0x00FF") +} + +@(private = "file", rodata) +// 16-bit RGB, one pixel per row: (0x1234, 0x5678, 0x9ABC) and (0,0,0). +RGB16_DATA := [?]byte{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0, 0, 0, 0, 0, 0} + +@(test) +test_image_16bit_rgb :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 1 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 16 >>", + RGB16_DATA[:]) + if bmp == nil { return } + + // 0x1234 = 4660 -> 4660*255/65535 = 18.13 -> 18 + // 0x5678 = 22136 -> 22136*255/65535 = 86.13 -> 86 + // 0x9ABC = 39612 -> 39612*255/65535 = 154.13 -> 154 + expect_rgba(t, bmp, 0, 0, {18, 86, 154, 255}, "16-bit RGB") + expect_rgba(t, bmp, 0, 1, {0, 0, 0, 255}, "16-bit black") +} + +// --------------------------------------------------------------------------- +// Row padding +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +// Three 1-bit pixels per row, each row padded out to a whole byte: +// 101 00000 / 010 00000. Packing the rows continuously instead would put +// row 1 at bit 3 of byte 0, and both rows would read 1,0,1. +PAD1_DATA := [?]byte{0b101_00000, 0b010_00000} + +@(test) +test_image_row_padding_1bpc :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 3 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", + PAD1_DATA[:]) + if bmp == nil { return } + + expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "row 0 px 0") + expect_rgba(t, bmp, 1, 0, {0, 0, 0, 255}, "row 0 px 1") + expect_rgba(t, bmp, 2, 0, {255, 255, 255, 255}, "row 0 px 2") + expect_rgba(t, bmp, 0, 1, {0, 0, 0, 255}, "row 1 px 0") + expect_rgba(t, bmp, 1, 1, {255, 255, 255, 255}, "row 1 px 1") + expect_rgba(t, bmp, 2, 1, {0, 0, 0, 255}, "row 1 px 2") +} + +@(private = "file", rodata) +// Three 4-bit pixels is 12 bits, so each row occupies two bytes with the last +// four bits padding: samples 0,15,5 then 10,5,0. +PAD4_DATA := [?]byte{0x0F, 0x50, 0xA5, 0x00} + +@(test) +test_image_row_padding_4bpc :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 3 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 4 >>", + PAD4_DATA[:]) + if bmp == nil { return } + + expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "row 0 px 0") + expect_rgba(t, bmp, 1, 0, {255, 255, 255, 255}, "row 0 px 1") + expect_rgba(t, bmp, 2, 0, {85, 85, 85, 255}, "row 0 px 2") + expect_rgba(t, bmp, 0, 1, {170, 170, 170, 255}, "row 1 px 0") + expect_rgba(t, bmp, 1, 1, {85, 85, 85, 255}, "row 1 px 1") + expect_rgba(t, bmp, 2, 1, {0, 0, 0, 255}, "row 1 px 2") +} + +// --------------------------------------------------------------------------- +// /Decode +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +DEC_DATA := [?]byte{0, 128, 255} + +@(test) +test_image_decode_inverts_gray :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + plain := im_decode(t, doc, + "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + DEC_DATA[:]) + expect_rgba(t, plain, 0, 0, {0, 0, 0, 255}, "no /Decode") + expect_rgba(t, plain, 2, 0, {255, 255, 255, 255}, "no /Decode") + + inverted := im_decode(t, doc, + "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Decode [1 0] >>", + DEC_DATA[:]) + expect_rgba(t, inverted, 0, 0, {255, 255, 255, 255}, "/Decode [1 0]") + expect_rgba(t, inverted, 1, 0, {127, 127, 127, 255}, "/Decode [1 0]") + expect_rgba(t, inverted, 2, 0, {0, 0, 0, 255}, "/Decode [1 0]") +} + +@(test) +test_image_decode_wrong_length_is_dropped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A three-component space with a two-entry /Decode. Half an applied decode + // is worse than none, so the default is used and a repair recorded. + before := len(doc.repairs) + bmp := im_decode(t, doc, + "<< /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Decode [1 0] >>", + im_arena(doc, {255, 0, 0})) + expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "short /Decode ignored") + testing.expect(t, len(doc.repairs) > before, "a dropped /Decode must record a repair") +} + +@(test) +test_image_decode_partial_range :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Decode [0.25 0.75] compresses the range: sample 0 -> 0.25 -> 64, + // sample 255 -> 0.75 -> 191, sample 128 -> 0.25 + 128/255*0.5 = 0.5010 -> 128. + bmp := im_decode(t, doc, + "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Decode [0.25 0.75] >>", + DEC_DATA[:]) + expect_rgba(t, bmp, 0, 0, {64, 64, 64, 255}, "0.25") + expect_rgba(t, bmp, 1, 0, {128, 128, 128, 255}, "midpoint") + expect_rgba(t, bmp, 2, 0, {191, 191, 191, 255}, "0.75") +} + +// --------------------------------------------------------------------------- +// Indexed, whose default /Decode is [0 2^bpc - 1] and not [0 1] +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +// Four 2-bit samples: 0, 1, 2, 3 -- red, green, blue, white. +INDEXED_DATA := [?]byte{0b00_01_10_11} + +@(test) +test_image_indexed_default_decode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 4 /Height 1 /BitsPerComponent 2 " + + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", + INDEXED_DATA[:]) + if bmp == nil { return } + + // The sample IS the index. Assuming the [0 1] default that every other + // space has would scale it to 0, 0.33, 0.67, 1, which rounds to indices + // 0, 0, 1, 1 -- red, red, green, green. + expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "index 0") + expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index 1") + expect_rgba(t, bmp, 2, 0, {0, 0, 255, 255}, "index 2") + expect_rgba(t, bmp, 3, 0, {255, 255, 255, 255}, "index 3") +} + +@(test) +test_image_indexed_out_of_range_index :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /HiVal 1 with an 8-bit sample: index 200 is past the end of the table + // and must clamp to the last entry, never read past it. + bmp := im_decode(t, doc, + "<< /Width 2 /Height 1 /BitsPerComponent 8 " + + "/ColorSpace [/Indexed /DeviceRGB 1 ] >>", + im_arena(doc, {0, 200})) + expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "index 0") + expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index clamped to hival") +} + +@(test) +test_image_indexed_explicit_decode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Decode [3 0] reverses the palette order at 2 bits per component. + bmp := im_decode(t, doc, + "<< /Width 4 /Height 1 /BitsPerComponent 2 /Decode [3 0] " + + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", + INDEXED_DATA[:]) + expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "sample 0 -> index 3") + expect_rgba(t, bmp, 3, 0, {255, 0, 0, 255}, "sample 3 -> index 0") +} + +// --------------------------------------------------------------------------- +// Lab, whose default /Decode is [0 100 amin amax bmin bmax] +// --------------------------------------------------------------------------- + +@(test) +test_image_lab_default_decode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // L* = 100, a* = b* = 0 is white. Scaling the samples onto 0..1 instead -- + // the default every other space has -- would give L* = 1, which is black. + bmp := im_decode(t, doc, + "<< /Width 1 /Height 1 /BitsPerComponent 8 " + + "/ColorSpace [/Lab << /WhitePoint [0.9505 1.0 1.089] /Range [-100 100 -100 100] >>] >>", + im_arena(doc, {255, 128, 128})) + got := im_rgba(bmp, 0, 0) + testing.expectf(t, got[0] > 240 && got[1] > 240 && got[2] > 240, + "Lab (100, ~0, ~0) should be white, got %v", got) +} + +// --------------------------------------------------------------------------- +// Separation: one component, so it goes through the sample lookup table +// --------------------------------------------------------------------------- + +@(test) +test_image_separation :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Object 5 of the fixture is [/Separation /Spot /DeviceCMYK 6 0 R], whose + // full tint is 0 1 1 0 -- red. + bmp := im_decode(t, doc, + "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace 5 0 R >>", + im_arena(doc, {0, 128, 255})) + expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "no tint") + expect_rgba(t, bmp, 2, 0, {255, 0, 0, 255}, "full tint") + // Half tint: c = 0, m = y = 128/255, k = 0. + expect_rgba_near(t, bmp, 1, 0, {255, 127, 127, 255}, 1, "half tint") +} + +// --------------------------------------------------------------------------- +// CMYK +// --------------------------------------------------------------------------- + +@(test) +test_image_cmyk_8bit :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", + im_arena(doc, {0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0})) + expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "no ink") + expect_rgba(t, bmp, 1, 0, {0, 0, 0, 255}, "full black") + expect_rgba(t, bmp, 2, 0, {0, 255, 255, 255}, "full cyan") +} + +// --------------------------------------------------------------------------- +// /ImageMask: a stencil, both polarities +// --------------------------------------------------------------------------- + +@(test) +test_image_mask_both_ways :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Bits 1 0 1 1 0 0 0 1. Sample 0 paints under the default /Decode [0 1]. + plain := im_decode(t, doc, "<< /Width 8 /Height 1 /ImageMask true >>", GRAY1_DATA[:]) + if plain == nil { return } + testing.expect_value(t, plain.n, 1) + want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} + for x in 0 ..< 8 { + expect_cov(t, plain, x, 0, want[x], "default /Decode") + } + + flipped := im_decode(t, doc, + "<< /Width 8 /Height 1 /ImageMask true /Decode [1 0] >>", GRAY1_DATA[:]) + for x in 0 ..< 8 { + expect_cov(t, flipped, x, 0, 255 - want[x], "/Decode [1 0]") + } +} + +@(test) +test_image_mask_forces_one_bit :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 8.9.6.2: an /ImageMask has no colour space and /BitsPerComponent shall + // be 1. Producers write 8 anyway; the format wins. + img := im_load(t, doc, + "<< /Width 8 /Height 1 /ImageMask true /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", + GRAY1_DATA[:]) + if img == nil { return } + testing.expect_value(t, img.bpc, 1) + testing.expect(t, img.cs == nil, "a stencil has no colour space") + bmp := im_pixels(t, doc, img) + expect_cov(t, bmp, 1, 0, 255, "still read as 1 bit") +} + +// --------------------------------------------------------------------------- +// /SMask +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +// 2x2 DeviceRGB: red, green / blue, white. +SMASK_BASE := [?]byte{255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255} + +@(test) +test_image_smask_matching_dimensions :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + alpha := cf_stream(doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena(doc, {0, 64, 192, 255})) + d, _ := to_dict(cf_parse(doc, + "<< /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>")) + dict_set(d, "SMask", alpha) + img, err := image_load(doc, new_stream(d, SMASK_BASE[:], doc.allocator), nil) + testing.expect_value(t, err, Error.None) + testing.expect(t, img != nil && img.smask != nil, "the /SMask must have loaded") + + bmp := im_pixels(t, doc, img) + expect_rgba(t, bmp, 0, 0, {255, 0, 0, 0}, "alpha 0") + expect_rgba(t, bmp, 1, 0, {0, 255, 0, 64}, "alpha 64") + expect_rgba(t, bmp, 0, 1, {0, 0, 255, 192}, "alpha 192") + expect_rgba(t, bmp, 1, 1, {255, 255, 255, 255}, "alpha 255") +} + +@(test) +test_image_smask_different_dimensions :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A 4x4 image with a 2x2 mask: nearest-neighbour, so columns 0-1 take mask + // column 0 and columns 2-3 take mask column 1. + alpha := cf_stream(doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena(doc, {0, 255, 128, 64})) + d, _ := to_dict(cf_parse(doc, + "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + dict_set(d, "SMask", alpha) + img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 16, 200), doc.allocator), nil) + bmp := im_pixels(t, doc, img) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 4) + + for y in 0 ..< 4 { + for x in 0 ..< 4 { + want: u8 = y < 2 ? (x < 2 ? 0 : 255) : (x < 2 ? 128 : 64) + expect_rgba(t, bmp, x, y, {200, 200, 200, want}, "resampled /SMask") + } + } +} + +@(test) +test_image_smask_matte :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Matte [1 1 1]: the colour was stored pre-blended against white, as + // c' = m + a*(c - m). At alpha 0.5 a stored 191 came from + // 255 + 0.5*(c - 255), i.e. c = 127. Undoing the blend must recover it. + alpha := cf_stream(doc, + "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Matte [1 1 1] >>", + im_arena(doc, {128, 255})) + d, _ := to_dict(cf_parse(doc, + "<< /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>")) + dict_set(d, "SMask", alpha) + img, _ := image_load(doc, new_stream(d, im_arena(doc, {191, 191, 191, 10, 20, 30}), + doc.allocator), nil) + testing.expect(t, img != nil && img.smask != nil && img.smask.has_matte, + "/Matte must be read from the soft mask's dictionary") + bmp := im_pixels(t, doc, img) + // 255 + (191 - 255) * 255/128 = 255 - 127.5 = 127.5 -> 127 or 128. + expect_rgba_near(t, bmp, 0, 0, {128, 128, 128, 128}, 2, "unblended against white") + // A fully opaque pixel is not pre-blended at all and must not be touched. + expect_rgba(t, bmp, 1, 0, {10, 20, 30, 255}, "opaque pixel is untouched") +} + +// --------------------------------------------------------------------------- +// /Mask as a stencil +// --------------------------------------------------------------------------- + +@(test) +test_image_stencil_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 1 masks OUT by default. Bits 1 0 1 1 0 0 0 1 over an 8x1 grey image. + stencil := cf_stream(doc, "<< /Width 8 /Height 1 /ImageMask true >>", GRAY1_DATA[:]) + d, _ := to_dict(cf_parse(doc, + "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + dict_set(d, "Mask", stencil) + img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 100), doc.allocator), nil) + testing.expect(t, img != nil && img.mask != nil, "the stencil /Mask must have loaded") + + bmp := im_pixels(t, doc, img) + want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} + for x in 0 ..< 8 { + expect_rgba(t, bmp, x, 0, {100, 100, 100, want[x]}, "stencil /Mask") + } +} + +// --------------------------------------------------------------------------- +// Colour-key masking, which compares SOURCE samples +// --------------------------------------------------------------------------- + +@(test) +test_image_color_key_uses_source_samples :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Decode [1 0] and a key range of [0 0]. Sample 0 is in the range and is + // masked out; after the decode that same pixel is 1.0, and a colour-key + // test done on the decoded value would instead mask sample 255. The two + // answers are exact opposites, so this cannot pass by accident. + bmp := im_decode(t, doc, + "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 " + + "/Decode [1 0] /Mask [0 0] >>", DEC_DATA[:]) + expect_rgba(t, bmp, 0, 0, {255, 255, 255, 0}, "sample 0 is masked out") + expect_rgba(t, bmp, 1, 0, {127, 127, 127, 255}, "sample 128 is kept") + expect_rgba(t, bmp, 2, 0, {0, 0, 0, 255}, "sample 255 is kept") +} + +@(test) +test_image_color_key_all_components_must_match :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Key range green-ish: every component must fall inside its own range for + // the pixel to be masked. + bmp := im_decode(t, doc, + "<< /Width 3 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 " + + "/Mask [0 10 200 255 0 10] >>", + im_arena(doc, {0, 255, 0, 5, 210, 8, 0, 255, 200})) + expect_rgba(t, bmp, 0, 0, {0, 255, 0, 0}, "exactly in range") + expect_rgba(t, bmp, 1, 0, {5, 210, 8, 0}, "inside every range") + expect_rgba(t, bmp, 2, 0, {0, 255, 200, 255}, "blue is outside its range") +} + +@(test) +test_image_color_key_indexed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // On an Indexed image the key range is a range of INDICES. + bmp := im_decode(t, doc, + "<< /Width 4 /Height 1 /BitsPerComponent 2 /Mask [2 2] " + + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", + INDEXED_DATA[:]) + expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index 1 kept") + expect_rgba(t, bmp, 2, 0, {0, 0, 255, 0}, "index 2 masked out") + expect_rgba(t, bmp, 3, 0, {255, 255, 255, 255}, "index 3 kept") +} + +// --------------------------------------------------------------------------- +// DCTDecode +// --------------------------------------------------------------------------- + +@(test) +test_image_cmyk_jpeg_needs_its_decode_array :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // JPEG_T_CMYK is a 16x16 Adobe CMYK JPEG whose samples are stored + // complemented: (255-16x, 255-16y, 55, 225). filter_dct.odin deliberately + // does not un-complement them, because the file expresses the inversion in + // /Decode [1 0 1 0 1 0 1 0]. Applying it recovers the source image + // (16x, 16y, 200, 30), and forgetting it turns the photograph into its own + // negative -- which the second half of this test pins down. + bmp := im_decode(t, doc, + "<< /Width 16 /Height 16 /BitsPerComponent 8 /ColorSpace /DeviceCMYK " + + "/Filter /DCTDecode /Decode [1 0 1 0 1 0 1 0] >>", JPEG_T_CMYK[:]) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 16) + + // c = 16x/255, m = 16y/255, y = 200/255, k = 30/255, then + // r = 1 - min(1, c+k), g = 1 - min(1, m+k), b = 1 - min(1, y+k). + expect_rgba_near(t, bmp, 0, 0, {225, 225, 25, 255}, 8, "top-left") + expect_rgba_near(t, bmp, 4, 2, {161, 193, 25, 255}, 8, "interior") + expect_rgba_near(t, bmp, 15, 15, {0, 0, 25, 255}, 8, "bottom-right") + + // The same JPEG with no /Decode: every component is complemented, so + // c = m = 1 at the top-left and the pixel goes black. + naive := im_decode(t, doc, + "<< /Width 16 /Height 16 /BitsPerComponent 8 /ColorSpace /DeviceCMYK " + + "/Filter /DCTDecode >>", JPEG_T_CMYK[:]) + got := im_rgba(naive, 0, 0) + testing.expectf(t, got[0] < 40 && got[1] < 40, + "without /Decode the corner must stay dark, got %v", got) +} + +@(test) +test_image_grey_jpeg :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // JPEG_T_GREY8 is an 8x8 grey ramp; JPEG_T_GREY8_PIXELS is what the + // decoder's own test expects out of it. + bmp := im_decode(t, doc, + "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /DCTDecode >>", + JPEG_T_GREY8[:]) + if bmp == nil { return } + for y in 0 ..< 8 { + for x in 0 ..< 8 { + v := JPEG_T_GREY8_PIXELS[y * 8 + x] + expect_rgba_near(t, bmp, x, y, {v, v, v, 255}, 4, "grey JPEG") + } + } +} + +@(test) +test_image_jpeg_dimensions_win_over_the_dictionary :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The frame header says 8x8. A /Width that disagrees would shear every row + // if it were believed, so the frame header is taken and a repair recorded. + before := len(doc.repairs) + img := im_load(t, doc, + "<< /Width 40 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /DCTDecode >>", + JPEG_T_GREY8[:]) + bmp := im_pixels(t, doc, img) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 8) + testing.expect_value(t, bmp.height, 8) + w, h := image_size(img) + testing.expect_value(t, w, 8) + testing.expect_value(t, h, 8) + testing.expect(t, len(doc.repairs) > before, "a size disagreement must record a repair") +} + +@(test) +test_image_jpeg_component_count_wins_over_the_colour_space :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A one-component JPEG declared /DeviceRGB. Believing the dictionary would + // read three consecutive grey pixels as one RGB pixel. + bmp := im_decode(t, doc, + "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /DCTDecode >>", + JPEG_T_GREY8[:]) + if bmp == nil { return } + v := JPEG_T_GREY8_PIXELS[0] + expect_rgba_near(t, bmp, 0, 0, {v, v, v, 255}, 4, "read as grey") +} + +// --------------------------------------------------------------------------- +// CCITTFaxDecode +// +// G4_IMAGE is the 8-column, 12-row Group 4 vector filter_ccitt_test.odin +// checks the codec against; it was produced by libtiff. Decoded it is +// G4_ROWS, one byte per row, and with /BlackIs1 false a 1 bit is white. +// --------------------------------------------------------------------------- + +@(private = "file", rodata) +G4_IMAGE := [?]byte{ + 0x93, 0x51, 0x59, 0x43, 0x94, 0x38, 0x4A, 0xAE, 0xC4, 0x38, 0x2A, 0x3D, 0x8C, 0xDB, 0x07, 0x09, + 0x46, 0x00, 0x20, 0x02, +} + +@(private = "file", rodata) +G4_ROWS := [?]byte{0xFF, 0x00, 0xAA, 0x33, 0xFC, 0xE0, 0xCF, 0xFF, 0xF0, 0xFE, 0xF9, 0xFF} + +@(private = "file") +g4_bit :: proc(row: int, x: int) -> u8 { + return (G4_ROWS[row] >> uint(7 - x)) & 1 +} + +@(test) +test_image_ccitt :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bmp := im_decode(t, doc, + "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 >> >>", + G4_IMAGE[:]) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 8) + testing.expect_value(t, bmp.height, 12) + + for y in 0 ..< 12 { + for x in 0 ..< 8 { + // /BlackIs1 defaults to false, so a 1 bit is white, which + // DeviceGray maps to 1.0. The filter's convention and the image's + // are the same one, so nothing is inverted between them. + v: u8 = g4_bit(y, x) == 1 ? 255 : 0 + expect_rgba(t, bmp, x, y, {v, v, v, 255}, "CCITT G4") + } + } +} + +@(test) +test_image_ccitt_black_is_1 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /BlackIs1 exchanges the bits in the SAMPLE data. Nothing in the image + // layer compensates, so the picture comes out inverted -- which is exactly + // what the format says happens, and why a producer that sets it also + // writes /Decode [1 0]. + bmp := im_decode(t, doc, + "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 /BlackIs1 true >> >>", + G4_IMAGE[:]) + for y in 0 ..< 12 { + for x in 0 ..< 8 { + v: u8 = g4_bit(y, x) == 1 ? 0 : 255 + expect_rgba(t, bmp, x, y, {v, v, v, 255}, "/BlackIs1 true") + } + } +} + +@(test) +test_image_ccitt_missing_columns_uses_width :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Columns is absent. Its format default is 1728, but inside a PDF the two + // are required to agree, so /Width is what the producer meant; taking 1728 + // would lay the rows out 216 bytes apart and show nothing at all. + bmp := im_decode(t, doc, + "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 >> >>", G4_IMAGE[:]) + for x in 0 ..< 8 { + v: u8 = g4_bit(2, x) == 1 ? 255 : 0 + expect_rgba(t, bmp, x, 2, {v, v, v, 255}, "row 2 with no /Columns") + } +} + +@(test) +test_image_ccitt_unusable_columns_uses_width :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Columns is present but is not a width the codec will honour as + // written: it reads a non-positive one as the format's 1728 default and + // clamps anything past MAX_CCITT_COLUMNS. Left alone, the decoder lays its + // rows out 216 (or 8192) bytes apart while the image reads them 1 byte + // apart, and every row after the first starts at the wrong byte -- the + // picture is gone with nothing to say so. /Width is what both sides use. + HEAD :: "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Rows 12 /Columns " + srcs := [?]string{ + HEAD + "0 >> >>", + HEAD + "-8 >> >>", + HEAD + "70000 >> >>", + } + for src in srcs { + bmp := im_decode(t, doc, src, G4_IMAGE[:]) + if bmp == nil { continue } + for y in 0 ..< 12 { + for x in 0 ..< 8 { + v: u8 = g4_bit(y, x) == 1 ? 255 : 0 + expect_rgba(t, bmp, x, y, {v, v, v, 255}, src) + } + } + } +} + +@(test) +test_image_ccitt_as_stencil :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The same fax data as an /ImageMask: sample 0 (black) paints. + bmp := im_decode(t, doc, + "<< /Width 8 /Height 12 /ImageMask true " + + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 >> >>", + G4_IMAGE[:]) + if bmp == nil { return } + testing.expect_value(t, bmp.n, 1) + for x in 0 ..< 8 { + expect_cov(t, bmp, x, 0, 0, "row 0 is white, so it paints nothing") + expect_cov(t, bmp, x, 1, 255, "row 1 is black, so it paints") + } +} + +// --------------------------------------------------------------------------- +// Formats that are not decoded +// --------------------------------------------------------------------------- + +@(test) +test_image_jpx_is_a_transparent_placeholder :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sources := [?]string{ + "<< /Width 4 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /JPXDecode >>", + "<< /Width 4 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /JBIG2Decode >>", + } + for src in sources { + before := len(doc.repairs) + img := im_load(t, doc, src, im_arena_fill(doc, 32, 0x42)) + bmp, err := image_decode(doc, img) + testing.expectf(t, err == .None, "%s must not fail the page: %v", src, err) + if bmp == nil { continue } + testing.expect_value(t, bmp.width, 4) + testing.expect_value(t, bmp.height, 3) + for y in 0 ..< 3 { + for x in 0 ..< 4 { + expect_rgba(t, bmp, x, y, {0, 0, 0, 0}, "placeholder is transparent") + } + } + testing.expect(t, len(doc.repairs) > before, "an undecoded format must record a repair") + testing.expect(t, img.failed, "the image must be marked failed") + } +} + +@(test) +test_image_broken_stream_degrades :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Flate data that is not flate data at all. The chain fails outright, so + // this takes the same route as a format that is not implemented: a + // transparent image of the declared size, a repair, and no error. + before := len(doc.repairs) + img := im_load(t, doc, + "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /FlateDecode >>", + im_arena(doc, {0xDE, 0xAD, 0xBE, 0xEF})) + bmp, err := image_decode(doc, img) + testing.expect_value(t, err, Error.None) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 2) + testing.expect_value(t, bmp.height, 2) + expect_rgba(t, bmp, 0, 0, {0, 0, 0, 0}, "unreadable stream") + expect_rgba(t, bmp, 1, 1, {0, 0, 0, 0}, "unreadable stream") + testing.expect(t, img.failed, "the image must be marked failed") + testing.expect(t, len(doc.repairs) > before, "and record a repair") +} + +// --------------------------------------------------------------------------- +// Malformed dictionaries +// --------------------------------------------------------------------------- + +@(test) +test_image_short_data_pads :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 16 samples are needed and 5 are supplied. + bmp := im_decode(t, doc, + "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena(doc, {10, 20, 30, 40, 50})) + if bmp == nil { return } + testing.expect_value(t, bmp.width, 4) + testing.expect_value(t, bmp.height, 4) + expect_rgba(t, bmp, 0, 0, {10, 10, 10, 255}, "supplied") + expect_rgba(t, bmp, 3, 0, {40, 40, 40, 255}, "supplied") + expect_rgba(t, bmp, 0, 1, {50, 50, 50, 255}, "last supplied byte") + expect_rgba(t, bmp, 1, 1, {0, 0, 0, 255}, "past the data") + expect_rgba(t, bmp, 3, 3, {0, 0, 0, 255}, "past the data") +} + +@(test) +test_image_absurd_dimensions_are_refused :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + bad := [?]string{ + "<< /Width 0 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /Width 4 /Height 0 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /Width -5 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /Width 100000 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /Width 1000000000 /Height 1000000000 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + // 2^26 is the pixel cap; 20000 x 20000 is 4x10^8 and well past it. + "<< /Width 20000 /Height 20000 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /Width 1e300 /Height 1e300 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + "<< /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + } + for src in bad { + img, err := image_load(doc, cf_stream(doc, src, GRAY8_DATA[:]), nil) + testing.expectf(t, err == .Bad_Image, "%s should be refused, got %v", src, err) + testing.expect(t, img == nil, "a refused image must not be returned") + } +} + +@(test) +test_image_dimensions_are_checked_against_the_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 65536 x 1024 is exactly MAX_IMAGE_PIXELS, so the dimension caps pass it + // and nothing else ever related it to the bytes that arrived: about 150 + // bytes of dictionary bought a 256 MiB allocation and a colour conversion + // over 67 million pixels, and a page may name many such XObjects. + bad := [?]string{ + "<< /Width 65536 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", + "<< /Width 65536 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", + "<< /Width 65536 /Height 1024 /BitsPerComponent 16 /ColorSpace /DeviceRGB >>", + "<< /Width 65536 /Height 1024 /ImageMask true >>", + } + for src in bad { + img, err := image_load(doc, cf_stream(doc, src, nil), nil) + testing.expectf(t, err == .Bad_Image, "%s should be refused, got %v", src, err) + testing.expect(t, img == nil, "a refused image must not be returned") + } + testing.expect(t, len(doc.repairs) > 0, "refusing one should record a repair") +} + +@(test) +test_image_badly_truncated_but_plausible_still_loads :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The stream check is about the RATIO, not about the shortfall: 600 bytes + // for a 2 MiB image is a 3500-fold truncation and still loads, because a + // blank page under CCITT G4 or a flat one under Flate really does compress + // that far and neither is a broken file. + img := im_load(t, doc, + "<< /Width 2048 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", + im_arena_fill(doc, 600, 0x40)) + testing.expect(t, img != nil, "a truncated image must still load") + if img == nil { return } + testing.expect_value(t, img.width, 2048) + testing.expect_value(t, img.height, 1024) + + // And a small image is never looked at at all, however empty its stream. + tiny, err := image_load(doc, + cf_stream(doc, "<< /Width 64 /Height 64 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", + nil), nil) + testing.expect_value(t, err, Error.None) + testing.expect(t, tiny != nil, "a small image with an empty stream still loads") +} + +@(test) +test_image_rows_past_the_data_are_the_zero_sample :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A row the samples do not reach is one colour from end to end, so it is + // computed once and filled. That shortcut has to produce exactly what the + // per-pixel path would: in CMYK the all-zero sample is WHITE, not the + // black that assuming a zeroed RGB pixel would give. + bmp := im_decode(t, doc, + "<< /Width 2 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", + im_arena(doc, {0, 0, 0, 255, 0, 0, 0, 255})) + if bmp == nil { return } + expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "the one supplied row: K=255 is black") + expect_rgba(t, bmp, 0, 1, {255, 255, 255, 255}, "past the data: zero CMYK is white") + expect_rgba(t, bmp, 1, 3, {255, 255, 255, 255}, "past the data, last row") + + // Colour-key masking is decided on the raw samples, so an absent row can + // be transparent as well -- the filled row has to carry that too. + keyed := im_decode(t, doc, + "<< /Width 2 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceRGB " + + "/Mask [0 0 0 0 0 0] >>", + im_arena(doc, {9, 9, 9, 9, 9, 9})) + if keyed == nil { return } + expect_rgba(t, keyed, 0, 0, {9, 9, 9, 255}, "the supplied row is not in the key range") + expect_rgba(t, keyed, 0, 2, {0, 0, 0, 0}, "an absent row is all zeroes, which is masked") +} + +@(test) +test_image_no_dict_is_refused :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + _, err := image_load(doc, new_stream(nil, GRAY8_DATA[:], doc.allocator), nil) + testing.expect_value(t, err, Error.Bad_Image) + + _, nerr := image_load(doc, nil, nil) + testing.expect_value(t, nerr, Error.Invalid_Argument) + + _, derr := image_decode(nil, nil) + testing.expect_value(t, derr, Error.Invalid_Argument) + + w, h := image_size(nil) + testing.expect_value(t, w, 0) + testing.expect_value(t, h, 0) +} + +@(test) +test_image_missing_colorspace_is_inferred :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // No /ColorSpace at all, and 12 bytes for 2x2 pixels: three components. + bmp := im_decode(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 >>", + im_arena(doc, {255, 0, 0, 0, 255, 0, 0, 0, 255, 9, 9, 9})) + expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "inferred DeviceRGB") + expect_rgba(t, bmp, 1, 1, {9, 9, 9, 255}, "inferred DeviceRGB") + + // One byte per pixel: grey. + grey := im_decode(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 >>", + im_arena(doc, {1, 2, 3, 4})) + expect_rgba(t, grey, 0, 0, {1, 1, 1, 255}, "inferred DeviceGray") +} + +@(test) +test_image_bad_bpc_falls_back_to_8 :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + img := im_load(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 7 >>", GRAY8_DATA[:]) + if img == nil { return } + testing.expect_value(t, img.bpc, 8) + expect_rgba(t, im_pixels(t, doc, img), 1, 0, {64, 64, 64, 255}, "read at 8 bits") +} + +@(test) +test_image_self_referential_smask_terminates :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A stream whose /SMask is itself. The nesting cap is what stops this. + d, _ := to_dict(cf_parse(doc, + "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + stm := new_stream(d, im_arena(doc, {10, 200}), doc.allocator) + dict_set(d, "SMask", stm) + + img, err := image_load(doc, stm, nil) + testing.expect_value(t, err, Error.None) + bmp := im_pixels(t, doc, img) + if bmp == nil { return } + // The mask is the image itself, so its own samples become the alpha. + expect_rgba(t, bmp, 0, 0, {10, 10, 10, 10}, "self mask") + expect_rgba(t, bmp, 1, 0, {200, 200, 200, 200}, "self mask") +} + +@(test) +test_image_broken_smask_leaves_the_image_opaque :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The /SMask is a JPEG 2000 codestream, which this library does not + // decode. Losing the alpha is bad; losing the picture would be worse. + alpha := cf_stream(doc, + "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /JPXDecode >>", + im_arena_fill(doc, 4, 0)) + d, _ := to_dict(cf_parse(doc, + "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + dict_set(d, "SMask", alpha) + img, _ := image_load(doc, new_stream(d, im_arena(doc, {40, 90}), doc.allocator), nil) + bmp := im_pixels(t, doc, img) + expect_rgba(t, bmp, 0, 0, {40, 40, 40, 255}, "opaque despite the broken mask") + expect_rgba(t, bmp, 1, 0, {90, 90, 90, 255}, "opaque despite the broken mask") +} + +// --------------------------------------------------------------------------- +// Inline images +// --------------------------------------------------------------------------- + +@(test) +test_image_inline_abbreviated_keys :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Every key and the colour space in their abbreviated spellings. + d, ok := to_dict(cf_parse(doc, "<< /W 4 /H 2 /BPC 8 /CS /G /D [1 0] /IM false >>")) + testing.expect(t, ok, "inline dictionary did not parse") + img, err := image_load_inline(doc, d, GRAY8_DATA[:], nil) + testing.expect_value(t, err, Error.None) + if img == nil { return } + testing.expect_value(t, img.width, 4) + testing.expect_value(t, img.height, 2) + testing.expect_value(t, img.bpc, 8) + testing.expect(t, img.is_inline, "an inline image must know that it is one") + testing.expect(t, img.cs != nil && img.cs.kind == .Device_Gray, "/G is DeviceGray") + + bmp := im_pixels(t, doc, img) + for x in 0 ..< 4 { + v := 255 - GRAY8_DATA[x] + expect_rgba(t, bmp, x, 0, {v, v, v, 255}, "inline /D [1 0]") + } +} + +@(test) +test_image_inline_data_is_copied :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The content interpreter's buffer is scratch; an Image outlives it. + scratch := make([]byte, 4, context.allocator) + scratch[0], scratch[1], scratch[2], scratch[3] = 1, 2, 3, 4 + d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 2 /BPC 8 /CS /G >>")) + img, _ := image_load_inline(doc, d, scratch, nil) + for &b in scratch { + b = 0xFF + } + delete(scratch, context.allocator) + + bmp := im_pixels(t, doc, img) + expect_rgba(t, bmp, 0, 0, {1, 1, 1, 255}, "copied, not borrowed") + expect_rgba(t, bmp, 1, 1, {4, 4, 4, 255}, "copied, not borrowed") +} + +@(test) +test_image_inline_rgb_and_indexed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 1 /BPC 8 /CS /RGB >>")) + img, _ := image_load_inline(doc, d, im_arena(doc, {255, 0, 0, 0, 0, 255}), nil) + expect_rgba(t, im_pixels(t, doc, img), 0, 0, {255, 0, 0, 255}, "/RGB") + + // /I is the abbreviation for Indexed inside a colour space array. + di, _ := to_dict(cf_parse(doc, + "<< /W 2 /H 1 /BPC 8 /CS [/I /RGB 1 ] >>")) + imi, _ := image_load_inline(doc, di, im_arena(doc, {1, 0}), nil) + bi := im_pixels(t, doc, imi) + expect_rgba(t, bi, 0, 0, {0, 255, 0, 255}, "/I palette entry 1") + expect_rgba(t, bi, 1, 0, {255, 0, 0, 255}, "/I palette entry 0") + + // /CMYK, the fourth abbreviation. + dc, _ := to_dict(cf_parse(doc, "<< /W 1 /H 1 /BPC 8 /CS /CMYK >>")) + imc, _ := image_load_inline(doc, dc, im_arena(doc, {0, 0, 0, 0}), nil) + expect_rgba(t, im_pixels(t, doc, imc), 0, 0, {255, 255, 255, 255}, "/CMYK") +} + +@(test) +test_image_inline_named_colorspace_from_resources :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A name that is not one of the abbreviations resolves through the page's + // /Resources /ColorSpace. + res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /Cs1 /DeviceRGB >> >>")) + d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 1 /BPC 8 /CS /Cs1 >>")) + img, err := image_load_inline(doc, d, im_arena(doc, {255, 0, 0, 0, 255, 0}), res) + testing.expect_value(t, err, Error.None) + testing.expect(t, img != nil && img.cs != nil && img.cs.kind == .Device_RGB, + "/Cs1 must resolve through /Resources") + expect_rgba(t, im_pixels(t, doc, img), 1, 0, {0, 255, 0, 255}, "resource colour space") +} + +@(test) +test_image_inline_stencil_with_filter :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /IM true and /F, the abbreviated /Filter, on the same inline image. + // "8 %-fdt" is <2A 2A 2A> in ASCII85 terms; use ASCIIHexDecode instead, + // which is unambiguous: B1 is 1011 0001. + d, _ := to_dict(cf_parse(doc, "<< /W 8 /H 1 /IM true /F /AHx >>")) + img, err := image_load_inline(doc, d, transmute([]byte)string("B1>"), nil) + testing.expect_value(t, err, Error.None) + bmp := im_pixels(t, doc, img) + if bmp == nil { return } + testing.expect_value(t, bmp.n, 1) + want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} + for x in 0 ..< 8 { + expect_cov(t, bmp, x, 0, want[x], "inline stencil through /F /AHx") + } +} + +// --------------------------------------------------------------------------- +// Caching +// --------------------------------------------------------------------------- + +@(test) +test_image_load_is_cached_by_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + stm := cf_stream(doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + GRAY8_DATA[:]) + a, _ := image_load(doc, stm, nil) + b, _ := image_load(doc, stm, nil) + testing.expect(t, a != nil && a == b, "the same stream must give the same Image") + + first := im_pixels(t, doc, a) + bytes_after_first := doc.img_cache.bytes + second := im_pixels(t, doc, b) + testing.expect(t, first == second, "the decoded bitmap must be reused") + testing.expect_value(t, doc.img_cache.bytes, bytes_after_first) + testing.expect_value(t, len(doc.img_cache.live), 1) +} + +@(test) +test_image_cache_evicts_least_recently_used :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Three 100x100 grey images: 40000 bytes of RGBA each. A budget of 90000 + // holds two. + doc.img_cache.budget = 90000 + imgs: [3]^Image + for i in 0 ..< 3 { + imgs[i] = im_load(t, doc, + "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena_fill(doc, 10000, u8(i * 30))) + } + + im_pixels(t, doc, imgs[0]) + im_pixels(t, doc, imgs[1]) + testing.expect_value(t, doc.img_cache.bytes, 80000) + testing.expect(t, imgs[0].decoded && imgs[1].decoded, "both fit in the budget") + + im_pixels(t, doc, imgs[2]) + testing.expect(t, doc.img_cache.bytes <= 90000, "the budget must be respected") + testing.expect(t, !imgs[0].decoded, "the least recently used image must be evicted") + testing.expect(t, imgs[1].decoded && imgs[2].decoded, "the two newest must survive") + testing.expect_value(t, len(doc.img_cache.live), 2) + + // An evicted image decodes again on demand, with the same pixels. + again := im_pixels(t, doc, imgs[0]) + testing.expect(t, imgs[0].decoded, "re-decoded after eviction") + expect_rgba(t, again, 0, 0, {0, 0, 0, 255}, "re-decoded content") +} + +@(test) +test_image_cache_keeps_the_image_being_decoded :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A budget smaller than a single image. It is still decoded and returned: + // throwing it away before the caller has drawn it would be worse than + // being briefly over budget. + doc.img_cache.budget = 16 + img := im_load(t, doc, + "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena_fill(doc, 10000, 77)) + bmp := im_pixels(t, doc, img) + expect_rgba(t, bmp, 50, 50, {77, 77, 77, 255}, "over-budget image still decodes") + testing.expect(t, img.decoded, "and is still the one held") +} + +@(test) +test_image_touch_orders_the_cache :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + doc.img_cache.budget = 90000 + imgs: [3]^Image + for i in 0 ..< 3 { + imgs[i] = im_load(t, doc, + "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena_fill(doc, 10000, u8(i * 30))) + } + im_pixels(t, doc, imgs[0]) + im_pixels(t, doc, imgs[1]) + // Re-reading image 0 makes image 1 the least recently used one. + im_pixels(t, doc, imgs[0]) + im_pixels(t, doc, imgs[2]) + testing.expect(t, imgs[0].decoded, "the re-read image must survive") + testing.expect(t, !imgs[1].decoded, "the untouched image must be the victim") +} + +// --------------------------------------------------------------------------- +// Fast paths must agree with the general one +// --------------------------------------------------------------------------- + +@(test) +test_image_iccbased_rgb_matches_devicergb :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // ICCBased renders through its /Alternate, so this is DeviceRGB by another + // name and must give byte-identical pixels. `img_cs_is_identity_rgb` + // probes for exactly this. + icc := cf_stream(doc, "<< /N 3 /Alternate /DeviceRGB >>", im_arena_fill(doc, 8, 0)) + d, _ := to_dict(cf_parse(doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 >>")) + a := new_array(doc.allocator, 2) + array_push(a, Name("ICCBased")) + array_push(a, icc) + dict_set(d, "ColorSpace", a) + + payload := im_arena(doc, {255, 0, 0, 0, 128, 200, 3, 3, 3}) + img, _ := image_load(doc, new_stream(d, payload, doc.allocator), nil) + got := im_pixels(t, doc, img) + + plain := im_decode(t, doc, + "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", payload) + for x in 0 ..< 3 { + testing.expectf(t, im_rgba(got, x, 0) == im_rgba(plain, x, 0), + "ICCBased pixel %d: %v vs DeviceRGB %v", x, + im_rgba(got, x, 0), im_rgba(plain, x, 0)) + } +} + +@(test) +test_image_calrgb_with_gamma_is_not_the_identity :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A CalRGB with /Gamma 2.2 does change its components, so the direct-copy + // fast path must not be taken for it. If the probe were replaced by a + // "kind is Cal_RGB, close enough" test this pixel would come back as 128. + bmp := im_decode(t, doc, + "<< /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace " + + "[/CalRGB << /WhitePoint [0.9505 1.0 1.089] /Gamma [2.2 2.2 2.2] >>] >>", + im_arena(doc, {128, 128, 128})) + got := im_rgba(bmp, 0, 0) + testing.expectf(t, got[0] != 128, "CalRGB gamma must be applied, got %v", got) +} + +@(test) +test_image_general_path_matches_the_lut :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 16 bits per component takes the general per-pixel path; 8 bits takes the + // 256-entry table. Feeding both the same values must give the same answer. + lut := im_decode(t, doc, + "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace 5 0 R >>", + im_arena(doc, {0, 128, 255})) + // 0x0000, 0x8080, 0xFFFF are the same three tints at 16 bits. + general := im_decode(t, doc, + "<< /Width 3 /Height 1 /BitsPerComponent 16 /ColorSpace 5 0 R >>", + im_arena(doc, {0, 0, 0x80, 0x80, 0xFF, 0xFF})) + for x in 0 ..< 3 { + a := im_rgba(lut, x, 0) + b := im_rgba(general, x, 0) + worst := 0 + for i in 0 ..< 4 { + worst = max(worst, abs(int(a[i]) - int(b[i]))) + } + testing.expectf(t, worst <= 1, "pixel %d: table %v vs general %v", x, a, b) + } +} + +// --------------------------------------------------------------------------- +// Odds and ends +// --------------------------------------------------------------------------- + +@(test) +test_image_interpolate_is_recorded :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + off := im_load(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) + testing.expect(t, off != nil && !off.interpolate, "/Interpolate defaults to false") + + on := im_load(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate true >>", + GRAY8_DATA[:]) + testing.expect(t, on != nil && on.interpolate, "/Interpolate true") + + // The inline spelling is /I. + d, _ := to_dict(cf_parse(doc, "<< /W 4 /H 2 /BPC 8 /CS /G /I true >>")) + inl, _ := image_load_inline(doc, d, GRAY8_DATA[:], nil) + testing.expect(t, inl != nil && inl.interpolate, "/I true") +} + +@(test) +test_image_size_reports_pixels :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + img := im_load(t, doc, + "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) + w, h := image_size(img) + testing.expect_value(t, w, 4) + testing.expect_value(t, h, 2) +} + +@(test) +test_image_real_valued_dimensions :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // "/Width 4.0" occurs; rejecting it would lose the image over nothing. + img := im_load(t, doc, + "<< /Width 4.0 /Height 2.0 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + GRAY8_DATA[:]) + testing.expect(t, img != nil, "a real-valued /Width must be accepted") + if img == nil { return } + testing.expect_value(t, img.width, 4) + expect_rgba(t, im_pixels(t, doc, img), 3, 1, {40, 40, 40, 255}, "real /Width") +} + +@(test) +test_image_non_stencil_mask_is_taken_as_one :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // 8.9.6.4 requires the stencil to be an /ImageMask; producers write a + // 1-bit DeviceGray. The samples mean the same thing, so it is read anyway. + stencil := cf_stream(doc, + "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", GRAY1_DATA[:]) + d, _ := to_dict(cf_parse(doc, + "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + dict_set(d, "Mask", stencil) + img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 60), doc.allocator), nil) + testing.expect(t, img != nil && img.mask != nil, "the mask must still load") + + bmp := im_pixels(t, doc, img) + expect_rgba(t, bmp, 0, 0, {60, 60, 60, 0}, "bit 1 masks out") + expect_rgba(t, bmp, 1, 0, {60, 60, 60, 255}, "bit 0 paints") +} + +@(test) +test_image_non_stencil_mask_leaves_the_shared_image_alone :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // One stream used both ways, which is what a page holding Im1 and an Im2 + // whose /Mask names Im1 does. Reading it as a stencil for the masking use + // is right; writing that decision back onto the Image the by-stream cache + // holds is what makes the direct `Do` of it decode as a one-channel + // stencil instead of the greyscale picture it is. + shared := cf_stream(doc, + "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena(doc, {0b1010_1010, 0, 255, 0x11, 0x22, 0x33, 0x44, 0x55})) + pic, _ := image_load(doc, shared, nil) + testing.expect(t, pic != nil, "the picture must load") + if pic == nil { return } + + first := im_pixels(t, doc, pic) + testing.expect_value(t, first.n, 4) + expect_rgba(t, first, 0, 0, {170, 170, 170, 255}, "greyscale before the mask loads") + expect_rgba(t, first, 2, 0, {255, 255, 255, 255}, "greyscale before the mask loads") + + d, _ := to_dict(cf_parse(doc, + "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) + dict_set(d, "Mask", shared) + masked, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 60), doc.allocator), nil) + testing.expect(t, masked != nil && masked.mask != nil, "the stencil /Mask must load") + if masked == nil || masked.mask == nil { return } + + testing.expect(t, masked.mask != pic, "the stencil reading is a second Image") + testing.expect(t, masked.mask.is_mask, "and it IS read as a stencil") + testing.expect_value(t, masked.mask.bpc, 1) + testing.expect(t, !pic.is_mask, "the picture must not have become a stencil") + testing.expect_value(t, pic.bpc, 8) + + // Read one bit deep the first byte is the whole 8-pixel row: 0b10101010, + // so a 1 bit masks out and a 0 bit paints, alternately. + mbmp := im_pixels(t, doc, masked) + expect_rgba(t, mbmp, 0, 0, {60, 60, 60, 0}, "bit 1 masks out") + expect_rgba(t, mbmp, 1, 0, {60, 60, 60, 255}, "bit 0 paints") + + // And the picture still decodes as colour when it is decoded again from + // scratch, which is where a flipped is_mask shows up even if the cached + // bitmap happened to survive. + doc.img_cache.budget = 1 + other := im_load(t, doc, + "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", + im_arena_fill(doc, 16, 9)) + im_pixels(t, doc, other) + testing.expect(t, !pic.decoded, "the picture should have been evicted") + again := im_pixels(t, doc, pic) + testing.expect_value(t, again.n, 4) + expect_rgba(t, again, 0, 0, {170, 170, 170, 255}, "still greyscale after re-decoding") + expect_rgba(t, again, 2, 0, {255, 255, 255, 255}, "still greyscale after re-decoding") +} + +@(test) +test_image_color_key_wrong_length_is_dropped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Three entries for a three-component space needs six. + img := im_load(t, doc, + "<< /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Mask [0 0 0] >>", + im_arena(doc, {0, 0, 0})) + testing.expect(t, img != nil && !img.has_color_key, "a short /Mask array is ignored") + expect_rgba(t, im_pixels(t, doc, img), 0, 0, {0, 0, 0, 255}, "still opaque") +} + +@(test) +test_image_flate_encoded_samples :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A stored (uncompressed) deflate block wrapped in a zlib header, so the + // byte-filter half of stream_image_data is exercised too. + // Built by hand: 78 01, then BFINAL=1 BTYPE=00, LEN=4, NLEN=~4, payload, + // then the Adler-32 of {10,20,30,40}. + raw := [?]byte{ + 0x78, 0x01, 0x01, 0x04, 0x00, 0xFB, 0xFF, 10, 20, 30, 40, + 0x00, 0x63, 0x00, 0x1B, + } + bmp := im_decode(t, doc, + "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode >>", + im_arena(doc, raw[:])) + expect_rgba(t, bmp, 0, 0, {10, 10, 10, 255}, "flate") + expect_rgba(t, bmp, 1, 0, {20, 20, 20, 255}, "flate") + expect_rgba(t, bmp, 0, 1, {30, 30, 30, 255}, "flate") + expect_rgba(t, bmp, 1, 1, {40, 40, 40, 255}, "flate") +} + +@(test) +test_image_filter_chain_stops_at_the_image_filter :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // /Filter [/ASCIIHexDecode /DCTDecode]: the hex is a byte filter and is + // applied here, the JPEG is an image filter and is handed to the decoder + // with the image dictionary. Doing it the other way round -- running the + // whole chain first -- would leave nothing but a bag of bytes. + hex := make([]byte, len(JPEG_T_GREY8) * 2 + 1, doc.allocator) + digits := "0123456789ABCDEF" + for b, i in JPEG_T_GREY8 { + hex[i * 2] = digits[b >> 4] + hex[i * 2 + 1] = digits[b & 15] + } + hex[len(hex) - 1] = '>' + + bmp := im_decode(t, doc, + "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceGray " + + "/Filter [/ASCIIHexDecode /DCTDecode] >>", hex) + if bmp == nil { return } + for y in 0 ..< 8 { + for x in 0 ..< 8 { + v := JPEG_T_GREY8_PIXELS[y * 8 + x] + expect_rgba_near(t, bmp, x, y, {v, v, v, 255}, 4, "hex over JPEG") + } + } +} diff --git a/lexer.odin b/lexer.odin new file mode 100644 index 0000000..c355820 --- /dev/null +++ b/lexer.odin @@ -0,0 +1,498 @@ +package pdf + +import "core:mem" +import "core:strings" + +// The COS tokeniser (ISO 32000-2, 7.2). +// +// It is deliberately total: every call to `lexer_next` either advances `pos` +// or returns an EOF token, so no input -- valid, truncated or random -- can +// make a caller spin. Nothing here rejects input either; the malformations +// this layer meets (a string that never closes, a hex digit missing its pair, +// a number that is three signs and a dot) are all common enough in real files +// that failing on them would mean refusing documents every viewer opens. The +// damage is reported through `Token.truncated` and through the values +// themselves, and the parser turns that into a `Repair`. + +// --------------------------------------------------------------------------- +// Character classes (ISO 32000-2, 7.2.3, tables 1 and 2) +// --------------------------------------------------------------------------- + +is_pdf_whitespace :: proc "contextless" (c: byte) -> bool { + switch c { + case 0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20: return true + } + return false +} + +is_pdf_delimiter :: proc "contextless" (c: byte) -> bool { + switch c { + case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%': return true + } + return false +} + +// A regular character is anything that is neither whitespace nor a delimiter; +// names, numbers and keywords are runs of them. +is_pdf_regular :: proc "contextless" (c: byte) -> bool { + return !is_pdf_whitespace(c) && !is_pdf_delimiter(c) +} + +// --------------------------------------------------------------------------- +// Tokens +// --------------------------------------------------------------------------- + +Token_Kind :: enum u8 { + EOF, + Integer, + Real, + String, + Name, + Array_Open, + Array_Close, + Dict_Open, + Dict_Close, + Brace_Open, // only meaningful inside a type 4 (PostScript) function + Brace_Close, + Keyword, // obj, endobj, stream, R, true, false, null, operators + Error, // a stray delimiter; one byte was consumed +} + +Token :: struct { + kind: Token_Kind, + // Byte range of the token in the lexer's input: [start, end). + start: int, + end: int, + // Both numeric fields are filled for both numeric kinds, so a caller that + // wants "a number" never has to switch on the kind. A value that does not + // fit, or that parsed to an infinity, reads as 0 -- see `parse_number`. + int_value: i64, + real_value: f64, + // Names (with #xx resolved) and keywords. Borrows from the input unless + // the name contained an escape, in which case it was allocated with the + // allocator passed to `lexer_next`. + text: string, + // String objects, unescaped and hex-unpacked. Always allocated with the + // allocator passed to `lexer_next` (never a view into the input): a + // decryption pass may have to rewrite these bytes in place, and the input + // is the file itself. + bytes: []byte, + // The token ran into the end of the input before its closing delimiter. + // The value is still usable -- this is a repair, not an error. + truncated: bool, +} + +Lexer :: struct { + data: []byte, + pos: int, +} + +lexer_make :: proc(data: []byte) -> Lexer { + return Lexer{data = data, pos = 0} +} + +// lexer_skip_whitespace steps over whitespace and comments. A '%' outside of a +// string runs to the end of the line and is whitespace for every purpose +// (ISO 32000-2, 7.2.4). +lexer_skip_whitespace :: proc(lex: ^Lexer) { + for lex.pos < len(lex.data) { + c := lex.data[lex.pos] + if is_pdf_whitespace(c) { + lex.pos += 1 + continue + } + if c == '%' { + lex.pos += 1 + for lex.pos < len(lex.data) { + d := lex.data[lex.pos] + if d == '\r' || d == '\n' { break } + lex.pos += 1 + } + continue + } + return + } +} + +// lexer_next returns the next token, advancing past it. +// +// `allocator` owns any bytes the token had to materialise: the payload of a +// string, and the text of a name that contained #xx escapes. Callers holding a +// Document pass `doc.allocator` so the token's memory dies with the Document. +lexer_next :: proc(lex: ^Lexer, allocator := context.allocator) -> Token { + lexer_skip_whitespace(lex) + if lex.pos >= len(lex.data) { + return Token{kind = .EOF, start = lex.pos, end = lex.pos} + } + + start := lex.pos + c := lex.data[start] + + switch c { + case '[': + lex.pos += 1 + return Token{kind = .Array_Open, start = start, end = lex.pos} + case ']': + lex.pos += 1 + return Token{kind = .Array_Close, start = start, end = lex.pos} + case '{': + lex.pos += 1 + return Token{kind = .Brace_Open, start = start, end = lex.pos} + case '}': + lex.pos += 1 + return Token{kind = .Brace_Close, start = start, end = lex.pos} + case '(': + return lex_literal_string(lex, allocator) + case '/': + return lex_name(lex, allocator) + case '<': + if start + 1 < len(lex.data) && lex.data[start + 1] == '<' { + lex.pos += 2 + return Token{kind = .Dict_Open, start = start, end = lex.pos} + } + return lex_hex_string(lex, allocator) + case '>': + if start + 1 < len(lex.data) && lex.data[start + 1] == '>' { + lex.pos += 2 + return Token{kind = .Dict_Close, start = start, end = lex.pos} + } + // A lone '>' is junk. Consume it so the caller makes progress. + lex.pos += 1 + return Token{kind = .Error, start = start, end = lex.pos} + case ')': + lex.pos += 1 + return Token{kind = .Error, start = start, end = lex.pos} + case '+', '-', '.', '0' ..= '9': + return lex_number(lex) + } + + if is_pdf_regular(c) { + return lex_keyword(lex) + } + + // Unreachable for the delimiters above, but a delimiter added to the class + // table later must still consume a byte rather than stall. + lex.pos += 1 + return Token{kind = .Error, start = start, end = lex.pos} +} + +// lexer_next_structural returns the next token without ever allocating: the +// kinds whose value has to be materialised -- strings, and names carrying #xx +// escapes -- come back with an empty payload. Their `kind`, `start` and `end` +// are still right, which is all a structural probe looks at. +// +// The probes that hunt for "num gen obj" or an xref subsection line accept +// integers and keywords only, and in a damaged file they run at every +// plausible offset and reject nearly all of them. Lexing those rejects with a +// real allocator copies the string bodies they step over -- a file reading +// "1 (" at a hundred probed offsets would copy its own tail a hundred times, +// and whichever of those copies is not attached to a Document leaks outright. +lexer_next_structural :: proc(lex: ^Lexer) -> Token { + return lexer_next(lex, mem.nil_allocator()) +} + +// lexer_peek returns the next token without consuming it. It may allocate +// exactly as `lexer_next` does, so peeking at a string costs a copy; the +// parser keeps its own token queue instead of peeking for that reason. +lexer_peek :: proc(lex: ^Lexer, allocator := context.allocator) -> Token { + save := lex.pos + tok := lexer_next(lex, allocator) + lex.pos = save + return tok +} + +// lexer_at_keyword reports whether the next token is exactly `kw`, without +// consuming anything. Used for the keywords that are found by position rather +// than parsed -- `stream`, `endstream`, `endobj`. +lexer_at_keyword :: proc(lex: ^Lexer, kw: string) -> bool { + save := lex.pos + defer lex.pos = save + lexer_skip_whitespace(lex) + return lexer_match_here(lex, kw) +} + +// lexer_accept_keyword consumes `kw` if it is next, and reports whether it did. +lexer_accept_keyword :: proc(lex: ^Lexer, kw: string) -> bool { + save := lex.pos + lexer_skip_whitespace(lex) + if lexer_match_here(lex, kw) { + lex.pos += len(kw) + return true + } + lex.pos = save + return false +} + +@(private = "file") +lexer_match_here :: proc(lex: ^Lexer, kw: string) -> bool { + end := lex.pos + len(kw) + if end > len(lex.data) { return false } + if string(lex.data[lex.pos:end]) != kw { return false } + // "stream" must not match the first six bytes of "streamable". + if end < len(lex.data) && is_pdf_regular(lex.data[end]) { return false } + return true +} + +// lexer_skip_eol steps over one end-of-line marker: CRLF, LF or a bare CR. +// The bare CR is not legal after `stream` but is written by enough producers +// that rejecting it would fail real documents (ISO 32000-2, 7.3.8.1). +lexer_skip_eol :: proc(lex: ^Lexer) { + if lex.pos < len(lex.data) && lex.data[lex.pos] == '\r' { + lex.pos += 1 + } + if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' { + lex.pos += 1 + } +} + +// --------------------------------------------------------------------------- +// Numbers +// --------------------------------------------------------------------------- + +@(private = "file") +lex_number :: proc(lex: ^Lexer) -> Token { + start := lex.pos + for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) { + lex.pos += 1 + } + text := string(lex.data[start:lex.pos]) + + tok := Token{start = start, end = lex.pos} + // parse_number never reports failure as an exception: a number a viewer + // would read as 0 must not abort the object around it. + value, _ := parse_number(text) + #partial switch v in value { + case i64: + tok.kind = .Integer + tok.int_value = v + tok.real_value = f64(v) + case f64: + tok.kind = .Real + tok.real_value = clamp_finite(v, 0) + tok.int_value = f64_to_i64_saturating(tok.real_value) + case: + tok.kind = .Integer + } + return tok +} + +// f64_to_i64_saturating clamps before converting: an out-of-range float to +// integer conversion is undefined behaviour, and /Length values of 1e300 exist. +@(private) +f64_to_i64_saturating :: proc "contextless" (v: f64) -> i64 { + if !is_finite(v) { return 0 } + if v >= 9.2e18 { return max(i64) } + if v <= -9.2e18 { return min(i64) } + return i64(v) +} + +// --------------------------------------------------------------------------- +// Names (ISO 32000-2, 7.3.5) +// --------------------------------------------------------------------------- + +@(private = "file") +lex_name :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token { + start := lex.pos + lex.pos += 1 // the '/' + body := lex.pos + for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) { + lex.pos += 1 + } + raw := string(lex.data[body:lex.pos]) + // A bare '/' is the empty name, which is a legal object. + return Token{ + kind = .Name, + start = start, + end = lex.pos, + text = decode_name_escapes(raw, allocator), + } +} + +// --------------------------------------------------------------------------- +// Keywords +// --------------------------------------------------------------------------- + +@(private = "file") +lex_keyword :: proc(lex: ^Lexer) -> Token { + start := lex.pos + for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) { + lex.pos += 1 + } + return Token{ + kind = .Keyword, + start = start, + end = lex.pos, + text = string(lex.data[start:lex.pos]), + } +} + +// --------------------------------------------------------------------------- +// Literal strings (ISO 32000-2, 7.3.4.2) +// --------------------------------------------------------------------------- + +@(private = "file") +lex_literal_string :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token { + start := lex.pos + lex.pos += 1 // the '(' + + // The unescaped form is never longer than the escaped one, so one + // allocation of the remaining length is always enough and never wrong. + // Size the buffer from the raw body once, up front: the unescaped value is + // never longer than the bytes it came from, so this both fits and cannot + // over-allocate on a file that claims a huge string it does not contain. + b := strings.builder_make(0, literal_body_len(lex.data, lex.pos), allocator) + depth := 1 + truncated := true + + scan: for lex.pos < len(lex.data) { + c := lex.data[lex.pos] + lex.pos += 1 + switch c { + case '\\': + if lex.pos >= len(lex.data) { break scan } + e := lex.data[lex.pos] + lex.pos += 1 + switch e { + case 'n': strings.write_byte(&b, '\n') + case 'r': strings.write_byte(&b, '\r') + case 't': strings.write_byte(&b, '\t') + case 'b': strings.write_byte(&b, '\b') + case 'f': strings.write_byte(&b, '\f') + case '(': strings.write_byte(&b, '(') + case ')': strings.write_byte(&b, ')') + case '\\': strings.write_byte(&b, '\\') + case '\r': + // Backslash before an EOL is a line continuation: both the + // backslash and the EOL disappear. + if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' { + lex.pos += 1 + } + case '\n': + // continuation + case '0' ..= '7': + value := int(e - '0') + for _ in 0 ..< 2 { + if lex.pos >= len(lex.data) { break } + d := lex.data[lex.pos] + if d < '0' || d > '7' { break } + value = value * 8 + int(d - '0') + lex.pos += 1 + } + strings.write_byte(&b, byte(value & 0xFF)) + case: + // "\q" is just "q": the backslash is dropped, the byte kept. + strings.write_byte(&b, e) + } + case '(': + depth += 1 + strings.write_byte(&b, '(') + case ')': + depth -= 1 + if depth == 0 { + truncated = false + break scan + } + strings.write_byte(&b, ')') + case '\r': + // A raw CR, LF or CRLF in the source is one LF in the value. + if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' { + lex.pos += 1 + } + strings.write_byte(&b, '\n') + case: + strings.write_byte(&b, c) + } + } + + return Token{ + kind = .String, + start = start, + end = lex.pos, + bytes = transmute([]byte)strings.to_string(b), + truncated = truncated, + } +} + +// --------------------------------------------------------------------------- +// Hex strings (ISO 32000-2, 7.3.4.3) +// --------------------------------------------------------------------------- + +@(private = "file") +lex_hex_string :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token { + start := lex.pos + lex.pos += 1 // the '<' + + b := strings.builder_make(0, hex_body_len(lex.data, lex.pos) / 2 + 1, allocator) + nibble: byte + have_nibble := false + truncated := true + + for lex.pos < len(lex.data) { + c := lex.data[lex.pos] + lex.pos += 1 + if c == '>' { + truncated = false + break + } + v, ok := hex_value(c) + // Whitespace is defined to be skipped here, and other junk is skipped + // too: files with a stray '<' or a UTF-8 quote inside a hex string are + // repaired by every viewer rather than rejected. + if !ok { continue } + if have_nibble { + strings.write_byte(&b, nibble << 4 | v) + have_nibble = false + } else { + nibble = v + have_nibble = true + } + } + // "An odd number of digits: the final digit is assumed to be followed by + // a zero." (7.3.4.3) + if have_nibble { + strings.write_byte(&b, nibble << 4) + } + + return Token{ + kind = .String, + start = start, + end = lex.pos, + bytes = transmute([]byte)strings.to_string(b), + truncated = truncated, + } +} + +// literal_body_len counts the raw bytes of a literal string body starting at +// `pos` (just past the '('), stopping at the balancing ')' or at EOF. It is an +// upper bound on the decoded length, which is what the value buffer is sized +// from. +@(private = "file") +literal_body_len :: proc(data: []byte, pos: int) -> int { + depth := 1 + i := pos + for i < len(data) { + switch data[i] { + case '\\': + i += 1 // whatever follows is data, never a delimiter + case '(': + depth += 1 + case ')': + depth -= 1 + if depth == 0 { + return i - pos + } + } + i += 1 + } + return len(data) - pos +} + +@(private = "file") +hex_body_len :: proc(data: []byte, pos: int) -> int { + for i := pos; i < len(data); i += 1 { + if data[i] == '>' { + return i - pos + } + } + return len(data) - pos +} diff --git a/lexer_test.odin b/lexer_test.odin new file mode 100644 index 0000000..bbe9d2e --- /dev/null +++ b/lexer_test.odin @@ -0,0 +1,525 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:slice" +import "core:testing" + +// A scratch arena keeps the tokens' allocations together and out of the test +// runner's leak tracker, which is exactly how the library uses the parser. +@(private = "file") +scratch_begin :: proc(t: ^testing.T, a: ^virtual.Arena) -> mem.Allocator { + err := virtual.arena_init_growing(a) + testing.expect(t, err == nil, "arena init failed") + return virtual.arena_allocator(a) +} + +// Odin will not transmute an untyped literal, and every test here starts from +// one. +@(private = "file") +bytes_of :: proc(s: string) -> []byte { + return transmute([]byte)s +} + +@(private = "file") +lex_all :: proc(src: string, allocator: mem.Allocator, out: ^[dynamic]Token) { + lex := lexer_make(transmute([]byte)src) + for { + tok := lexer_next(&lex, allocator) + if tok.kind == .EOF { return } + append(out, tok) + if len(out) > 4096 { return } + } +} + +// --------------------------------------------------------------------------- +// Structure tokens +// --------------------------------------------------------------------------- + +@(test) +test_lexer_structure_tokens :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + kinds: []Token_Kind, + } + cases := []Case{ + {"", {}}, + {" \r\n\t\x00\f ", {}}, + {"[]", {.Array_Open, .Array_Close}}, + {"<<>>", {.Dict_Open, .Dict_Close}}, + {"{}", {.Brace_Open, .Brace_Close}}, + {"[ 1 2 3 ]", {.Array_Open, .Integer, .Integer, .Integer, .Array_Close}}, + {"<>", {.Dict_Open, .Name, .Name, .Dict_Close}}, + // A hex string is not a dictionary, and "<<" is not two hex strings. + {"<41>", {.String}}, + {"<< <41> >>", {.Dict_Open, .String, .Dict_Close}}, + // Stray closers are Error tokens, and each consumes exactly one byte. + {">", {.Error}}, + {")", {.Error}}, + {"))", {.Error, .Error}}, + {"1 0 obj", {.Integer, .Integer, .Keyword}}, + {"true false null", {.Keyword, .Keyword, .Keyword}}, + // '{' and '}' only mean anything in a type 4 function, but must + // tokenise wherever they appear. + {"{ dup 0.5 mul }", {.Brace_Open, .Keyword, .Real, .Keyword, .Brace_Close}}, + } + + toks := make([dynamic]Token, alloc) + for c in cases { + clear(&toks) + lex_all(c.src, alloc, &toks) + testing.expectf(t, len(toks) == len(c.kinds), + "%q: got %d tokens, want %d", c.src, len(toks), len(c.kinds)) + if len(toks) != len(c.kinds) { continue } + for k, i in c.kinds { + testing.expectf(t, toks[i].kind == k, + "%q: token %d is %v, want %v", c.src, i, toks[i].kind, k) + } + } +} + +@(test) +test_lexer_token_spans :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + src := " /Name 42 (str) " + lex := lexer_make(transmute([]byte)src) + + n := lexer_next(&lex, alloc) + testing.expect_value(t, n.start, 2) + testing.expect_value(t, n.end, 7) + + i := lexer_next(&lex, alloc) + testing.expect_value(t, i.start, 8) + testing.expect_value(t, i.end, 10) + + s := lexer_next(&lex, alloc) + testing.expect_value(t, s.start, 11) + testing.expect_value(t, s.end, 16) + + e := lexer_next(&lex, alloc) + testing.expect_value(t, e.kind, Token_Kind.EOF) + testing.expect_value(t, e.start, len(src)) +} + +// --------------------------------------------------------------------------- +// Comments (ISO 32000-2, 7.2.4) +// --------------------------------------------------------------------------- + +@(test) +test_lexer_comments :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + toks := make([dynamic]Token, alloc) + lex_all("% comment\n1 % another\n2%trailing, no EOL", alloc, &toks) + testing.expect_value(t, len(toks), 2) + if len(toks) == 2 { + testing.expect_value(t, toks[0].int_value, i64(1)) + testing.expect_value(t, toks[1].int_value, i64(2)) + } + + // A '%' inside a string is data, not a comment. + clear(&toks) + lex_all("(50% off)", alloc, &toks) + testing.expect_value(t, len(toks), 1) + if len(toks) == 1 { + testing.expect(t, slice.equal(toks[0].bytes, transmute([]byte)string("50% off")), + "'%' inside a literal string must not start a comment") + } + + // A comment ends the token before it without whitespace in between. + clear(&toks) + lex_all("/Name%c\n/Other", alloc, &toks) + testing.expect_value(t, len(toks), 2) + if len(toks) == 2 { + testing.expect_value(t, toks[0].text, "Name") + testing.expect_value(t, toks[1].text, "Other") + } +} + +// --------------------------------------------------------------------------- +// Numbers (ISO 32000-2, 7.3.3) +// --------------------------------------------------------------------------- + +@(test) +test_lexer_numbers :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + kind: Token_Kind, + i: i64, + r: f64, + } + cases := []Case{ + // The spec's own examples, table 2. + {"123", .Integer, 123, 123}, + {"43445", .Integer, 43445, 43445}, + {"+17", .Integer, 17, 17}, + {"-98", .Integer, -98, -98}, + {"0", .Integer, 0, 0}, + {"34.5", .Real, 34, 34.5}, + {"-3.62", .Real, -3, -3.62}, + {"+123.6", .Real, 123, 123.6}, + {"4.", .Real, 4, 4}, + {"-.002", .Real, 0, -0.002}, + {"0.0", .Real, 0, 0}, + // Shapes that only real files produce. + {".5", .Real, 0, 0.5}, + {".", .Real, 0, 0}, + {"1e2", .Real, 100, 100}, + {"-2.5e-2", .Real, 0, -0.025}, + // Nothing here is allowed to fail; an unreadable number is 0. + {"--5", .Integer, 0, 0}, + {"1.2.3", .Real, 0, 0}, + {"-", .Integer, 0, 0}, + {"+", .Integer, 0, 0}, + // An exponent that overflows must not leak an infinity into geometry. + {"1e400", .Real, 0, 0}, + {"-1e400", .Real, 0, 0}, + } + + for c in cases { + lex := lexer_make(transmute([]byte)c.src) + tok := lexer_next(&lex, alloc) + testing.expectf(t, tok.kind == c.kind, "%q: kind %v, want %v", c.src, tok.kind, c.kind) + testing.expectf(t, tok.int_value == c.i, "%q: int %d, want %d", c.src, tok.int_value, c.i) + testing.expectf(t, tok.real_value == c.r, "%q: real %f, want %f", c.src, tok.real_value, c.r) + testing.expectf(t, is_finite(tok.real_value), "%q: real value is not finite", c.src) + testing.expectf(t, tok.end == len(c.src), "%q: consumed %d of %d bytes", c.src, tok.end, len(c.src)) + } +} + +@(test) +test_lexer_number_stops_at_delimiter :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + lex := lexer_make(bytes_of("12/Name")) + a := lexer_next(&lex, alloc) + testing.expect_value(t, a.kind, Token_Kind.Integer) + testing.expect_value(t, a.int_value, i64(12)) + b := lexer_next(&lex, alloc) + testing.expect_value(t, b.kind, Token_Kind.Name) + testing.expect_value(t, b.text, "Name") +} + +// --------------------------------------------------------------------------- +// Names (ISO 32000-2, 7.3.5, table 4) +// --------------------------------------------------------------------------- + +@(test) +test_lexer_names :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: string, + } + cases := []Case{ + {"/Name1", "Name1"}, + {"/ASomewhatLongerName", "ASomewhatLongerName"}, + {"/A;Name_With-Various***Characters?", "A;Name_With-Various***Characters?"}, + {"/1.2", "1.2"}, + {"/$$", "$$"}, + {"/@pattern", "@pattern"}, + {"/.notdef", ".notdef"}, + {"/lime#20Green", "lime Green"}, + {"/paired#28#29parentheses", "paired()parentheses"}, + {"/The_Key_of_F#23_Minor", "The_Key_of_F#_Minor"}, + {"/A#42", "AB"}, + // A bare '/' is the empty name, which is legal. + {"/", ""}, + {"/ ", ""}, + {"/[", ""}, + // A '#' that is not followed by two hex digits stays as written. + {"/Bad#", "Bad#"}, + {"/Bad#zz", "Bad#zz"}, + {"/Bad#4", "Bad#4"}, + } + + for c in cases { + lex := lexer_make(transmute([]byte)c.src) + tok := lexer_next(&lex, alloc) + testing.expectf(t, tok.kind == .Name, "%q: kind %v", c.src, tok.kind) + testing.expectf(t, tok.text == c.want, "%q: got %q, want %q", c.src, tok.text, c.want) + } +} + +// --------------------------------------------------------------------------- +// Literal strings (ISO 32000-2, 7.3.4.2) +// --------------------------------------------------------------------------- + +@(test) +test_lexer_literal_strings :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: string, + truncated: bool, + } + cases := []Case{ + {"(This is a string)", "This is a string", false}, + {"()", "", false}, + // Balanced unescaped parentheses nest. + {"(Strings may contain balanced parentheses ( ) and\nspecial characters (*!&}^% and so on).)", + "Strings may contain balanced parentheses ( ) and\nspecial characters (*!&}^% and so on).", false}, + {"(((deep)))", "((deep))", false}, + {"(These \\(brackets\\) work)", "These (brackets) work", false}, + // Every escape in table 3. + {"(a\\nb)", "a\nb", false}, + {"(a\\rb)", "a\rb", false}, + {"(a\\tb)", "a\tb", false}, + {"(a\\bb)", "a\bb", false}, + {"(a\\fb)", "a\fb", false}, + {"(a\\(b)", "a(b", false}, + {"(a\\)b)", "a)b", false}, + {"(a\\\\b)", "a\\b", false}, + // Octal, 1 to 3 digits. \0053 is \005 followed by '3'. + {"(\\53)", "+", false}, + {"(\\053)", "+", false}, + {"(\\0053)", "\x053", false}, + {"(\\401)", "\x01", false}, // 0401 & 0xFF + {"(\\0)", "\x00", false}, + // A backslash before anything else is dropped, the byte kept. + {"(\\8)", "8", false}, + {"(\\q)", "q", false}, + // A backslash before an EOL is a line continuation. + {"(line1\\\nline2)", "line1line2", false}, + {"(line1\\\r\nline2)", "line1line2", false}, + {"(line1\\\rline2)", "line1line2", false}, + // A raw CR, LF or CRLF in the source is a single LF in the value. + {"(a\nb)", "a\nb", false}, + {"(a\rb)", "a\nb", false}, + {"(a\r\nb)", "a\nb", false}, + {"(a\n\rb)", "a\n\nb", false}, + // Unterminated: the value survives, the damage is flagged. + {"(abc", "abc", true}, + {"(abc\\", "abc", true}, + {"((abc)", "(abc)", true}, + {"(", "", true}, + } + + for c in cases { + lex := lexer_make(transmute([]byte)c.src) + tok := lexer_next(&lex, alloc) + testing.expectf(t, tok.kind == .String, "%q: kind %v", c.src, tok.kind) + testing.expectf(t, slice.equal(tok.bytes, transmute([]byte)c.want), + "%q: got %q, want %q", c.src, string(tok.bytes), c.want) + testing.expectf(t, tok.truncated == c.truncated, + "%q: truncated %v, want %v", c.src, tok.truncated, c.truncated) + testing.expectf(t, lex.pos <= len(c.src), "%q: lexer ran past the end", c.src) + } +} + +@(test) +test_lexer_string_stops_where_it_should :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // The escaped ')' must not close the string, and the token must end + // exactly at the real closer. + src := "(a\\)b) 7" + lex := lexer_make(transmute([]byte)src) + s := lexer_next(&lex, alloc) + testing.expect_value(t, s.kind, Token_Kind.String) + testing.expect_value(t, s.end, 6) + n := lexer_next(&lex, alloc) + testing.expect_value(t, n.kind, Token_Kind.Integer) + testing.expect_value(t, n.int_value, i64(7)) +} + +// --------------------------------------------------------------------------- +// Hex strings (ISO 32000-2, 7.3.4.3) +// --------------------------------------------------------------------------- + +@(test) +test_lexer_hex_strings :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: string, + truncated: bool, + } + cases := []Case{ + {"<4E6F762073686D6F7A206B6120706F702E>", "Nov shmoz ka pop.", false}, + {"<901FA3>", "\x90\x1f\xa3", false}, + // "If the final digit is missing it is assumed to be 0." + {"<901FA>", "\x90\x1f\xa0", false}, + {"<>", "", false}, + {"", "\xa0", false}, + // Case-insensitive, whitespace anywhere. + {"<4a4B>", "JK", false}, + {"<4 1 4 2>", "AB", false}, + {"<41\n42\r43>", "ABC", false}, + // Junk between digits is skipped rather than failing the string. + {"<41-42>", "AB", false}, + {"<41zz42>", "AB", false}, + // Unterminated runs to the end of the input. + {"<4142", "AB", true}, + {"<", "", true}, + } + + for c in cases { + lex := lexer_make(transmute([]byte)c.src) + tok := lexer_next(&lex, alloc) + testing.expectf(t, tok.kind == .String, "%q: kind %v", c.src, tok.kind) + testing.expectf(t, slice.equal(tok.bytes, transmute([]byte)c.want), + "%q: got %x, want %x", c.src, tok.bytes, transmute([]byte)c.want) + testing.expectf(t, tok.truncated == c.truncated, + "%q: truncated %v, want %v", c.src, tok.truncated, c.truncated) + } +} + +// --------------------------------------------------------------------------- +// Keyword helpers +// --------------------------------------------------------------------------- + +@(test) +test_lexer_keyword_helpers :: proc(t: ^testing.T) { + lex := lexer_make(bytes_of(" stream\r\ndata")) + testing.expect(t, lexer_at_keyword(&lex, "stream"), "at_keyword should match past whitespace") + testing.expect_value(t, lex.pos, 0) // and must not consume + testing.expect(t, !lexer_at_keyword(&lex, "endstream"), "wrong keyword must not match") + + // A keyword must not match a prefix of a longer regular-character run. + lex2 := lexer_make(bytes_of("streamable")) + testing.expect(t, !lexer_at_keyword(&lex2, "stream"), "prefix must not match") + + testing.expect(t, lexer_accept_keyword(&lex, "stream"), "accept should match") + testing.expect_value(t, lex.pos, 8) + lexer_skip_eol(&lex) + testing.expect_value(t, lex.pos, 10) + + // A failed accept leaves the position alone. + save := lex.pos + testing.expect(t, !lexer_accept_keyword(&lex, "endstream"), "accept should fail") + testing.expect_value(t, lex.pos, save) + + lex3 := lexer_make(bytes_of("\rx")) + lexer_skip_eol(&lex3) + testing.expect_value(t, lex3.pos, 1) // a bare CR is one EOL +} + +@(test) +test_lexer_peek :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + lex := lexer_make(bytes_of("/A /B")) + p1 := lexer_peek(&lex, alloc) + p2 := lexer_peek(&lex, alloc) + testing.expect_value(t, p1.text, "A") + testing.expect_value(t, p2.text, "A") + testing.expect_value(t, lex.pos, 0) + n := lexer_next(&lex, alloc) + testing.expect_value(t, n.text, "A") +} + +// --------------------------------------------------------------------------- +// Totality +// +// The one property the whole reader rests on: no input makes the lexer stall. +// --------------------------------------------------------------------------- + +@(private = "file") +Rng :: struct { + state: u64, +} + +// xorshift64*, so the corpus is identical on every run and every platform. +@(private = "file") +rng_next :: proc(r: ^Rng) -> u64 { + x := r.state + x ~= x >> 12 + x ~= x << 25 + x ~= x >> 27 + r.state = x + return x * 0x2545F4914F6CDD1D +} + +@(private = "file") +assert_lexer_terminates :: proc(t: ^testing.T, data: []byte, alloc: mem.Allocator, what: string) { + lex := lexer_make(data) + // Every token consumes at least one byte, so more iterations than there + // are bytes means the lexer stalled. + budget := len(data) + 2 + for _ in 0 ..< budget { + before := lex.pos + tok := lexer_next(&lex, alloc) + if tok.kind == .EOF { + testing.expectf(t, lex.pos >= before, "%s: EOF moved backwards", what) + return + } + testing.expectf(t, lex.pos > before, "%s: token %v did not advance at %d", what, tok.kind, before) + testing.expectf(t, tok.end >= tok.start && tok.end <= len(data), + "%s: token span [%d,%d) outside the input", what, tok.start, tok.end) + if lex.pos <= before { return } + } + testing.expectf(t, false, "%s: lexer did not reach EOF within %d tokens", what, budget) +} + +@(test) +test_lexer_is_total_on_random_bytes :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + r := Rng{state = 0x9E3779B97F4A7C15} + buf := make([]byte, 64, alloc) + for round in 0 ..< 400 { + n := int(rng_next(&r) % 64) + for i in 0 ..< n { + v := rng_next(&r) + // Half the corpus is drawn from the bytes that mean something, so + // the delimiters and escapes are hit far more often than 1/256. + if v & 1 == 0 { + syntax := "()<>[]{}/%\\ \t\r\n0123456789.+-eRobj" + buf[i] = syntax[int(v >> 8) % len(syntax)] + } else { + buf[i] = byte(v >> 8) + } + } + assert_lexer_terminates(t, buf[:n], alloc, "random") + _ = round + } +} + +@(test) +test_lexer_is_total_on_truncations :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + sample := transmute([]byte)string( + "<< /Type /Page /Parent 3 0 R /Contents [ 4 0 R ] /S (a\\(b) /H <4142> " + + "/N /A#20B /R [1 2 [3 [4]]] /X 1e3 >> stream\r\nxx\nendstream endobj %c\n") + for n in 0 ..= len(sample) { + assert_lexer_terminates(t, sample[:n], alloc, "truncation") + } + // And every suffix, which puts the lexer in the middle of every construct. + for n in 0 ..< len(sample) { + assert_lexer_terminates(t, sample[n:], alloc, "suffix") + } +} diff --git a/object.odin b/object.odin new file mode 100644 index 0000000..efdfd5b --- /dev/null +++ b/object.odin @@ -0,0 +1,474 @@ +package pdf + +import "core:strconv" +import "core:strings" + +// Ref is an indirect reference: "12 0 R". +// +// Object number 0 is reserved by the format for the head of the free list, so +// a Ref with num == 0 is the library's "no reference" value. +Ref :: struct { + num: u32, + gen: u16, +} + +// Name is a PDF name object with the leading '/' stripped and any #xx escapes +// already decoded, so `Name("Adobe Green")` is what a file wrote as +// `/Adobe#20Green`. +Name :: distinct string + +// String is a PDF string object as raw bytes: escapes resolved, hex pairs +// packed, and -- in an encrypted document -- already decrypted. It is *not* +// text. A string that is meant to be read by a human still has to go through +// `decode_text_string`, which handles the UTF-16BE and UTF-8 byte-order marks +// and the PDFDocEncoding fallback. +String :: distinct []byte + +// Array is a PDF array. Elements may be indirect references; use `array_get` +// rather than indexing directly when the element could be one. +// +// The struct wrapper is not decoration: `Object` holds a `^Array` and an array +// holds `Object`s, and Odin will not close that cycle through a bare +// `[dynamic]Object`. Going through `items` also keeps every read site honest +// about the fact that an element may be a reference. +Array :: struct { + items: [dynamic]Object, +} + +// Dict is a PDF dictionary, keyed by the decoded name without its '/'. +// +// A key that is present with a null value and a key that is absent mean the +// same thing in almost every part of the format, and this library does not +// distinguish them: nulls are dropped at parse time. +Dict :: map[string]Object + +// Stream is a PDF stream: a dictionary plus a run of bytes. +// +// `raw` is the encoded payload exactly as stored (but decrypted, if the +// document was encrypted). The decoded bytes are produced on demand by +// `stream_data`, which caches them here. +Stream :: struct { + dict: ^Dict, + raw: []byte, + // Cache filled in by `stream_data`. `state` distinguishes "not decoded + // yet" from "decoded to nothing", which a length-0 slice cannot. + decoded: []byte, + state: Stream_Decode_State, + // The object this stream was parsed from, needed to derive its per-object + // decryption key. {0,0} for streams that came from an object stream (those + // are decrypted as part of their container) or were synthesised. + ref: Ref, +} + +Stream_Decode_State :: enum u8 { + Pending, + Decoded, + Failed, +} + +// Object is any PDF object. The zero value -- an empty union -- is the null +// object, which is also what a missing dictionary entry reads as. +Object :: union { + bool, + i64, // integer object + f64, // real object + Name, + String, + Ref, + ^Array, + ^Dict, + ^Stream, +} + +// --------------------------------------------------------------------------- +// Construction +// +// Everything allocated here is meant to live in a Document's arena, so nothing +// has a destructor: `close` reclaims the lot. Passing a non-arena allocator is +// supported (the parser is usable standalone) but then the caller owns the +// teardown. +// --------------------------------------------------------------------------- + +new_dict :: proc(allocator := context.allocator, cap := 8) -> ^Dict { + d := new(Dict, allocator) + d^ = make(Dict, cap, allocator) + return d +} + +new_array :: proc(allocator := context.allocator, cap := 8) -> ^Array { + a := new(Array, allocator) + a.items = make([dynamic]Object, 0, cap, allocator) + return a +} + +new_stream :: proc(dict: ^Dict, raw: []byte, allocator := context.allocator) -> ^Stream { + s := new(Stream, allocator) + s.dict = dict + s.raw = raw + s.state = .Pending + return s +} + +// --------------------------------------------------------------------------- +// Type tests and coercions +// +// Every `to_*` returns (value, ok) and never resolves references, so a caller +// that might be holding a Ref must go through `resolve` (or one of the +// resolving accessors below) first. +// --------------------------------------------------------------------------- + +is_null :: proc(o: Object) -> bool { + return o == nil +} + +to_bool :: proc(o: Object) -> (val: bool, ok: bool) { + val, ok = o.(bool) + return +} + +to_int :: proc(o: Object) -> (val: i64, ok: bool) { + #partial switch v in o { + case i64: return v, true + // Files write "1.0" where an integer is required often enough that + // refusing it would reject working documents. + case f64: + if v >= -9.22e18 && v <= 9.22e18 { + return i64(v), true + } + } + return 0, false +} + +to_real :: proc(o: Object) -> (val: f64, ok: bool) { + val, ok = o.(f64) + return +} + +// to_number accepts either numeric object type. This is what almost every +// caller wants: the format's distinction between 1 and 1.0 carries no meaning +// outside of a handful of places (array indices, object numbers, /Length). +to_number :: proc(o: Object) -> (val: f64, ok: bool) { + #partial switch v in o { + case i64: return f64(v), true + case f64: return v, true + } + return 0, false +} + +to_name :: proc(o: Object) -> (val: string, ok: bool) { + n, is_name := o.(Name) + return string(n), is_name +} + +to_bytes :: proc(o: Object) -> (val: []byte, ok: bool) { + s, is_str := o.(String) + return transmute([]byte)s, is_str +} + +to_array :: proc(o: Object) -> (val: ^Array, ok: bool) { + val, ok = o.(^Array) + return +} + +// to_dict accepts a stream where a dictionary is wanted and hands back the +// stream's dictionary. Several parts of the format (/Resources, /Group, +// annotation appearances) are reached through entries that are sometimes one +// and sometimes the other. +to_dict :: proc(o: Object) -> (val: ^Dict, ok: bool) { + #partial switch v in o { + case ^Dict: return v, true + case ^Stream: return v.dict, v.dict != nil + } + return nil, false +} + +to_stream :: proc(o: Object) -> (val: ^Stream, ok: bool) { + val, ok = o.(^Stream) + return +} + +to_ref :: proc(o: Object) -> (val: Ref, ok: bool) { + val, ok = o.(Ref) + return +} + +// name_is reports whether o is exactly this name. Saves a cast at every +// /Type and /Subtype check in the library. +name_is :: proc(o: Object, want: string) -> bool { + n, ok := o.(Name) + return ok && string(n) == want +} + +// object_kind names an object's type for diagnostics. +object_kind :: proc(o: Object) -> string { + switch _ in o { + case bool: return "boolean" + case i64: return "integer" + case f64: return "real" + case Name: return "name" + case String: return "string" + case Ref: return "reference" + case ^Array: return "array" + case ^Dict: return "dictionary" + case ^Stream: return "stream" + } + return "null" +} + +// object_identity is a stable cache key for an object: the address of the +// container it parsed to. Document.cache guarantees one address per object +// number, so the same indirect reference always keys the same slot, and a +// dictionary written inline in a resource is keyed by its own address. +// Scalars (a name, a number) have no identity and key nothing -- they cost +// nothing to rebuild. This is what `colorspace_load` and `function_load` cache +// on, which is why it lives here rather than in either of them. +@(private) +object_identity :: proc(o: Object) -> uintptr { + #partial switch v in o { + case ^Dict: return uintptr(rawptr(v)) + case ^Array: return uintptr(rawptr(v)) + case ^Stream: return uintptr(rawptr(v)) + } + return 0 +} + +// --------------------------------------------------------------------------- +// Dictionary access +// +// The `*_raw` forms never touch the Document and never follow a reference. +// Everything else takes a Document (which may be nil, degrading to the raw +// behaviour) and resolves indirect references before answering, because at +// almost every read site in a PDF the value is allowed to be one. +// --------------------------------------------------------------------------- + +dict_has :: proc(d: ^Dict, key: string) -> bool { + if d == nil { return false } + return key in d^ +} + +dict_get_raw :: proc(d: ^Dict, key: string) -> Object { + if d == nil { return nil } + return d[key] or_else nil +} + +dict_get :: proc(doc: ^Document, d: ^Dict, key: string) -> Object { + if d == nil { return nil } + v := d[key] or_else nil + return resolve(doc, v) +} + +// dict_get_alt reads `key`, falling back to `alt`. Inline image dictionaries +// abbreviate their keys (/W for /Width, /BPC for /BitsPerComponent), and the +// abbreviation is legal in both spellings for some of them. +dict_get_alt :: proc(doc: ^Document, d: ^Dict, key, alt: string) -> Object { + v := dict_get(doc, d, key) + if v == nil { + v = dict_get(doc, d, alt) + } + return v +} + +dict_set :: proc(d: ^Dict, key: string, value: Object) { + if d == nil { return } + d[key] = value +} + +dict_bool :: proc(doc: ^Document, d: ^Dict, key: string, default := false) -> bool { + v, ok := to_bool(dict_get(doc, d, key)) + return ok ? v : default +} + +dict_int :: proc(doc: ^Document, d: ^Dict, key: string, default: i64 = 0) -> i64 { + v, ok := to_int(dict_get(doc, d, key)) + return ok ? v : default +} + +dict_num :: proc(doc: ^Document, d: ^Dict, key: string, default: f64 = 0) -> f64 { + v, ok := to_number(dict_get(doc, d, key)) + return ok ? v : default +} + +dict_name :: proc(doc: ^Document, d: ^Dict, key: string, default := "") -> string { + v, ok := to_name(dict_get(doc, d, key)) + return ok ? v : default +} + +dict_bytes :: proc(doc: ^Document, d: ^Dict, key: string) -> []byte { + v, _ := to_bytes(dict_get(doc, d, key)) + return v +} + +dict_dict :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Dict { + v, _ := to_dict(dict_get(doc, d, key)) + return v +} + +dict_array :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Array { + v, _ := to_array(dict_get(doc, d, key)) + return v +} + +dict_stream :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Stream { + v, _ := to_stream(dict_get(doc, d, key)) + return v +} + +// dict_ref returns the entry *without* resolving it, for the places that need +// the identity of the referenced object rather than its value -- structural +// keys like /Parent, where following the reference is exactly what causes a +// cyclic walk. +dict_ref :: proc(d: ^Dict, key: string) -> (Ref, bool) { + return to_ref(dict_get_raw(d, key)) +} + +// dict_type reads /Type. Missing /Type is extremely common in the wild +// (it is optional on most dictionaries), so callers should treat "" as +// "unknown", never as "wrong". +dict_type :: proc(doc: ^Document, d: ^Dict) -> string { + return dict_name(doc, d, "Type") +} + +dict_subtype :: proc(doc: ^Document, d: ^Dict) -> string { + return dict_name(doc, d, "Subtype") +} + +// --------------------------------------------------------------------------- +// Array access +// --------------------------------------------------------------------------- + +array_len :: proc(a: ^Array) -> int { + if a == nil { return 0 } + return len(a.items) +} + +array_get_raw :: proc(a: ^Array, i: int) -> Object { + if a == nil || i < 0 || i >= len(a.items) { return nil } + return a.items[i] +} + +array_get :: proc(doc: ^Document, a: ^Array, i: int) -> Object { + return resolve(doc, array_get_raw(a, i)) +} + +array_num :: proc(doc: ^Document, a: ^Array, i: int, default: f64 = 0) -> f64 { + v, ok := to_number(array_get(doc, a, i)) + return ok ? v : default +} + +array_int :: proc(doc: ^Document, a: ^Array, i: int, default: i64 = 0) -> i64 { + v, ok := to_int(array_get(doc, a, i)) + return ok ? v : default +} + +array_name :: proc(doc: ^Document, a: ^Array, i: int, default := "") -> string { + v, ok := to_name(array_get(doc, a, i)) + return ok ? v : default +} + +array_dict :: proc(doc: ^Document, a: ^Array, i: int) -> ^Dict { + v, _ := to_dict(array_get(doc, a, i)) + return v +} + +array_push :: proc(a: ^Array, o: Object) { + if a == nil { return } + append(&a.items, o) +} + +// array_numbers copies an array of numbers into a freshly allocated slice. +// Non-numeric entries read as 0 rather than failing, matching how viewers +// treat a stray null in a /Widths or /Decode array. +array_numbers :: proc(doc: ^Document, a: ^Array, allocator := context.allocator) -> []f64 { + n := array_len(a) + if n == 0 { return nil } + out := make([]f64, n, allocator) + for i in 0 ..< n { + out[i] = array_num(doc, a, i, 0) + } + return out +} + +// --------------------------------------------------------------------------- +// Names +// --------------------------------------------------------------------------- + +// decode_name_escapes resolves the #xx escapes in a raw name token. +// Returns the input unchanged (and allocates nothing) when there are none, +// which is the overwhelmingly common case. +decode_name_escapes :: proc(raw: string, allocator := context.allocator) -> string { + if strings.index_byte(raw, '#') < 0 { + return raw + } + b := strings.builder_make(0, len(raw), allocator) + for i := 0; i < len(raw); { + if raw[i] == '#' && i + 2 < len(raw) { + hi, hi_ok := hex_value(raw[i + 1]) + lo, lo_ok := hex_value(raw[i + 2]) + // "#" followed by anything that isn't two hex digits is kept + // verbatim: it is a broken name, not a reason to drop bytes. + if hi_ok && lo_ok { + strings.write_byte(&b, hi << 4 | lo) + i += 3 + continue + } + } + strings.write_byte(&b, raw[i]) + i += 1 + } + return strings.to_string(b) +} + +@(private) +hex_value :: proc "contextless" (c: byte) -> (val: byte, ok: bool) { + switch c { + case '0' ..= '9': return c - '0', true + case 'a' ..= 'f': return c - 'a' + 10, true + case 'A' ..= 'F': return c - 'A' + 10, true + } + return 0, false +} + +// --------------------------------------------------------------------------- +// Numbers +// --------------------------------------------------------------------------- + +// parse_number reads the number syntax PDF actually uses, which is looser than +// the grammar in the spec: a leading '+', repeated signs, a bare '.', a +// trailing '.', and exponent notation all turn up in files produced by real +// software. Anything unparsable reads as 0, the value a viewer substitutes. +parse_number :: proc(s: string) -> (value: Object, ok: bool) { + if len(s) == 0 { return i64(0), false } + + is_real := false + for c in transmute([]byte)s { + if c == '.' || c == 'e' || c == 'E' { + is_real = true + break + } + } + + if !is_real { + if v, iok := strconv.parse_i64_of_base(s, 10); iok { + return v, true + } + // Integers too large for i64 (seen in damaged files) clamp rather than + // failing, so a bad /Length cannot abort a parse on its own. + if v, fok := strconv.parse_f64(s); fok { + return v, true + } + return i64(0), false + } + + if v, fok := strconv.parse_f64(s); fok { + return v, true + } + // ".5" and "4." are both legal PDF and both rejected by strict parsers. + cleaned := s + if len(cleaned) > 0 && cleaned[len(cleaned) - 1] == '.' { + cleaned = cleaned[:len(cleaned) - 1] + if v, fok := strconv.parse_f64(cleaned); fok { + return v, true + } + } + return f64(0), false +} diff --git a/object_test.odin b/object_test.odin new file mode 100644 index 0000000..858d96a --- /dev/null +++ b/object_test.odin @@ -0,0 +1,333 @@ +package pdf + +import "core:mem/virtual" +import "core:testing" + +// --------------------------------------------------------------------------- +// Coercions +// --------------------------------------------------------------------------- + +@(test) +test_to_int_accepts_reals :: proc(t: ^testing.T) { + // Files write "1.0" where an integer is required often enough that + // refusing it would reject working documents. + v, ok := to_int(f64(1.0)) + testing.expect(t, ok, "a real coerces to an integer") + testing.expect_value(t, v, i64(1)) + + v2, ok2 := to_int(i64(-7)) + testing.expect(t, ok2, "") + testing.expect_value(t, v2, i64(-7)) + + // Truncation, not rounding -- an index of 2.9 is element 2. + v3, _ := to_int(f64(2.9)) + testing.expect_value(t, v3, i64(2)) + + // A real too large for i64 must be refused, not wrapped. + _, ok4 := to_int(f64(1e300)) + testing.expect(t, !ok4, "a real beyond i64 range is not an integer") + _, ok5 := to_int(f64(-1e300)) + testing.expect(t, !ok5, "") + + _, ok6 := to_int(nil) + testing.expect(t, !ok6, "null is not an integer") + _, ok7 := to_int(true) + testing.expect(t, !ok7, "a boolean is not an integer") +} + +@(test) +test_to_number_and_real :: proc(t: ^testing.T) { + v, ok := to_number(i64(3)) + testing.expect(t, ok, "an integer is a number") + testing.expect_value(t, v, 3.0) + + v2, ok2 := to_number(f64(2.5)) + testing.expect(t, ok2, "") + testing.expect_value(t, v2, 2.5) + + _, ok3 := to_number(nil) + testing.expect(t, !ok3, "") + + // to_real is strict: an integer object is not a real object. + _, ok4 := to_real(i64(3)) + testing.expect(t, !ok4, "to_real does not coerce") +} + +@(test) +test_to_name_bytes_and_ref :: proc(t: ^testing.T) { + n, ok := to_name(Name("Page")) + testing.expect(t, ok, "") + testing.expect_value(t, n, "Page") + + b, ok2 := to_bytes(String(transmute([]byte)string("hi"))) + testing.expect(t, ok2, "") + testing.expect_value(t, string(b), "hi") + + r, ok3 := to_ref(Ref{12, 0}) + testing.expect(t, ok3, "") + testing.expect_value(t, r, Ref{12, 0}) + + testing.expect(t, name_is(Name("Catalog"), "Catalog"), "") + testing.expect(t, !name_is(Name("Catalog"), "Page"), "") + testing.expect(t, !name_is(String(transmute([]byte)string("Catalog")), "Catalog"), + "a string is not a name, even with the same bytes") + testing.expect(t, !name_is(nil, ""), "null is not the empty name") +} + +@(test) +test_object_kind :: proc(t: ^testing.T) { + testing.expect_value(t, object_kind(nil), "null") + testing.expect_value(t, object_kind(true), "boolean") + testing.expect_value(t, object_kind(i64(1)), "integer") + testing.expect_value(t, object_kind(f64(1)), "real") + testing.expect_value(t, object_kind(Name("x")), "name") + testing.expect_value(t, object_kind(Ref{1, 0}), "reference") + testing.expect(t, is_null(nil), "") + testing.expect(t, !is_null(i64(0)), "the integer zero is not null") +} + +// A stream where a dictionary is wanted hands back the stream's dictionary: +// /Resources, /Group and annotation appearances are all reached through entries +// that are sometimes one and sometimes the other. +@(test) +test_to_dict_accepts_a_stream :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + d := new_dict(a) + dict_set(d, "Type", Name("XObject")) + s := new_stream(d, nil, a) + + got, ok := to_dict(s) + testing.expect(t, ok, "a stream is usable as a dictionary") + testing.expect_value(t, got, d) + + _, ok2 := to_stream(d) + testing.expect(t, !ok2, "but a dictionary is not a stream") + + // A stream with no dictionary must not report success with a nil result. + bare := new_stream(nil, nil, a) + _, ok3 := to_dict(bare) + testing.expect(t, !ok3, "a stream without a dictionary is not a dictionary") +} + +// --------------------------------------------------------------------------- +// Dictionary access +// --------------------------------------------------------------------------- + +@(test) +test_dict_accessors_and_defaults :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + d := new_dict(a) + dict_set(d, "Count", i64(5)) + dict_set(d, "Size", f64(2.5)) + dict_set(d, "Type", Name("Pages")) + dict_set(d, "Flag", true) + dict_set(d, "Ref", Ref{9, 0}) + + testing.expect_value(t, dict_int(nil, d, "Count"), i64(5)) + testing.expect_value(t, dict_num(nil, d, "Size"), 2.5) + testing.expect_value(t, dict_num(nil, d, "Count"), 5.0) + testing.expect_value(t, dict_name(nil, d, "Type"), "Pages") + testing.expect_value(t, dict_type(nil, d), "Pages") + testing.expect(t, dict_bool(nil, d, "Flag"), "") + + // A missing key takes the default, and so does a key of the wrong type -- + // a viewer substitutes rather than failing. + testing.expect_value(t, dict_int(nil, d, "Absent", 42), i64(42)) + testing.expect_value(t, dict_int(nil, d, "Type", 42), i64(42)) + testing.expect_value(t, dict_name(nil, d, "Count", "fallback"), "fallback") + testing.expect_value(t, dict_bool(nil, d, "Absent", true), true) + + testing.expect(t, dict_has(d, "Count"), "") + testing.expect(t, !dict_has(d, "Nope"), "") + testing.expect(t, !dict_has(nil, "Count"), "a nil dictionary has nothing") + + // dict_ref must not resolve: /Parent is followed for identity, and + // resolving it is exactly what makes a cyclic walk. + r, ok := dict_ref(d, "Ref") + testing.expect(t, ok, "") + testing.expect_value(t, r, Ref{9, 0}) + _, ok2 := dict_ref(d, "Count") + testing.expect(t, !ok2, "a direct value is not a reference") + + // Every accessor must survive a nil dictionary. + testing.expect_value(t, dict_int(nil, nil, "Count", 1), i64(1)) + testing.expect(t, dict_get(nil, nil, "Count") == nil, "") + testing.expect(t, dict_dict(nil, nil, "x") == nil, "") + testing.expect(t, dict_array(nil, nil, "x") == nil, "") + testing.expect(t, dict_stream(nil, nil, "x") == nil, "") + dict_set(nil, "x", i64(1)) // must not crash +} + +// Inline images abbreviate their keys, and both spellings are legal. +@(test) +test_dict_get_alt :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + d := new_dict(a) + dict_set(d, "W", i64(16)) + v, ok := to_int(dict_get_alt(nil, d, "Width", "W")) + testing.expect(t, ok, "the abbreviation is found when the full name is absent") + testing.expect_value(t, v, i64(16)) + + dict_set(d, "Width", i64(32)) + v2, _ := to_int(dict_get_alt(nil, d, "Width", "W")) + testing.expect_value(t, v2, i64(32), ) +} + +// --------------------------------------------------------------------------- +// Arrays +// --------------------------------------------------------------------------- + +@(test) +test_array_accessors :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + arr := new_array(a) + array_push(arr, i64(10)) + array_push(arr, f64(2.5)) + array_push(arr, Name("Foo")) + + testing.expect_value(t, array_len(arr), 3) + testing.expect_value(t, array_int(nil, arr, 0), i64(10)) + testing.expect_value(t, array_num(nil, arr, 1), 2.5) + testing.expect_value(t, array_name(nil, arr, 2), "Foo") + + // Out of range in either direction reads as the default, never a panic. + testing.expect_value(t, array_int(nil, arr, 99, -1), i64(-1)) + testing.expect_value(t, array_int(nil, arr, -1, -1), i64(-1)) + testing.expect(t, array_get_raw(arr, 99) == nil, "") + testing.expect_value(t, array_len(nil), 0) + testing.expect(t, array_get_raw(nil, 0) == nil, "") + array_push(nil, i64(1)) // must not crash +} + +// A stray null in a /Widths or /Decode array reads as 0, which is what a +// viewer substitutes -- dropping the entry would shift every later index. +@(test) +test_array_numbers :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + arr := new_array(a) + array_push(arr, i64(1)) + array_push(arr, nil) + array_push(arr, f64(3.5)) + array_push(arr, Name("junk")) + + nums := array_numbers(nil, arr, a) + testing.expect_value(t, len(nums), 4) + testing.expect_value(t, nums[0], 1.0) + testing.expect_value(t, nums[1], 0.0) + testing.expect_value(t, nums[2], 3.5) + testing.expect_value(t, nums[3], 0.0) + + testing.expect_value(t, len(array_numbers(nil, nil, a)), 0) +} + +// --------------------------------------------------------------------------- +// Names and numbers +// --------------------------------------------------------------------------- + +@(test) +test_decode_name_escapes :: proc(t: ^testing.T) { + // The common case allocates nothing and returns the input unchanged. + plain := decode_name_escapes("Plain") + testing.expect_value(t, plain, "Plain") + + check :: proc(t: ^testing.T, input, want: string) { + got := decode_name_escapes(input) + testing.expect_value(t, got, want) + if raw_data(got) != raw_data(input) { + delete(got) + } + } + check(t, "Adobe#20Green", "Adobe Green") + check(t, "#41#42", "AB") + check(t, "a#2Fb", "a/b") + check(t, "lower#7ecase", "lower~case") + // A '#' that is not followed by two hex digits is a broken name, not a + // reason to drop bytes. + check(t, "bad#zz", "bad#zz") + check(t, "trailing#", "trailing#") + check(t, "short#4", "short#4") + check(t, "", "") +} + +@(test) +test_parse_number :: proc(t: ^testing.T) { + expect_int :: proc(t: ^testing.T, s: string, want: i64) { + o, ok := parse_number(s) + testing.expect(t, ok, "should parse") + v, is_int := o.(i64) + testing.expect(t, is_int, "should be an integer object") + testing.expect_value(t, v, want) + } + expect_real :: proc(t: ^testing.T, s: string, want: f64) { + o, ok := parse_number(s) + testing.expect(t, ok, "should parse") + v, is_real := o.(f64) + testing.expect(t, is_real, "should be a real object") + testing.expect_value(t, v, want) + } + + expect_int(t, "0", 0) + expect_int(t, "42", 42) + expect_int(t, "-17", -17) + expect_int(t, "+6", 6) + + expect_real(t, "3.5", 3.5) + expect_real(t, "-2.25", -2.25) + // ".5" and "4." are both legal PDF and both rejected by strict parsers. + expect_real(t, ".5", 0.5) + expect_real(t, "4.", 4.0) + expect_real(t, "-.5", -0.5) + + // Garbage yields 0, the value a viewer substitutes, and reports failure so + // a caller that cares can tell. + _, ok := parse_number("") + testing.expect(t, !ok, "the empty string is not a number") + _, ok2 := parse_number("--5") + testing.expect(t, !ok2, "a doubled sign is not a number") + o, _ := parse_number("junk") + v, _ := to_number(o) + testing.expect_value(t, v, 0.0) +} + +@(test) +test_new_object_constructors :: proc(t: ^testing.T) { + arena: virtual.Arena + _ = virtual.arena_init_growing(&arena) + defer virtual.arena_destroy(&arena) + a := virtual.arena_allocator(&arena) + + d := new_dict(a) + testing.expect(t, d != nil, "") + testing.expect_value(t, len(d^), 0) + + arr := new_array(a) + testing.expect(t, arr != nil, "") + testing.expect_value(t, array_len(arr), 0) + + payload := []byte{1, 2, 3} + s := new_stream(d, payload, a) + testing.expect(t, s != nil, "") + testing.expect_value(t, s.state, Stream_Decode_State.Pending) + testing.expect_value(t, len(s.raw), 3) + testing.expect_value(t, s.dict, d) +} diff --git a/objstm.odin b/objstm.odin new file mode 100644 index 0000000..ab53505 --- /dev/null +++ b/objstm.odin @@ -0,0 +1,246 @@ +package pdf + +// Object streams (ISO 32000-2, 7.5.7). +// +// An /ObjStm is a stream whose decoded bytes hold /N complete objects, one +// after another, preceded by a header of /N "object-number offset" integer +// pairs. /First is where the objects start; each pair's offset is measured +// from there. +// +// Two properties of the format matter to everything below. The objects inside +// have no "N G obj" header of their own, so the pair table is the only thing +// that says which object is where -- and when it disagrees with the +// cross-reference table, one of the two is wrong and the pair table is the one +// that was written next to the bytes it describes. And the whole stream is +// decrypted as a unit (7.5.7, and 7.6.2 for the reason), so the objects taken +// out of it must not be decrypted a second time: they are parsed with the zero +// Ref, which is what tells the parser to leave their strings alone. + +// Objstm is a decoded object stream and its pair table, cached on the Document +// for as long as the Document lives. +Objstm :: struct { + // The container object, so a repair can name it. + ref: Ref, + // Object numbers, in the order the pair table lists them. + nums: []u32, + // Where each object starts in `data`, or -1 for a pair whose offset does + // not land inside the stream. + offsets: []int, + // The decoded stream. Owned by the Document arena; never freed here. + data: []byte, + // Byte offset the objects start at, after any repair to /First. + first: int, + // What /N claimed, which is not always what the pair table holds. The + // dictionary is never encrypted even when the payload is, so the two + // disagreeing is evidence that the payload was not decoded correctly. + declared_n: int, + // Set while the header is being read, so an object stream that reaches + // itself -- directly, or through a second stream that points back -- is + // caught instead of recursed into. + loading: bool, + failed: bool, +} + +// objstm_get_object returns object `num`, which the cross-reference table says +// is entry `index` of object stream `container`. +// +// `index` is treated as a hint rather than as the truth. A file that was +// incrementally updated without its cross-reference stream being rewritten has +// stale indices, and the pair table is right where the index is wrong, so a +// mismatch is resolved by searching the table for the number that was asked +// for. +objstm_get_object :: proc(doc: ^Document, container: u32, index: u32, num: u32) -> (Object, Error) { + if doc == nil || num == 0 { return nil, .Invalid_Argument } + + stm, err := objstm_load(doc, container) + if err != .None { return nil, err } + if stm == nil || len(stm.nums) == 0 { return nil, .Object_Not_Found } + + slot := -1 + if int(index) < len(stm.nums) && stm.nums[index] == num { + slot = int(index) + } else { + for n, i in stm.nums { + if n == num { + slot = i + break + } + } + if slot >= 0 { + add_repair(doc, .Xref_Entry_Mismatch, 0, Ref{num, 0}) + } + } + if slot < 0 { return nil, .Object_Not_Found } + + off := stm.offsets[slot] + if off < 0 || off >= len(stm.data) { + add_repair(doc, .Missing_Object, 0, Ref{num, 0}) + return nil, .Object_Not_Found + } + + p := parser_make(doc, stm.data, doc.allocator) + p.lex.pos = off + // p.obj_ref stays {0,0}: this object's strings were decrypted with the + // container and must not be touched again (7.5.7). + obj, perr := parse_object(&p) + if perr != .None { + add_repair(doc, .Missing_Object, i64(off), Ref{num, 0}) + return nil, perr + } + return obj, .None +} + +// objstm_load decodes an object stream and reads its pair table, from the +// Document's cache if it has been read before. +@(private) +objstm_load :: proc(doc: ^Document, container: u32) -> (^Objstm, Error) { + if doc == nil || container == 0 || container > MAX_OBJECT_NUMBER { + return nil, .Invalid_Argument + } + + if cached, ok := doc.objstm_cache[container]; ok && cached != nil { + if cached.loading { + add_repair(doc, .Missing_Object, 0, cached.ref) + return nil, .Cyclic_Reference + } + if cached.failed { return nil, .Bad_Stream } + return cached, .None + } + + gen := u16(0) + if int(container) < len(doc.xref) { + gen = doc.xref[container].gen + } + + stm := new(Objstm, doc.allocator) + stm.ref = Ref{container, gen} + stm.loading = true + // Cached before it is read, so a stream that contains itself meets its own + // half-built entry rather than recursing. + doc.objstm_cache[container] = stm + + err := objstm_read(doc, stm) + stm.loading = false + if err != .None { + stm.failed = true + stm.nums = nil + stm.offsets = nil + stm.data = nil + return nil, err + } + return stm, .None +} + +// objstm_pairs reads the "object-number offset" table at the front of the +// decoded stream. `hit_limit` says it stopped because it reached `header_limit` +// rather than because the tokens stopped being a pair. +@(private) +objstm_pairs :: proc(doc: ^Document, data: []byte, limit, header_limit: int) -> + (nums: [dynamic]u32, rels: [dynamic]i64, header_end: int, hit_limit: bool) { + nums = make([dynamic]u32, 0, min(limit, 64), doc.allocator) + rels = make([dynamic]i64, 0, min(limit, 64), doc.allocator) + + lex := Lexer{data = data, pos = 0} + for len(nums) < limit { + save := lex.pos + t1 := lexer_next_structural(&lex) + t2 := lexer_next_structural(&lex) + if t1.kind != .Integer || t2.kind != .Integer { + lex.pos = save + break + } + if lex.pos > header_limit { + lex.pos = save + hit_limit = true + break + } + if t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER { + // A pair whose object number is impossible still occupies a slot: + // dropping it would shift every index after it. + append(&nums, u32(0)) + } else { + append(&nums, u32(t1.int_value)) + } + append(&rels, t2.int_value) + } + header_end = lex.pos + return +} + +@(private) +objstm_read :: proc(doc: ^Document, stm: ^Objstm) -> Error { + s := get_stream(doc, stm.ref) + if s == nil { return .Object_Not_Found } + // A missing /Type is tolerated -- it is missing from a great many + // dictionaries that are otherwise perfectly formed -- but a /Type that + // says something else means the cross-reference table sent us to the + // wrong object, and taking its bytes apart as pairs would invent objects. + if ty := dict_name(doc, s.dict, "Type"); ty != "" && ty != "ObjStm" { + return .Type_Mismatch + } + + data, derr := stream_data(doc, s) + if derr != .None { return derr } + if len(data) == 0 || len(data) > MAX_OBJSTM_SIZE { return .Bad_Stream } + stm.data = data + + declared_n := dict_int(doc, s.dict, "N", 0) + declared_first := dict_int(doc, s.dict, "First", -1) + stm.declared_n = int(clamp(declared_n, 0, i64(max(i32)))) + + // A pair is two integers and a separator, so three bytes at the very + // least. Whatever /N claims, the header cannot hold more than that -- and + // a stream is allowed to decode to a gigabyte, so the file's own length + // bounds it too: every object in here also needs an entry in the + // cross-reference table, which is bounded the same way. + max_pairs := min(len(data) / 3 + 1, xref_max_objects(doc)) + limit := max_pairs + if declared_n > 0 && declared_n <= i64(max_pairs) { + limit = int(declared_n) + } else { + // /N missing, zero, negative or larger than the stream can hold. Read + // pairs until they stop being pairs and take that as the count. + add_repair(doc, .Bad_Stream_Length, 0, stm.ref) + } + + // The header ends where /First says it does, when /First is believable. + // Reading past it would take the first object's own bytes for pairs. + header_limit := len(data) + if declared_first > 0 && declared_first <= i64(len(data)) { + header_limit = int(declared_first) + } + + nums, rels, header_end, hit_limit := objstm_pairs(doc, data, limit, header_limit) + // A /First that lands inside the pair table cuts it short. Reading the + // header again without that ceiling costs nothing -- the pairs stop at the + // first token that is not an integer either way -- and it is the + // difference between reading the stream and losing every object in it. + if hit_limit && (len(nums) == 0 || (declared_n > 0 && len(nums) < int(declared_n))) { + nums, rels, header_end, _ = objstm_pairs(doc, data, limit, len(data)) + } + + first := declared_first + if first < i64(header_end) || first > i64(len(data)) { + // /First is measured from the start of the decoded stream and must be + // at or after the end of the pair table. When it is not, the pair + // table itself says where the objects begin. + add_repair(doc, .Bad_Stream_Length, 0, stm.ref) + first = i64(header_end) + } + stm.first = int(first) + + offsets := make([]int, len(rels), doc.allocator) + for r, i in rels { + abs := first + r + if r < 0 || abs < 0 || abs >= i64(len(data)) { + offsets[i] = -1 + continue + } + offsets[i] = int(abs) + } + + stm.nums = nums[:] + stm.offsets = offsets + if len(stm.nums) == 0 { return .Bad_Stream } + return .None +} diff --git a/objstm_test.odin b/objstm_test.odin new file mode 100644 index 0000000..f08412e --- /dev/null +++ b/objstm_test.odin @@ -0,0 +1,805 @@ +package pdf + +import "core:crypto/legacy/md5" +import "core:testing" + +// The sample-PDF builders live in xref_test.odin; an object stream is only +// reachable through a cross-reference stream, so the two are tested with the +// same tools. + +@(private = "file") +Objstm_Opts :: struct { + // What the dictionary declares, or -1 to declare the truth. + declared_n: int, + declared_first: int, + // Overrides the /Type of the container, for the wrong-object case. + type_name: string, + // The /Index field of the type-2 entries for objects 2, 3 and 4. + indices: [3]u64, + // Write the container as a plain dictionary rather than a stream. + not_a_stream: bool, + // Point every type-2 entry at this container instead of object 1. + container: u64, +} + +@(private = "file") +objstm_defaults :: proc() -> Objstm_Opts { + return Objstm_Opts{ + declared_n = -1, + declared_first = -1, + indices = {0, 1, 2}, + container = 1, + } +} + +// build_objstm_file writes a modern file: one object stream holding the +// catalog, the page tree and a page, and a cross-reference stream pointing at +// all three. +@(private = "file") +build_objstm_file :: proc(opts: Objstm_Opts) -> Pdf_Builder { + pb := pb_make("%PDF-1.5\n") + + bodies := [3]string{ + "<< /Type /Catalog /Pages 3 0 R >>", + "<< /Type /Pages /Kids [4 0 R] /Count 1 >>", + "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 250 250] >>", + } + nums := [3]u32{2, 3, 4} + payload, first := objstm_payload(nums[:], bodies[:]) + defer delete(payload) + + ty := opts.type_name if opts.type_name != "" else "/ObjStm" + + pb_mark(&pb, 1) + pb_str(&pb, "1 0 obj\n<< /Type ") + pb_str(&pb, ty) + pb_str(&pb, " /N ") + pb_num(&pb, opts.declared_n >= 0 ? opts.declared_n : 3) + pb_str(&pb, " /First ") + pb_num(&pb, opts.declared_first >= 0 ? opts.declared_first : first) + if opts.not_a_stream { + pb_str(&pb, " >>\nendobj\n") + } else { + pb_str(&pb, " /Length ") + pb_num(&pb, len(payload)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, payload) + pb_str(&pb, "\nendstream\nendobj\n") + } + + at := pb_here(&pb) + c := opts.container + rows := [6]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {2, c, opts.indices[0]}, + {2, c, opts.indices[1]}, + {2, c, opts.indices[2]}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 5, "/Type /XRef /Size 6 /W [1 4 2] /Root 2 0 R", raw) + pb_startxref(&pb, at) + return pb +} + +@(private = "file") +expect_objstm_page :: proc(t: ^testing.T, doc: ^Document, width: f64) { + d := get_dict(doc, Ref{4, 0}) + if !testing.expect(t, d != nil, "the page did not come out of the object stream") { + return + } + testing.expect(t, dict_name(doc, d, "Type") == "Page", "object 4 is not a /Page") + r := dict_rect(doc, d, "MediaBox") + testing.expectf(t, r.x1 == width, "MediaBox width %v, wanted %v", r.x1, width) +} + +// --------------------------------------------------------------------------- +// The ordinary case +// --------------------------------------------------------------------------- + +@(test) +test_objstm_basic :: proc(t: ^testing.T) { + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, !doc.is_damaged, "a well-formed object stream recorded a repair") + testing.expect(t, doc.catalog != nil, "the catalog did not come out of the object stream") + testing.expect(t, doc.xref[2].kind == .Compressed, "object 2 is not a compressed entry") + testing.expect(t, dict_name(doc, get_dict(doc, Ref{3, 0}), "Type") == "Pages", + "object 3 is not the page tree root") + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_header_is_cached :: proc(t: ^testing.T) { + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + _ = get_object(doc, Ref{2, 0}) + stm, ok := doc.objstm_cache[1] + testing.expect(t, ok && stm != nil, "the object stream header was not cached") + if stm == nil { return } + testing.expectf(t, len(stm.nums) == 3, "cached %v pairs, wanted 3", len(stm.nums)) + testing.expect(t, stm.nums[0] == 2 && stm.nums[1] == 3 && stm.nums[2] == 4, + "the pair table was read in the wrong order") + testing.expect(t, !stm.loading, "the loading flag was left set") +} + +// --------------------------------------------------------------------------- +// A pair table that disagrees with the cross-reference table +// --------------------------------------------------------------------------- + +@(test) +test_objstm_stale_index_is_searched_for :: proc(t: ^testing.T) { + // Every type-2 entry claims index 0. The pair table is the thing that was + // written next to the bytes, so it wins and the object is found by number. + opts := objstm_defaults() + opts.indices = {0, 0, 0} + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + expect_objstm_page(t, doc, 250) + testing.expect(t, dict_name(doc, get_dict(doc, Ref{3, 0}), "Type") == "Pages", + "object 3 was not found by number") + testing.expect(t, doc_has_repair(doc, .Xref_Entry_Mismatch), + "a stale index should be recorded as a repair") +} + +@(test) +test_objstm_index_past_the_end :: proc(t: ^testing.T) { + opts := objstm_defaults() + opts.indices = {900, 901, 902} + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_get_object_rejects_absent_number :: proc(t: ^testing.T) { + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + obj, gerr := objstm_get_object(doc, 1, 0, 99) + testing.expect(t, obj == nil && gerr == .Object_Not_Found, + "an object number the stream does not hold must not resolve") + + _, zerr := objstm_get_object(doc, 1, 0, 0) + testing.expect(t, zerr == .Invalid_Argument, "object 0 is not a legal request") +} + +// --------------------------------------------------------------------------- +// /N and /First lying +// --------------------------------------------------------------------------- + +@(test) +test_objstm_first_past_end_is_repaired :: proc(t: ^testing.T) { + // /First outside the decoded stream. The pair table itself says where the + // objects start, so the file is still readable. + opts := objstm_defaults() + opts.declared_first = 9999 + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc_has_repair(doc, .Bad_Stream_Length), "no repair was recorded") + expect_objstm_page(t, doc, 250) + + stm, ok := doc.objstm_cache[1] + if testing.expect(t, ok && stm != nil, "the object stream was not cached") { + testing.expectf(t, stm.first > 0 && stm.first <= len(stm.data), + "/First was repaired to %v, outside a %v byte stream", stm.first, len(stm.data)) + } +} + +@(test) +test_objstm_first_inside_pair_table_is_repaired :: proc(t: ^testing.T) { + // /First 1 puts the objects on top of the pair table, which cannot be. + opts := objstm_defaults() + opts.declared_first = 1 + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, doc_has_repair(doc, .Bad_Stream_Length), "no repair was recorded") + // Reading the pair table again without the bogus ceiling recovers the file + // completely. + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_n_too_large_is_clamped :: proc(t: ^testing.T) { + opts := objstm_defaults() + opts.declared_n = 100_000_000 + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + stm, ok := doc.objstm_cache[1] + if testing.expect(t, ok && stm != nil, "the object stream was not read at all") { + testing.expectf(t, len(stm.nums) <= 64, "read %v pairs from a 3-object stream", + len(stm.nums)) + } + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_n_missing_is_derived :: proc(t: ^testing.T) { + opts := objstm_defaults() + opts.declared_n = 0 + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_objstm_page(t, doc, 250) +} + +// --------------------------------------------------------------------------- +// Containers that are not object streams +// --------------------------------------------------------------------------- + +// The cross-reference stream says objects 2, 3 and 4 live inside object 1, but +// object 1 was written as a plain dictionary. There are no bytes to take +// apart, so the lookup has to fail rather than parse whatever follows the +// dictionary in the file -- which is the cross-reference stream itself. +@(test) +test_objstm_container_is_not_a_stream :: proc(t: ^testing.T) { + opts := objstm_defaults() + opts.not_a_stream = true + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, _ := pb_open(&pb) + defer close(doc) + if doc == nil { return } + + obj, gerr := objstm_get_object(doc, 1, 0, 2) + testing.expect(t, obj == nil, "a dictionary was taken apart as an object stream") + // Which refusal comes back depends on whether the container has been tried + // before -- `find_catalog` already walked the file during open, and a + // container that failed once is remembered as failed. Both are clean. + testing.expectf(t, gerr == .Object_Not_Found || gerr == .Bad_Stream, + "container that is not a stream: got %s", error_string(gerr)) + + // The container is remembered as failed, so the second and third objects + // do not each retry the decode. + stm, cached := doc.objstm_cache[1] + testing.expect(t, cached && stm != nil, "the failed container was not cached") + if stm != nil { + testing.expect(t, stm.failed, "the container was not marked failed") + testing.expect(t, !stm.loading, "the loading flag was left set") + testing.expect(t, len(stm.nums) == 0, "a failed container kept a pair table") + } + + // Everything the container was supposed to hold reads as null. Whether the + // xref still calls those entries compressed or the loader gave up on the + // section and rebuilt -- which is what happens here, and leaves 2, 3 and 4 + // free, because the rebuild finds only the objects physically in the file + // -- the answer a caller gets is the same. + for num in 2 ..= 4 { + testing.expectf(t, get_object(doc, Ref{u32(num), 0}) == nil, + "object %v came back out of a container that is not a stream", num) + } + + // Silence would be the real bug: a caller has no other way to tell an + // empty document from one whose objects were lost. + testing.expect(t, doc.is_damaged, "losing three objects was not recorded as damage") + testing.expect(t, doc_has_repair(doc, .Missing_Object), + "no Missing_Object repair for the objects the container was supposed to hold") + + // Object 1 itself is a perfectly good dictionary and still reads. + d := get_dict(doc, Ref{1, 0}) + testing.expect(t, d != nil && dict_name(doc, d, "Type") == "ObjStm", + "the container dictionary itself stopped resolving") +} + +@(test) +test_objstm_container_has_wrong_type :: proc(t: ^testing.T) { + opts := objstm_defaults() + opts.type_name = "/XObject" + pb := build_objstm_file(opts) + defer pb_destroy(&pb) + + doc, _ := pb_open(&pb) + defer close(doc) + if doc == nil { return } + + obj, gerr := objstm_get_object(doc, 1, 0, 2) + testing.expect(t, obj == nil && gerr == .Type_Mismatch, + "a container that says it is something else must be refused") +} + +// build_split_file writes a file whose catalog and page tree are ordinary +// top-level objects and whose page lives in an object stream. /Root therefore +// resolves without touching the object stream, so a broken entry for the page +// is a broken entry rather than a reason to rebuild the whole file -- which is +// what makes the degradation visible. +@(private = "file") +build_split_file :: proc(page_container: u64, objstm_self_hosted: bool) -> Pdf_Builder { + pb := pb_make("%PDF-1.5\n") + + bodies := [1]string{"<< /Type /Page /Parent 3 0 R /MediaBox [0 0 250 250] >>"} + nums := [1]u32{4} + payload, first := objstm_payload(nums[:], bodies[:]) + defer delete(payload) + + pb_mark(&pb, 1) + pb_str(&pb, "1 0 obj\n<< /Type /ObjStm /N 1 /First ") + pb_num(&pb, first) + pb_str(&pb, " /Length ") + pb_num(&pb, len(payload)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, payload) + pb_str(&pb, "\nendstream\nendobj\n") + + pb_obj(&pb, 2, "<< /Type /Catalog /Pages 3 0 R >>") + pb_obj(&pb, 3, "<< /Type /Pages /Kids [4 0 R] /Count 1 >>") + + at := pb_here(&pb) + // Object 1 either sits in the file where it really is, or claims to live + // inside itself. + row1 := Xref_Row{1, u64(pb.offs[1]), 0} + if objstm_self_hosted { + row1 = Xref_Row{2, 1, 0} + } + rows := [6]Xref_Row{ + {0, 0, 65535}, + row1, + {1, u64(pb.offs[2]), 0}, + {1, u64(pb.offs[3]), 0}, + {2, page_container, 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 5, "/Type /XRef /Size 6 /W [1 4 2] /Root 2 0 R", raw) + pb_startxref(&pb, at) + return pb +} + +@(test) +test_objstm_split_file_baseline :: proc(t: ^testing.T) { + pb := build_split_file(1, false) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, !doc.is_damaged, "a well-formed split file recorded a repair") + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_container_does_not_exist :: proc(t: ^testing.T) { + // The page claims to live inside object 90, which the file does not have. + // /Root still resolves, so nothing triggers a rebuild and the reference + // has to degrade to null on its own. + pb := build_split_file(90, false) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, get_object(doc, Ref{4, 0}) == nil, + "an entry pointing at a container that is not there must read as null") + testing.expect(t, doc.catalog != nil, "one bad entry must not cost the rest of the file") + testing.expect(t, doc_has_repair(doc, .Missing_Object), "no repair was recorded") +} + +// --------------------------------------------------------------------------- +// Cycles +// --------------------------------------------------------------------------- + +@(test) +test_objstm_contains_itself :: proc(t: ^testing.T) { + // The cross-reference table says the object stream itself is one of the + // objects inside it. Reading it needs its own bytes, which needs reading + // it: without a guard this is an unbounded recursion. + pb := build_split_file(1, true) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, get_object(doc, Ref{1, 0}) == nil, + "a self-containing object stream must read as null, not recurse") + // Everything the stream held goes with it, but the rest of the file does + // not. + testing.expect(t, doc.catalog != nil, "the catalog was lost with the object stream") + testing.expect(t, doc.is_damaged, "the cycle should have been recorded") +} + +@(test) +test_objstm_mutual_cycle :: proc(t: ^testing.T) { + // Object 1 says it lives in object 2's stream, and object 2 says it lives + // in object 1's. + pb := pb_make("%PDF-1.5\n") + defer pb_destroy(&pb) + + bodies := [1]string{"<< /Type /Catalog >>"} + for num in ([2]u32{1, 2}) { + nums := [1]u32{num == 1 ? 2 : 1} + payload, first := objstm_payload(nums[:], bodies[:]) + defer delete(payload) + pb_mark(&pb, num) + pb_num(&pb, int(num)) + pb_str(&pb, " 0 obj\n<< /Type /ObjStm /N 1 /First ") + pb_num(&pb, first) + pb_str(&pb, " /Length ") + pb_num(&pb, len(payload)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, payload) + pb_str(&pb, "\nendstream\nendobj\n") + } + + at := pb_here(&pb) + rows := [4]Xref_Row{ + {0, 0, 65535}, + {2, 2, 0}, + {2, 1, 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 3, "/Type /XRef /Size 4 /W [1 4 2] /Root 1 0 R", raw) + pb_startxref(&pb, at) + + doc, _ := pb_open(&pb) + defer close(doc) + if doc == nil { return } + _ = get_object(doc, Ref{1, 0}) + _ = get_object(doc, Ref{2, 0}) + testing.expect(t, true, "a mutual object-stream cycle must terminate") +} + +// --------------------------------------------------------------------------- +// Object streams found by a rebuild +// --------------------------------------------------------------------------- + +@(test) +test_objstm_found_by_rebuild :: proc(t: ^testing.T) { + // The cross-reference stream is unreachable, so every object in the file + // has to be found by scanning -- and three of the four only exist inside + // an object stream. + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + // Point startxref at nothing. + at := last_index_bytes(pb.buf[:], transmute([]byte)string("startxref")) + testing.expect(t, at > 0, "the sample has no startxref") + if at <= 0 { return } + resize(&pb.buf, at) + pb_str(&pb, "startxref\n99000000\n%%EOF\n") + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + testing.expect(t, doc.xref[4].kind == .Compressed, + "the rebuild did not register the object stream's contents") + testing.expect(t, doc.catalog != nil, "the catalog inside the object stream was lost") + expect_objstm_page(t, doc, 250) +} + +@(test) +test_objstm_rebuild_prefers_later_top_level_definition :: proc(t: ^testing.T) { + // The file was updated by appending an uncompressed object 4, which is + // newer than the object stream and must win. + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + at := last_index_bytes(pb.buf[:], transmute([]byte)string("startxref")) + if at <= 0 { return } + resize(&pb.buf, at) + pb_obj(&pb, 4, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 700 700] >>") + pb_str(&pb, "startxref\n99000000\n%%EOF\n") + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + expect_objstm_page(t, doc, 700) +} + +// --------------------------------------------------------------------------- +// Damaged payloads +// --------------------------------------------------------------------------- + +@(test) +test_objstm_truncation_survives :: proc(t: ^testing.T) { + pb := build_objstm_file(objstm_defaults()) + defer pb_destroy(&pb) + + for cut := 1; cut <= len(pb.buf); cut += 5 { + doc, _ := open_bytes(pb.buf[:cut], Open_Options{skip_page_tree = true}) + if doc != nil { + for num in 1 ..< object_count(doc) { + _ = get_object(doc, Ref{u32(num), 0}) + } + close(doc) + } + } + testing.expect(t, true, "a truncated object stream must never crash or hang") +} + +@(test) +test_objstm_garbage_payload_survives :: proc(t: ^testing.T) { + cases := [?]string{ + "", + " ", + "1", + "1 ", + "1 0", + "1 999999999999 << >>", + "-4 -4 << >>", + "1 0 <<", + "99999999999999999999 0 << >>", + } + for body in cases { + pb := pb_make("%PDF-1.5\n") + defer pb_destroy(&pb) + + payload := transmute([]byte)body + pb_mark(&pb, 1) + pb_str(&pb, "1 0 obj\n<< /Type /ObjStm /N 1 /First 4 /Length ") + pb_num(&pb, len(payload)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, payload) + pb_str(&pb, "\nendstream\nendobj\n") + + at := pb_here(&pb) + rows := [3]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 2, "/Type /XRef /Size 3 /W [1 4 2] /Root 1 0 R", raw) + pb_startxref(&pb, at) + + doc, _ := pb_open(&pb) + if doc != nil { + _, _ = objstm_get_object(doc, 1, 0, 7) + close(doc) + } + } + testing.expect(t, true, "a garbage object-stream payload must never crash") +} + +// --------------------------------------------------------------------------- +// Encrypted documents +// +// A modern encrypted file keeps its catalog inside an object stream, and that +// stream cannot be decoded until the file key exists -- which is after the +// cross-reference table has been read. Two things in xref.odin exist for this +// case and are only exercised here: /Root is not resolved as a validity check +// when /Encrypt is present, and every object and object stream read during the +// load is dropped afterwards, because all of it was read in the dark. +// --------------------------------------------------------------------------- + +@(private = "file") +rc4_object_key :: proc(file_key: []byte, num: u32, gen: u16, out: []byte) -> int { + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, file_key) + tail := [5]byte{ + byte(num), byte(num >> 8), byte(num >> 16), + byte(gen), byte(gen >> 8), + } + md5.update(&ctx, tail[:]) + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + n := min(len(file_key) + 5, 16) + copy(out[:n], digest[:n]) + return n +} + +@(private = "file") +pb_hex :: proc(pb: ^Pdf_Builder, data: []byte) { + hex := transmute([]byte)string("0123456789abcdef") + pb_str(pb, "<") + for c in data { + append(&pb.buf, hex[c >> 4]) + append(&pb.buf, hex[c & 0x0f]) + } + pb_str(pb, ">") +} + +// build_encrypted_objstm_file writes an RC4 40-bit, revision 2 document with an +// empty user password -- the oldest scheme, and the one whose key derivation is +// short enough to spell out here -- whose catalog, page tree and page all live +// inside an encrypted object stream. +@(private = "file") +build_encrypted_objstm_file :: proc() -> Pdf_Builder { + id0 := [16]byte{ + 0x2b, 0x1c, 0x7d, 0x0e, 0x55, 0x91, 0xa3, 0x44, + 0x10, 0xff, 0x6c, 0x22, 0x83, 0x39, 0xd7, 0x00, + } + permissions := i32(-1) + + // /O, Algorithm 3 with an empty owner password. + padded: [32]byte + crypt_pad_password(nil, padded[:]) + ctx: md5.Context + md5.init(&ctx) + md5.update(&ctx, padded[:]) + digest: [md5.DIGEST_SIZE]byte + md5.final(&ctx, digest[:]) + o_value: [32]byte + crypt_pad_password(nil, o_value[:]) + rc4_crypt(digest[:5], o_value[:], o_value[:]) + + file_key: [5]byte + crypt_key_r234(nil, o_value[:], permissions, id0[:], 2, 5, true, file_key[:]) + + u_value: [32]byte + crypt_user_value(file_key[:], 2, id0[:], u_value[:]) + + bodies := [3]string{ + "<< /Type /Catalog /Pages 3 0 R >>", + "<< /Type /Pages /Kids [4 0 R] /Count 1 >>", + "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 321 321] >>", + } + nums := [3]u32{2, 3, 4} + plain, first := objstm_payload(nums[:], bodies[:]) + defer delete(plain) + + // The whole object stream is encrypted with object 1's key; the objects + // inside it are then in the clear (ISO 32000-2, 7.5.7). + cipher := make([]byte, len(plain)) + defer delete(cipher) + okey: [16]byte + n_key := rc4_object_key(file_key[:], 1, 0, okey[:]) + rc4_crypt(okey[:n_key], cipher, plain) + + pb := pb_make("%PDF-1.5\n") + + pb_mark(&pb, 1) + pb_str(&pb, "1 0 obj\n<< /Type /ObjStm /N 3 /First ") + pb_num(&pb, first) + pb_str(&pb, " /Length ") + pb_num(&pb, len(cipher)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, cipher) + pb_str(&pb, "\nendstream\nendobj\n") + + // The /Encrypt dictionary's own strings are never encrypted: they are what + // the key is derived from. + pb_mark(&pb, 5) + pb_str(&pb, "5 0 obj\n<< /Filter /Standard /V 1 /R 2 /P -1 /O ") + pb_hex(&pb, o_value[:]) + pb_str(&pb, " /U ") + pb_hex(&pb, u_value[:]) + pb_str(&pb, " >>\nendobj\n") + + at := pb_here(&pb) + rows := [7]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {2, 1, 0}, + {2, 1, 1}, + {2, 1, 2}, + {1, u64(pb.offs[5]), 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + + // A cross-reference stream is never encrypted either, so it is written in + // the clear and its /ID strings stay as they are. + pb_mark(&pb, 6) + pb_str(&pb, "6 0 obj\n<< /Type /XRef /Size 7 /W [1 4 2] /Root 2 0 R /Encrypt 5 0 R /ID [") + pb_hex(&pb, id0[:]) + pb_str(&pb, " ") + pb_hex(&pb, id0[:]) + pb_str(&pb, "] /Length ") + pb_num(&pb, len(raw)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, raw) + pb_str(&pb, "\nendstream\nendobj\n") + pb_startxref(&pb, at) + return pb +} + +@(test) +test_objstm_encrypted_catalog_in_object_stream :: proc(t: ^testing.T) { + pb := build_encrypted_objstm_file() + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expectf(t, err == .None, "open failed: %v", error_string(err)) + if doc == nil { return } + + testing.expect(t, document_is_encrypted(doc), "the document is not marked encrypted") + testing.expect(t, !doc_has_repair(doc, .Rebuilt_Xref), + "an encrypted catalog inside an object stream must not force a rebuild") + testing.expect(t, doc.catalog != nil, "the catalog was not decrypted") + testing.expect(t, dict_name(doc, doc.catalog, "Type") == "Catalog", + "the catalog decoded to something else") + expect_objstm_page(t, doc, 321) +} + +@(test) +test_objstm_encrypted_rebuild_degrades_cleanly :: proc(t: ^testing.T) { + // An encrypted file that also needs a rebuild is the one case where the + // compressed objects genuinely cannot be recovered: the file key is not + // derived until after the table is read, so the object streams decode to + // ciphertext. What must not happen is that the garbage gets registered as + // if it were a pair table -- the invented entries would overwrite the real + // ones the scan found. + pb := build_encrypted_objstm_file() + defer pb_destroy(&pb) + + at := last_index_bytes(pb.buf[:], transmute([]byte)string("startxref")) + testing.expect(t, at > 0, "the sample has no startxref") + if at <= 0 { return } + resize(&pb.buf, at) + pb_str(&pb, "startxref\n99000000\n%%EOF\n") + + doc, err := pb_open(&pb) + defer close(doc) + testing.expectf(t, err == .None, "open failed: %v", error_string(err)) + if doc == nil { return } + + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + testing.expect(t, document_is_encrypted(doc), + "the /Encrypt dictionary survived the rebuild, so the file is still encrypted") + // The three objects in the stream are gone, but the objects that were + // written at the top level are exactly where the scan found them. + testing.expect(t, doc.xref[1].kind == .Normal, "the object stream itself was lost") + testing.expect(t, doc.xref[5].kind == .Normal, "the /Encrypt dictionary was lost") + for num in 1 ..< object_count(doc) { + testing.expect(t, doc.xref[num].kind != .Compressed, + "ciphertext was read as a pair table") + _ = get_object(doc, Ref{u32(num), 0}) + } +} diff --git a/outline.odin b/outline.odin new file mode 100644 index 0000000..2c321ba --- /dev/null +++ b/outline.odin @@ -0,0 +1,528 @@ +package pdf + +import "core:mem" +import "core:strings" + +// The document outline (bookmarks), destinations and page labels. +// +// None of this draws anything: it is the navigation model a viewer needs +// around the page it is showing -- the tree down the side, where a bookmark +// jumps to, and the "iv" it puts in the page box instead of "4". +// +// A destination is the format's way of naming a position: a page, a view +// (`/XYZ`, `/Fit`, ...), and the coordinates that view takes. It can be +// written out in full, or named, in which case it is looked up in the +// catalog's `/Dests` dictionary (PDF 1.1) or its `/Names /Dests` name tree +// (PDF 1.2 and later). Both spellings are still produced. +// +// The outline is a doubly linked tree -- `/First` and `/Last` down, `/Next` +// and `/Prev` across, `/Parent` up -- and not an array, so a file that gets +// one link wrong hands the reader an infinite loop rather than a short tree. +// Every walk here is bounded three ways: a depth cap, a sibling cap, and a +// visited set of the dictionaries already placed in the tree. The last is what +// catches the common damage, a `/Next` chain that points back at itself. + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +// Nesting the outline walk will follow. Real outlines are five or six deep; +// the format sets no limit and a hand-built file can nest until the stack ends. +@(private) +MAX_OUTLINE_DEPTH :: 32 + +// Items in one sibling chain, and in the whole tree. A file with more than a +// million bookmarks is not one anybody is going to navigate. +@(private) +MAX_OUTLINE_SIBLINGS :: 1 << 16 +@(private) +MAX_OUTLINE_ITEMS :: 1 << 20 + +// Indirections followed while resolving a destination: a name into the /Dests +// dictionary, whose value is a dictionary, whose /D is the array. Three hops +// is the deepest the format defines; the fourth is there so a file that adds +// one more legitimate wrapper still works, and the bound is what stops a name +// that resolves to itself. +@(private) +MAX_DEST_HOPS :: 4 + +// Bytes of a /P prefix that reach the label, and repetitions of a letter in an +// alphabetic label. Both are file-controlled and both would otherwise let a +// tiny file ask for an enormous string. +@(private) +MAX_LABEL_PREFIX :: 512 +@(private) +MAX_LABEL_LETTERS :: 64 + +// Largest value written as a Roman numeral. Above it the numeral would be a +// row of thousands with no information in it, so the number is written in +// decimal instead -- which is what a viewer showing "page 40000" wants anyway. +@(private) +MAX_LABEL_ROMAN :: 32767 + +// --------------------------------------------------------------------------- +// Destinations +// --------------------------------------------------------------------------- + +// Dest_Kind is the view a destination asks for (ISO 32000-2, 12.3.2.2). The +// names mirror the format's: /XYZ, /FitH, /FitBV and the rest. +Dest_Kind :: enum u8 { + XYZ = 0, // left top zoom -- any of the three may be "unchanged" + Fit, // fit the whole page + Fit_H, // top: fit the width, with this at the top of the window + Fit_V, // left: fit the height + Fit_R, // left bottom right top: fit this rectangle + Fit_B, // fit the bounding box of the page's contents + Fit_BH, // top: fit the bounding box's width + Fit_BV, // left: fit the bounding box's height +} + +// Destination is a resolved place in this document. +// +// `args` holds the view's numbers in the order the format writes them, and +// `has` says which of them the file actually gave: /XYZ's arguments are +// individually allowed to be null, meaning "leave that one as it is", and a +// zero is a legal coordinate, so the two cannot be told apart by value. +// +// The zero value is NOT a valid destination -- it names page 0. Use +// DESTINATION_NONE, whose page is -1, for "no destination". +Destination :: struct { + page: int, // page index, or -1 if the destination named no page here + kind: Dest_Kind, + args: [4]f64, + has: [4]bool, +} + +DESTINATION_NONE :: Destination{page = -1} + +// dest_kind_from_name maps a destination's view name. +@(private) +dest_kind_from_name :: proc(name: string) -> (kind: Dest_Kind, ok: bool) { + switch name { + case "XYZ": return .XYZ, true + case "Fit": return .Fit, true + case "FitH": return .Fit_H, true + case "FitV": return .Fit_V, true + case "FitR": return .Fit_R, true + case "FitB": return .Fit_B, true + case "FitBH": return .Fit_BH, true + case "FitBV": return .Fit_BV, true + } + return .XYZ, false +} + +// dest_arg_count is how many numbers follow the view name. +@(private) +dest_arg_count :: proc(kind: Dest_Kind) -> int { + switch kind { + case .XYZ: return 3 + case .Fit, .Fit_B: return 0 + case .Fit_H, .Fit_V, .Fit_BH, .Fit_BV: return 1 + case .Fit_R: return 4 + } + return 0 +} + +// dest_resolve turns any of the forms a destination is written in into a +// Destination. +// +// `obj` may be the destination array itself, a name or string that names one, +// a dictionary with a /D entry (which is both what the name tree stores and +// what a /GoTo action is), or a reference to any of those. +// +// `ok` is false only when nothing destination-shaped was found. A destination +// that is well formed but names a page this document does not have is still +// ok, with `page` -1: that is a broken link, not a broken file, and dropping +// the whole bookmark because its target was deleted loses its title too. +dest_resolve :: proc(doc: ^Document, obj: Object) -> (Destination, bool) { + if doc == nil { + return DESTINATION_NONE, false + } + o := resolve(doc, obj) + for _ in 0 ..< MAX_DEST_HOPS { + #partial switch v in o { + case ^Array: + return dest_from_array(doc, v) + case Name: + next, found := dest_by_name(doc, transmute([]byte)string(v)) + if !found { + return DESTINATION_NONE, false + } + o = next + case String: + next, found := dest_by_name(doc, ([]byte)(v)) + if !found { + return DESTINATION_NONE, false + } + o = next + case ^Dict: + // Both /Dests-tree values (<< /D [...] >>) and /GoTo actions + // (<< /S /GoTo /D [...] >>) put the destination under /D. An + // action of another kind -- /URI, /Launch -- has no destination in + // this document, and its /D, if it has one, is not one either. + if s := dict_name(doc, v, "S"); s != "" && s != "GoTo" { + return DESTINATION_NONE, false + } + d := dict_get(doc, v, "D") + if is_null(d) { + return DESTINATION_NONE, false + } + o = d + case: + return DESTINATION_NONE, false + } + } + return DESTINATION_NONE, false +} + +// dest_by_name looks a named destination up in both places one can live. +@(private) +dest_by_name :: proc(doc: ^Document, key: []byte) -> (Object, bool) { + if doc == nil || doc.catalog == nil { + return nil, false + } + // PDF 1.1: a flat /Dests dictionary in the catalog, keyed by name. + if dests := dict_dict(doc, doc.catalog, "Dests"); dests != nil { + if v := dict_get(doc, dests, string(key)); !is_null(v) { + return v, true + } + } + // PDF 1.2 and later: the /Names /Dests name tree, keyed by string. The two + // key spaces are distinct in the format and identical in practice, so a + // name is looked up in the tree and a string in the dictionary as well -- + // producers do mix them, and the bytes are the same either way. + if names := dict_dict(doc, doc.catalog, "Names"); names != nil { + if tree := dict_dict(doc, names, "Dests"); tree != nil { + if v := name_tree_lookup(doc, tree, key); !is_null(v) { + return v, true + } + } + } + return nil, false +} + +// dest_from_array reads the explicit form, [page /XYZ left top zoom]. An empty +// array names nothing at all and is the one array that is not a destination. +@(private) +dest_from_array :: proc(doc: ^Document, a: ^Array) -> (Destination, bool) { + dest := DESTINATION_NONE + if array_len(a) == 0 { + return dest, false + } + + // The page is a reference in a destination inside this document and an + // integer index in one that names a page of another file (a /GoToR + // action's /D). Files use the integer form locally too. + first := array_get_raw(a, 0) + if ref, is_ref := to_ref(first); is_ref { + dest.page = page_index_of(doc, ref) + } else if n, is_int := to_int(resolve(doc, first)); is_int { + if n >= 0 && n < i64(page_count(doc)) { + dest.page = int(n) + } + } + + // A missing or unknown view name leaves .XYZ with nothing set, which means + // "this page, view unchanged" -- the least surprising thing to do with a + // destination whose second element a producer got wrong. + kind, known := dest_kind_from_name(array_name(doc, a, 1)) + if !known { + return dest, true + } + dest.kind = kind + + for i in 0 ..< dest_arg_count(kind) { + v, is_num := to_number(array_get(doc, a, 2 + i)) + // A null argument is the format's "leave this unchanged", and a + // non-finite one is a producer bug with the same only sane reading. + if !is_num || !is_finite(v) { + continue + } + // "A zoom value of 0 has the same meaning as a null value" + // (ISO 32000-2, 12.3.2.2, Table 151). Producers write it constantly -- + // it is what every /XYZ in the PDF specification itself carries -- and + // a viewer that took it literally would zoom to nothing. + if kind == .XYZ && i == 2 && v == 0 { + continue + } + dest.args[i] = v + dest.has[i] = true + } + return dest, true +} + +// --------------------------------------------------------------------------- +// The outline +// --------------------------------------------------------------------------- + +// Outline_Item is one bookmark. +// +// `page` is `dest.page` repeated, because a viewer building a table of +// contents wants the page and nothing else nine times out of ten. +Outline_Item :: struct { + title: string, // decoded text, never nil, possibly empty + page: int, // resolved page index, or -1 + dest: Destination, + open: bool, // /Count > 0: this item's children are shown expanded + children: []Outline_Item, +} + +// outline_load reads the document outline. +// +// The whole tree is returned, in the order the file links it. A document with +// no outline gives an empty slice and no error -- that is the common case, not +// a failure. +// +// The result is the caller's, allocated with `allocator`: the item slices and +// every title string. Free it with `outline_destroy`, passing the same +// allocator. +outline_load :: proc(doc: ^Document, allocator := context.allocator) -> ([]Outline_Item, Error) { + if doc == nil { + return nil, .Invalid_Argument + } + root := dict_dict(doc, doc.catalog, "Outlines") + if root == nil { + return nil, .None + } + + // Keyed by dictionary address, which is stable: get_object caches by + // object number, so the same reference always yields the same ^Dict. An + // item reached twice is a damaged tree, and taking it once is what stops + // both a /Next cycle and a /First that points back at an ancestor. + visited := make(map[^Dict]bool, 64, context.allocator) + defer delete(visited) + visited[root] = true + + budget := MAX_OUTLINE_ITEMS + items := outline_children(doc, root, 0, &visited, &budget, allocator) + return items, .None +} + +// outline_destroy frees what outline_load returned. `allocator` must be the +// one it was given. +outline_destroy :: proc(items: []Outline_Item, allocator := context.allocator) { + for it in items { + delete(it.title, allocator) + outline_destroy(it.children, allocator) + } + if items != nil { + delete(items, allocator) + } +} + +// outline_children walks one /First .. /Next chain. +@(private) +outline_children :: proc( + doc: ^Document, parent: ^Dict, depth: int, + visited: ^map[^Dict]bool, budget: ^int, allocator: mem.Allocator, +) -> []Outline_Item { + if depth >= MAX_OUTLINE_DEPTH || budget^ <= 0 { + if depth >= MAX_OUTLINE_DEPTH { + add_repair(doc, .Recursion_Limit_Hit, 0) + } + return nil + } + + // Gathered in scratch and copied out exactly, so the returned slice has no + // spare capacity for the caller's `delete` to disagree about. + list := make([dynamic]Outline_Item, 0, 8, context.allocator) + defer delete(list) + + node := dict_dict(doc, parent, "First") + for i := 0; node != nil && i < MAX_OUTLINE_SIBLINGS; i += 1 { + if budget^ <= 0 { + break + } + if visited[node] or_else false { + // A /Next or /First that points at an item already in the tree. + add_repair(doc, .Bad_Page_Object, 0) + break + } + visited[node] = true + budget^ -= 1 + + append(&list, outline_item(doc, node, depth, visited, budget, allocator)) + node = dict_dict(doc, node, "Next") + } + + if len(list) == 0 { + return nil + } + out := make([]Outline_Item, len(list), allocator) + copy(out, list[:]) + return out +} + +// outline_item reads one node and, recursively, its children. +@(private) +outline_item :: proc( + doc: ^Document, node: ^Dict, depth: int, + visited: ^map[^Dict]bool, budget: ^int, allocator: mem.Allocator, +) -> (item: Outline_Item) { + item.page = -1 + item.dest = DESTINATION_NONE + + // decode_text_string always allocates, so a missing or non-string /Title + // still gives a string outline_destroy can free unconditionally. + raw, _ := to_bytes(dict_get(doc, node, "Title")) + item.title = decode_text_string(raw, allocator) + + // /Count is the number of visible descendants; its sign is what says + // whether this item is open (ISO 32000-2, 12.3.3). Zero means no children, + // for which "closed" is the only sensible answer. + item.open = dict_int(doc, node, "Count", 0) > 0 + + // /Dest first, then a /GoTo action's /D: a file may carry both, and /Dest + // is the one the format says wins. + target := dict_get(doc, node, "Dest") + if is_null(target) { + if a := dict_dict(doc, node, "A"); a != nil { + target = a + } + } + if !is_null(target) { + if d, ok := dest_resolve(doc, target); ok { + item.dest = d + } + } + item.page = item.dest.page + + item.children = outline_children(doc, node, depth + 1, visited, budget, allocator) + return item +} + +// --------------------------------------------------------------------------- +// Page labels +// --------------------------------------------------------------------------- + +// page_label is the label a viewer shows for page `index`: "iv" for the fourth +// page of a front matter numbered in lowercase Roman, "A-3" for a page in an +// appendix, "4" when the file says nothing. +// +// The catalog's /PageLabels number tree divides the document into ranges, each +// keyed by the index of its first page and each saying how that range is +// numbered: /S is the style (D, r, R, a, A), /P a prefix put in front of every +// label in the range, and /St the number the range starts at (ISO 32000-2, +// 12.4.2). A range with a /P and no /S labels every one of its pages with the +// prefix alone, which is legal and is how a cover page gets called "Cover". +// +// The result is always freshly allocated with `allocator` and owned by the +// caller, empty string included. +// +// Each call reads the tree: cheap, because a document has a handful of ranges +// and not one per page, but a caller labelling all 900 pages of a long +// document should keep the answers rather than ask again. +page_label :: proc(doc: ^Document, index: int, allocator := context.allocator) -> string { + b := strings.builder_make(0, 16, allocator) + if doc == nil || index < 0 { + return strings.to_string(b) + } + + entry, start, found := page_label_range(doc, index) + if !found { + // No /PageLabels, or none covering this page: the page's own position + // is what every viewer falls back to. + strings.write_int(&b, index + 1) + return strings.to_string(b) + } + + if raw := dict_bytes(doc, entry, "P"); len(raw) > 0 { + prefix := decode_text_string(raw[:min(len(raw), MAX_LABEL_PREFIX)], context.allocator) + defer delete(prefix, context.allocator) + strings.write_string(&b, prefix) + } + + style := dict_name(doc, entry, "S") + if style == "" { + return strings.to_string(b) + } + + // /St is required to be at least 1. Clamped rather than rejected, and + // clamped at the top as well so the arithmetic below cannot overflow. + st := clamp(dict_int(doc, entry, "St", 1), 1, 1 << 31) + n := st + i64(index) - start + + switch style { + case "D": + strings.write_i64(&b, n) + case "r": + label_write_roman(&b, n, false) + case "R": + label_write_roman(&b, n, true) + case "a": + label_write_alpha(&b, n, false) + case "A": + label_write_alpha(&b, n, true) + } + // An unknown style contributes nothing: the prefix is still a label, and + // guessing decimal would put a number on a page the file did not number. + return strings.to_string(b) +} + +// page_label_range finds the /PageLabels entry covering `index`: the one whose +// key is the largest that is not greater than it. +@(private) +page_label_range :: proc(doc: ^Document, index: int) -> (entry: ^Dict, start: i64, found: bool) { + root := dict_dict(doc, doc.catalog, "PageLabels") + if root == nil { + return nil, 0, false + } + + keys, values := number_tree_collect(doc, root, context.allocator) + defer delete(keys, context.allocator) + defer delete(values, context.allocator) + + // Scanned rather than binary-searched: the /Nums arrays are required to be + // in increasing order and damaged files are not, and a document has a + // handful of ranges, not thousands. + for k, i in keys { + if k < 0 || k > i64(index) { + continue + } + if found && k < start { + continue + } + d, is_dict := to_dict(resolve(doc, values[i])) + if !is_dict { + continue + } + entry, start, found = d, k, true + } + return +} + +// label_write_roman writes 1 as "i", 4 as "iv", 1990 as "mcmxc". +@(private) +label_write_roman :: proc(b: ^strings.Builder, n: i64, upper: bool) { + if n <= 0 { + return + } + if n > MAX_LABEL_ROMAN { + strings.write_i64(b, n) + return + } + values := [?]i64{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} + lower := [?]string{"m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"} + upper_ := [?]string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"} + rest := n + for v, i in values { + for rest >= v { + strings.write_string(b, upper ? upper_[i] : lower[i]) + rest -= v + } + } +} + +// label_write_alpha writes 1 as "a", 26 as "z", 27 as "aa", 53 as "aaa" -- the +// format's own scheme, which repeats the letter rather than carrying (12.4.2). +@(private) +label_write_alpha :: proc(b: ^strings.Builder, n: i64, upper: bool) { + if n <= 0 { + return + } + letter := byte((n - 1) % 26) + (upper ? 'A' : 'a') + reps := int(min((n - 1) / 26 + 1, MAX_LABEL_LETTERS)) + for _ in 0 ..< reps { + strings.write_byte(b, letter) + } +} diff --git a/outline_test.odin b/outline_test.odin new file mode 100644 index 0000000..7318750 --- /dev/null +++ b/outline_test.odin @@ -0,0 +1,862 @@ +package pdf + +import "core:fmt" +import "core:strings" +import "core:testing" + +// --------------------------------------------------------------------------- +// Outline, destinations and page labels +// +// These build whole files rather than Documents by hand: an outline is a +// linked structure spread over a dozen objects, and the cases worth testing -- +// a /Next that loops, a destination naming a deleted page -- only exist once +// the references are real. +// --------------------------------------------------------------------------- + +// ol_finish closes off a file whose objects are already written, with object 1 +// as the catalog. +@(private = "file") +ol_finish :: proc(pb: ^Pdf_Builder) { + at := pb_classic_xref(pb) + pb_str(pb, "trailer\n<< /Size ") + pb_num(pb, len(pb.offs)) + pb_str(pb, " /Root 1 0 R >>\n") + pb_startxref(pb, at) +} + +// ol_pages writes the page tree every test here shares: three pages, objects +// 3, 4 and 5. `catalog_extra` goes into the catalog dictionary. +@(private = "file") +ol_pages :: proc(pb: ^Pdf_Builder, catalog_extra: string) { + pb_obj(pb, 1, fmt.tprintf("<< /Type /Catalog /Pages 2 0 R %s >>", catalog_extra)) + pb_obj(pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>") + pb_obj(pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") + pb_obj(pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") + pb_obj(pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") +} + +// ol_titles joins the titles of one level, so a test can state the order it +// expects as one string instead of a loop of index assertions. +@(private = "file") +ol_titles :: proc(items: []Outline_Item, allocator := context.allocator) -> string { + b := strings.builder_make(0, 32, allocator) + for it, i in items { + if i > 0 { + strings.write_byte(&b, ',') + } + strings.write_string(&b, it.title) + } + return strings.to_string(b) +} + +@(private = "file") +ol_expect_titles :: proc(t: ^testing.T, items: []Outline_Item, want: string, where_: string) { + got := ol_titles(items) + defer delete(got) + testing.expectf(t, got == want, "%s: titles %q, wanted %q", where_, got, want) +} + +// --------------------------------------------------------------------------- +// The tree +// --------------------------------------------------------------------------- + +@(test) +test_outline_three_levels :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R /Last 12 0 R /Count 2 >>") + pb_obj(&pb, 11, "<< /Title (One) /Parent 10 0 R /Next 12 0 R /Count 1 " + + "/First 13 0 R /Last 13 0 R /Dest [3 0 R /Fit] >>") + pb_obj(&pb, 12, "<< /Title (Two) /Parent 10 0 R /Prev 11 0 R /Count -1 " + + "/First 16 0 R /Last 16 0 R /Dest [4 0 R /XYZ 10 20 0] >>") + pb_obj(&pb, 13, "<< /Title (One.A) /Parent 11 0 R /Next 14 0 R /Dest [5 0 R /FitH 700] >>") + pb_obj(&pb, 14, "<< /Title (One.B) /Parent 11 0 R /Prev 13 0 R /Count 1 /First 15 0 R /Last 15 0 R >>") + pb_obj(&pb, 15, "<< /Title (One.B.i) /Parent 14 0 R >>") + pb_obj(&pb, 16, "<< /Title (Two.A) /Parent 12 0 R >>") + ol_finish(&pb) + + doc, err := open_bytes(pb.buf[:]) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + items, oerr := outline_load(doc) + defer outline_destroy(items) + testing.expect(t, oerr == .None, "outline_load failed") + + ol_expect_titles(t, items, "One,Two", "top level") + if len(items) != 2 { return } + + // /Count 1 is open, /Count -1 is the same subtree collapsed. + testing.expect(t, items[0].open, "One should be open") + testing.expect(t, !items[1].open, "Two should be closed") + + // The linked chain 13 -> 14 is two children even though /Last names only + // the first: a walk that trusted /Last would stop one short. + ol_expect_titles(t, items[0].children, "One.A,One.B", "One's children") + ol_expect_titles(t, items[1].children, "Two.A", "Two's children") + if len(items[0].children) != 2 { return } + + ol_expect_titles(t, items[0].children[1].children, "One.B.i", "level three") + + testing.expectf(t, items[0].page == 0, "One page %v, wanted 0", items[0].page) + testing.expectf(t, items[1].page == 1, "Two page %v, wanted 1", items[1].page) + testing.expectf(t, items[0].children[0].page == 2, + "One.A page %v, wanted 2", items[0].children[0].page) + testing.expect(t, items[0].dest.kind == .Fit, "One's destination is not /Fit") + testing.expect(t, items[0].children[0].dest.kind == .Fit_H, "One.A's destination is not /FitH") + testing.expectf(t, items[0].children[0].dest.args[0] == 700, + "FitH top %v, wanted 700", items[0].children[0].dest.args[0]) + + // An item with no /Dest and no /A has no page. + testing.expectf(t, items[0].children[1].page == -1, + "One.B page %v, wanted -1", items[0].children[1].page) +} + +@(test) +test_outline_next_cycle_terminates :: proc(t: ^testing.T) { + // 11 -> 12 -> 11. A walk without a visited set never returns from this. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R /Last 12 0 R >>") + pb_obj(&pb, 11, "<< /Title (A) /Next 12 0 R >>") + pb_obj(&pb, 12, "<< /Title (B) /Next 11 0 R >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, oerr := outline_load(doc) + defer outline_destroy(items) + testing.expect(t, oerr == .None, "outline_load failed") + ol_expect_titles(t, items, "A,B", "cycle") + testing.expect(t, doc.is_damaged, "a cyclic /Next recorded no repair") +} + +@(test) +test_outline_first_cycle_terminates :: proc(t: ^testing.T) { + // /First pointing back at an ancestor: the same trap one level down. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + pb_obj(&pb, 11, "<< /Title (A) /First 12 0 R >>") + pb_obj(&pb, 12, "<< /Title (B) /First 11 0 R >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + ol_expect_titles(t, items, "A", "root") + if len(items) != 1 { return } + ol_expect_titles(t, items[0].children, "B", "A's children") + if len(items[0].children) != 1 { return } + testing.expectf(t, len(items[0].children[0].children) == 0, + "B has %v children, wanted 0", len(items[0].children[0].children)) +} + +@(test) +test_outline_self_next :: proc(t: ^testing.T) { + // The tightest loop there is: an item whose /Next is itself. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + pb_obj(&pb, 11, "<< /Title (A) /Next 11 0 R >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + ol_expect_titles(t, items, "A", "self-linked item") +} + +@(test) +test_outline_first_not_a_dict :: proc(t: ^testing.T) { + // /First as a number, as a reference to a number, and as a reference to + // nothing at all. None of the three is an outline item. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 42 /Last 42 /Count 3 >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, oerr := outline_load(doc) + defer outline_destroy(items) + testing.expect(t, oerr == .None, "a broken /First is not an error") + testing.expectf(t, len(items) == 0, "got %v items from a numeric /First", len(items)) + + pb2 := pb_make() + defer pb_destroy(&pb2) + ol_pages(&pb2, "/Outlines 10 0 R") + pb_obj(&pb2, 10, "<< /Type /Outlines /First 11 0 R >>") + pb_obj(&pb2, 11, "<< /Title (A) /First 12 0 R /Next 13 0 R >>") + pb_obj(&pb2, 12, "7") + pb_obj(&pb2, 13, "<< /Title (B) /First 99 0 R >>") + ol_finish(&pb2) + + doc2, _ := open_bytes(pb2.buf[:]) + defer close(doc2) + if doc2 == nil { return } + + items2, _ := outline_load(doc2) + defer outline_destroy(items2) + ol_expect_titles(t, items2, "A,B", "siblings past a broken /First") + if len(items2) != 2 { return } + testing.expectf(t, len(items2[0].children) == 0, + "A has %v children from an integer /First", len(items2[0].children)) + testing.expectf(t, len(items2[1].children) == 0, + "B has %v children from a dangling /First", len(items2[1].children)) +} + +@(test) +test_outline_missing_and_broken_roots :: proc(t: ^testing.T) { + // No /Outlines at all: an empty outline, and not an error. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, err := outline_load(doc) + defer outline_destroy(items) + testing.expect(t, err == .None, "a document with no outline reported an error") + testing.expectf(t, len(items) == 0, "got %v items from no outline", len(items)) + + // /Outlines pointing at an array. + pb2 := pb_make() + defer pb_destroy(&pb2) + ol_pages(&pb2, "/Outlines 10 0 R") + pb_obj(&pb2, 10, "[1 2 3]") + ol_finish(&pb2) + + doc2, _ := open_bytes(pb2.buf[:]) + defer close(doc2) + if doc2 == nil { return } + + items2, err2 := outline_load(doc2) + defer outline_destroy(items2) + testing.expect(t, err2 == .None, "a non-dictionary /Outlines is not an error") + testing.expectf(t, len(items2) == 0, "got %v items from an array /Outlines", len(items2)) + + nil_items, nil_err := outline_load(nil) + testing.expect(t, nil_err == .Invalid_Argument, "outline_load(nil) should be a caller error") + testing.expect(t, nil_items == nil, "outline_load(nil) returned items") + outline_destroy(nil) +} + +@(test) +test_outline_depth_capped :: proc(t: ^testing.T) { + // A chain of 200 nested /First links. The walk must stop at + // MAX_OUTLINE_DEPTH rather than recursing to the bottom. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + for i in 0 ..< 200 { + num := u32(11 + i) + if i == 199 { + pb_obj(&pb, num, fmt.tprintf("<< /Title (L%d) >>", i)) + } else { + pb_obj(&pb, num, fmt.tprintf("<< /Title (L%d) /First %d 0 R >>", i, num + 1)) + } + } + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + + depth := 0 + level := items + for len(level) > 0 && depth < 1000 { + depth += 1 + level = level[0].children + } + testing.expectf(t, depth == MAX_OUTLINE_DEPTH, + "walked %v levels, wanted the cap of %v", depth, MAX_OUTLINE_DEPTH) + testing.expectf(t, depth < 200, "the depth cap did not apply: %v levels", depth) +} + +@(test) +test_outline_titles_decoded :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + // UTF-16BE with a byte order mark: "Hi". + pb_obj(&pb, 11, "<< /Title /Next 12 0 R >>") + // 0x92 in PDFDocEncoding is a trademark sign, not the Latin-1 character. + pb_obj(&pb, 12, "<< /Title (a\\222b) /Next 13 0 R >>") + // No /Title at all, and a /Title that is not a string. + pb_obj(&pb, 13, "<< /Next 14 0 R >>") + pb_obj(&pb, 14, "<< /Title /NotAString >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + testing.expectf(t, len(items) == 4, "got %v items, wanted 4", len(items)) + if len(items) != 4 { return } + + testing.expectf(t, items[0].title == "Hi", "UTF-16 title %q", items[0].title) + testing.expectf(t, items[1].title == "aâ„¢b", "PDFDoc title %q", items[1].title) + testing.expectf(t, items[2].title == "", "missing title %q", items[2].title) + testing.expectf(t, items[3].title == "", "non-string title %q", items[3].title) +} + +@(test) +test_outline_action_destinations :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + // A /GoTo action rather than a /Dest. + pb_obj(&pb, 11, "<< /Title (A) /Next 12 0 R /A << /S /GoTo /D [4 0 R /Fit] >> >>") + // A /URI action goes nowhere in this document. + pb_obj(&pb, 12, "<< /Title (B) /Next 13 0 R /A << /S /URI /URI (http://example.com) >> >>") + // Both: /Dest wins (ISO 32000-2, 12.3.3). + pb_obj(&pb, 13, "<< /Title (C) /Next 14 0 R /Dest [5 0 R /Fit] /A << /S /GoTo /D [3 0 R /Fit] >> >>") + // A /GoToR destination is a page of another file. Taking its /D as a page + // of this one would send the reader somewhere arbitrary. + pb_obj(&pb, 14, "<< /Title (D) /A << /S /GoToR /F (other.pdf) /D [1 /Fit] >> >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + testing.expectf(t, len(items) == 4, "got %v items, wanted 4", len(items)) + if len(items) != 4 { return } + + testing.expectf(t, items[0].page == 1, "/GoTo action page %v, wanted 1", items[0].page) + testing.expectf(t, items[1].page == -1, "/URI action page %v, wanted -1", items[1].page) + testing.expectf(t, items[2].page == 2, "/Dest should win, page %v, wanted 2", items[2].page) + testing.expectf(t, items[3].page == -1, "/GoToR action page %v, wanted -1", items[3].page) +} + +// --------------------------------------------------------------------------- +// Destinations +// --------------------------------------------------------------------------- + +// ol_dest_doc builds a file whose object 30 is a dictionary of destinations to +// be resolved by key, so one document serves a table of cases. +@(private = "file") +ol_dest_doc :: proc(pb: ^Pdf_Builder, catalog_extra, dests_body: string) -> (^Document, ^Dict) { + ol_pages(pb, catalog_extra) + pb_obj(pb, 30, dests_body) + ol_finish(pb) + doc, _ := open_bytes(pb.buf[:]) + if doc == nil { return nil, nil } + return doc, get_dict(doc, Ref{30, 0}) +} + +@(test) +test_dest_explicit_array :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + doc, cases := ol_dest_doc(&pb, "", `<< + /a [3 0 R /XYZ 10 20 1.5] + /b [4 0 R /Fit] + /c [5 0 R /FitR 1 2 3 4] + /d [4 0 R /FitBV 90] + /e [5 0 R] + /f [4 0 R /Bogus 1 2] + /g [2 0 R /Fit] + /h [] + >>`) + defer close(doc) + if doc == nil || cases == nil { return } + + d, ok := dest_resolve(doc, dict_get(doc, cases, "a")) + testing.expect(t, ok, "/XYZ did not resolve") + testing.expectf(t, d.page == 0 && d.kind == .XYZ, "XYZ page %v kind %v", d.page, d.kind) + testing.expectf(t, d.args == [4]f64{10, 20, 1.5, 0}, "XYZ args %v", d.args) + testing.expectf(t, d.has == [4]bool{true, true, true, false}, "XYZ has %v", d.has) + + d, ok = dest_resolve(doc, dict_get(doc, cases, "b")) + testing.expect(t, ok && d.page == 1 && d.kind == .Fit, "/Fit did not resolve") + testing.expectf(t, d.has == [4]bool{}, "/Fit takes no arguments, has %v", d.has) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) + testing.expectf(t, d.kind == .Fit_R && d.args == [4]f64{1, 2, 3, 4}, + "/FitR kind %v args %v", d.kind, d.args) + testing.expectf(t, d.has == [4]bool{true, true, true, true}, "/FitR has %v", d.has) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) + testing.expectf(t, d.kind == .Fit_BV && d.args[0] == 90 && d.has[0] && !d.has[1], + "/FitBV kind %v args %v has %v", d.kind, d.args, d.has) + + // A page and no view name: the page, with the view left alone. + d, ok = dest_resolve(doc, dict_get(doc, cases, "e")) + testing.expect(t, ok, "a one-element destination did not resolve") + testing.expectf(t, d.page == 2 && d.kind == .XYZ && d.has == [4]bool{}, + "bare page destination %v", d) + + // An unknown view name is not a reason to lose the page. + d, ok = dest_resolve(doc, dict_get(doc, cases, "f")) + testing.expect(t, ok, "an unknown view name failed the whole destination") + testing.expectf(t, d.page == 1 && d.has == [4]bool{}, "unknown view %v", d) + + // A reference to something that is not a page. + d, _ = dest_resolve(doc, dict_get(doc, cases, "g")) + testing.expectf(t, d.page == -1, "the page tree root resolved to page %v", d.page) + + _, ok = dest_resolve(doc, dict_get(doc, cases, "h")) + testing.expect(t, !ok, "an empty destination array resolved") + + _, ok = dest_resolve(doc, dict_get(doc, cases, "nope")) + testing.expect(t, !ok, "a missing destination resolved") + _, ok = dest_resolve(nil, nil) + testing.expect(t, !ok, "dest_resolve(nil) resolved") +} + +@(test) +test_dest_xyz_nulls :: proc(t: ^testing.T) { + // /XYZ null 700 null: keep the current left and zoom, scroll to y=700. + // The distinction is not one a value can carry -- 0 is a legal coordinate + // and a legal zoom means "unchanged" as well -- so `has` is the only place + // it can live. + pb := pb_make() + defer pb_destroy(&pb) + doc, cases := ol_dest_doc(&pb, "", `<< + /a [3 0 R /XYZ null 700 null] + /b [3 0 R /XYZ 0 0 0] + /c [3 0 R /XYZ] + /d [3 0 R /XYZ 5] + /e [3 0 R /XYZ 0 0 1.5] + /f [3 0 R /FitR 0 0 0 0] + >>`) + defer close(doc) + if doc == nil || cases == nil { return } + + d, _ := dest_resolve(doc, dict_get(doc, cases, "a")) + testing.expectf(t, d.has == [4]bool{false, true, false, false}, "null XYZ has %v", d.has) + testing.expectf(t, d.args[1] == 700, "null XYZ top %v, wanted 700", d.args[1]) + testing.expectf(t, d.args[0] == 0 && d.args[2] == 0, + "an absent argument left a value behind: %v", d.args) + + // Zero left and zero top are coordinates; a zero zoom is the format's own + // spelling of null (ISO 32000-2, 12.3.2.2), and every /XYZ in the PDF + // specification's own bookmarks is written that way. + d, _ = dest_resolve(doc, dict_get(doc, cases, "b")) + testing.expectf(t, d.has == [4]bool{true, true, false, false}, + "zero zoom must read as unchanged, has %v", d.has) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "e")) + testing.expectf(t, d.has == [4]bool{true, true, true, false} && d.args[2] == 1.5, + "a real zoom was lost: args %v has %v", d.args, d.has) + + // The rule is /XYZ's alone: /FitR's numbers are coordinates and a zero + // among them is a real one. + d, _ = dest_resolve(doc, dict_get(doc, cases, "f")) + testing.expectf(t, d.has == [4]bool{true, true, true, true} && d.args == [4]f64{0, 0, 0, 0}, + "/FitR zeros %v has %v", d.args, d.has) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) + testing.expectf(t, d.has == [4]bool{}, "/XYZ with no arguments has %v", d.has) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) + testing.expectf(t, d.has == [4]bool{true, false, false, false}, + "/XYZ truncated after one argument has %v", d.has) +} + +@(test) +test_dest_missing_page :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + doc, cases := ol_dest_doc(&pb, "", `<< + /gone [99 0 R /XYZ 1 2 3] + /free [7 0 R /Fit] + /nth 3 + >>`) + defer close(doc) + if doc == nil || cases == nil { return } + + d, ok := dest_resolve(doc, dict_get(doc, cases, "gone")) + testing.expect(t, ok, "a destination to a deleted page should still resolve") + testing.expectf(t, d.page == -1, "deleted page resolved to %v, wanted -1", d.page) + // The view survives, so a viewer can still say where it would have gone. + testing.expectf(t, d.kind == .XYZ && d.args[0] == 1, "view lost with the page: %v", d) + + d, _ = dest_resolve(doc, dict_get(doc, cases, "free")) + testing.expectf(t, d.page == -1, "a free object resolved to page %v", d.page) + + // A destination is not a bare number, whatever it names. + _, ok = dest_resolve(doc, dict_get(doc, cases, "nth")) + testing.expect(t, !ok, "a number resolved as a destination") +} + +@(test) +test_dest_page_by_number :: proc(t: ^testing.T) { + // The remote form, [pageIndex /Fit], which producers use locally too. + pb := pb_make() + defer pb_destroy(&pb) + doc, cases := ol_dest_doc(&pb, "", `<< + /a [2 /Fit] + /b [0 /FitH 100] + /c [9 /Fit] + /d [-1 /Fit] + >>`) + defer close(doc) + if doc == nil || cases == nil { return } + + d, _ := dest_resolve(doc, dict_get(doc, cases, "a")) + testing.expectf(t, d.page == 2, "index 2 gave page %v", d.page) + d, _ = dest_resolve(doc, dict_get(doc, cases, "b")) + testing.expectf(t, d.page == 0 && d.kind == .Fit_H, "index 0 gave %v", d) + d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) + testing.expectf(t, d.page == -1, "index 9 of a 3-page document gave %v", d.page) + d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) + testing.expectf(t, d.page == -1, "index -1 gave %v", d.page) +} + +@(test) +test_dest_named_through_dests_dict :: proc(t: ^testing.T) { + // PDF 1.1: the catalog's /Dests dictionary. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Dests 20 0 R") + pb_obj(&pb, 20, "<< /chap1 [4 0 R /FitH 500] /chap2 21 0 R /chap3 << /D [5 0 R /Fit] >> >>") + pb_obj(&pb, 21, "[3 0 R /XYZ null null 2]") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + d, ok := dest_resolve(doc, Name("chap1")) + testing.expect(t, ok, "/chap1 did not resolve") + testing.expectf(t, d.page == 1 && d.kind == .Fit_H && d.args[0] == 500, "chap1 %v", d) + + // The same name written as a string, which files do. + d, ok = dest_resolve(doc, String(transmute([]byte)string("chap1"))) + testing.expectf(t, ok && d.page == 1, "chap1 as a string gave %v", d) + + // Through an indirect reference, and through a << /D ... >> wrapper. + d, ok = dest_resolve(doc, Name("chap2")) + testing.expectf(t, ok && d.page == 0 && d.has == [4]bool{false, false, true, false}, + "chap2 %v", d) + d, ok = dest_resolve(doc, Name("chap3")) + testing.expectf(t, ok && d.page == 2, "chap3 %v", d) + + _, ok = dest_resolve(doc, Name("chap9")) + testing.expect(t, !ok, "an undefined name resolved") +} + +@(test) +test_dest_named_through_name_tree :: proc(t: ^testing.T) { + // PDF 1.2 and later: the /Names /Dests name tree, with a /Kids level so + // the lookup has to descend rather than read one /Names array. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Names 20 0 R") + pb_obj(&pb, 20, "<< /Dests 21 0 R >>") + pb_obj(&pb, 21, "<< /Kids [22 0 R 23 0 R] >>") + pb_obj(&pb, 22, "<< /Limits [(alpha) (beta)] /Names [(alpha) [3 0 R /Fit] (beta) 24 0 R] >>") + pb_obj(&pb, 23, "<< /Limits [(gamma) (gamma)] /Names [(gamma) [5 0 R /FitV 12]] >>") + pb_obj(&pb, 24, "<< /D [4 0 R /XYZ 1 2 null] >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + d, ok := dest_resolve(doc, String(transmute([]byte)string("alpha"))) + testing.expectf(t, ok && d.page == 0 && d.kind == .Fit, "alpha %v", d) + + // A tree value that is a dictionary with /D, which is the form the format + // actually recommends for a named destination. + d, ok = dest_resolve(doc, String(transmute([]byte)string("beta"))) + testing.expectf(t, ok && d.page == 1 && d.has == [4]bool{true, true, false, false}, "beta %v", d) + + // In the second kid, so /Limits had to be read to get there. + d, ok = dest_resolve(doc, Name("gamma")) + testing.expectf(t, ok && d.page == 2 && d.kind == .Fit_V && d.args[0] == 12, "gamma %v", d) + + _, ok = dest_resolve(doc, String(transmute([]byte)string("delta"))) + testing.expect(t, !ok, "a name not in the tree resolved") +} + +@(test) +test_dest_name_cycle_terminates :: proc(t: ^testing.T) { + // A name whose destination is the same name, and a /D chain that never + // reaches an array. Both must stop. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Dests 20 0 R") + pb_obj(&pb, 20, "<< /loop /loop /deep 21 0 R >>") + pb_obj(&pb, 21, "<< /D << /D << /D << /D << /D [3 0 R /Fit] >> >> >> >> >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + _, ok := dest_resolve(doc, Name("loop")) + testing.expect(t, !ok, "a self-naming destination resolved") + _, ok = dest_resolve(doc, Name("deep")) + testing.expect(t, !ok, "a destination past the hop limit resolved") +} + +@(test) +test_outline_dest_named :: proc(t: ^testing.T) { + // The whole path: a bookmark whose /Dest is a name in the name tree. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/Outlines 10 0 R /Names 20 0 R") + pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") + pb_obj(&pb, 11, "<< /Title (Chapter) /Dest (ch1) /Next 12 0 R >>") + pb_obj(&pb, 12, "<< /Title (Appendix) /A << /S /GoTo /D /ch2 >> >>") + pb_obj(&pb, 20, "<< /Dests 21 0 R >>") + pb_obj(&pb, 21, "<< /Names [(ch1) [4 0 R /Fit] (ch2) [5 0 R /Fit]] >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + items, _ := outline_load(doc) + defer outline_destroy(items) + testing.expectf(t, len(items) == 2, "got %v items, wanted 2", len(items)) + if len(items) != 2 { return } + testing.expectf(t, items[0].page == 1, "named /Dest gave page %v, wanted 1", items[0].page) + testing.expectf(t, items[1].page == 2, "named action gave page %v, wanted 2", items[1].page) + testing.expect(t, items[0].dest.page == items[0].page, "item.page disagrees with dest.page") +} + +// --------------------------------------------------------------------------- +// Page labels +// --------------------------------------------------------------------------- + +@(private = "file") +ol_expect_label :: proc(t: ^testing.T, doc: ^Document, index: int, want: string) { + got := page_label(doc, index) + defer delete(got) + testing.expectf(t, got == want, "page_label(%v) = %q, wanted %q", index, got, want) +} + +@(test) +test_page_label_styles :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/PageLabels 20 0 R") + pb_obj(&pb, 20, `<< /Nums [ + 0 << /S /r >> + 4 << /S /D >> + 8 << /S /a >> + 12 << /S /A >> + 16 << /S /R >> + ] >>`) + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + ol_expect_label(t, doc, 0, "i") + ol_expect_label(t, doc, 3, "iv") + ol_expect_label(t, doc, 4, "1") + ol_expect_label(t, doc, 7, "4") + ol_expect_label(t, doc, 8, "a") + ol_expect_label(t, doc, 11, "d") + ol_expect_label(t, doc, 12, "A") + ol_expect_label(t, doc, 15, "D") + ol_expect_label(t, doc, 16, "I") + // Past the last range's start: the last range runs to the end of the file. + ol_expect_label(t, doc, 20, "V") + ol_expect_label(t, doc, 25, "X") + // Before any page: never a label. + ol_expect_label(t, doc, -1, "") +} + +@(test) +test_page_label_prefix_and_start :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/PageLabels 20 0 R") + pb_obj(&pb, 20, `<< /Nums [ + 0 << /P (Cover) >> + 1 << /S /D /P (A-) /St 7 >> + 4 << /S /r /St 3 >> + 6 << /S /D /St 0 >> + 8 << /P /S /D >> + ] >>`) + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + // A range with a prefix and no /S labels every page in it the same way. + ol_expect_label(t, doc, 0, "Cover") + // /St shifts the numbering; the prefix goes in front of every one. + ol_expect_label(t, doc, 1, "A-7") + ol_expect_label(t, doc, 3, "A-9") + ol_expect_label(t, doc, 4, "iii") + ol_expect_label(t, doc, 5, "iv") + // /St 0 is out of range and clamps to 1 rather than labelling a page "0". + ol_expect_label(t, doc, 6, "1") + ol_expect_label(t, doc, 7, "2") + // A UTF-16 prefix is a text string like any other. + ol_expect_label(t, doc, 8, "§1") +} + +@(test) +test_page_label_no_tree :: proc(t: ^testing.T) { + // With no /PageLabels the label is the page's own position, 1-based. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + ol_expect_label(t, doc, 0, "1") + ol_expect_label(t, doc, 41, "42") + ol_expect_label(t, doc, -3, "") + ol_expect_label(t, nil, 0, "") +} + +@(test) +test_page_label_broken_tree :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/PageLabels 20 0 R") + // Entries out of order, a key with a non-dictionary value, a negative key, + // an unknown style, and a range that starts after the pages under test. + pb_obj(&pb, 20, `<< /Nums [ + 4 << /S /D /St 100 >> + 0 << /S /D >> + 2 (not a dictionary) + -1 << /S /A >> + 3 << /S /Q /P (x) >> + 20 << /S /D /St 1 >> + ] >>`) + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + // Order in the file is not order in the tree: page 1 belongs to the range + // keyed 0 even though it is written second. + ol_expect_label(t, doc, 0, "1") + ol_expect_label(t, doc, 1, "2") + // The entry at key 2 is unusable, so page 2 falls back to the range at 0. + ol_expect_label(t, doc, 2, "3") + // An unknown /S numbers nothing; the prefix is still the label. + ol_expect_label(t, doc, 3, "x") + ol_expect_label(t, doc, 4, "100") + ol_expect_label(t, doc, 5, "101") + ol_expect_label(t, doc, 21, "2") + + // A /PageLabels that is not a tree at all. + pb2 := pb_make() + defer pb_destroy(&pb2) + ol_pages(&pb2, "/PageLabels 20 0 R") + pb_obj(&pb2, 20, "42") + ol_finish(&pb2) + + doc2, _ := open_bytes(pb2.buf[:]) + defer close(doc2) + if doc2 == nil { return } + ol_expect_label(t, doc2, 6, "7") +} + +@(test) +test_page_label_kids_tree :: proc(t: ^testing.T) { + // The /Kids form of a number tree, which is what a document with many + // ranges is written as. + pb := pb_make() + defer pb_destroy(&pb) + ol_pages(&pb, "/PageLabels 20 0 R") + pb_obj(&pb, 20, "<< /Kids [21 0 R 22 0 R] >>") + pb_obj(&pb, 21, "<< /Limits [0 0] /Nums [0 << /S /r >>] >>") + pb_obj(&pb, 22, "<< /Limits [5 5] /Nums [5 << /S /D /P (p) /St 2 >>] >>") + ol_finish(&pb) + + doc, _ := open_bytes(pb.buf[:]) + defer close(doc) + if doc == nil { return } + + ol_expect_label(t, doc, 2, "iii") + ol_expect_label(t, doc, 5, "p2") + ol_expect_label(t, doc, 6, "p3") +} + +@(test) +test_page_label_numerals :: proc(t: ^testing.T) { + // The numeral writers, driven directly: a range per case would be a very + // long file, and these are pure functions of one number. + check_roman :: proc(t: ^testing.T, n: i64, upper: bool, want: string) { + b := strings.builder_make(0, 16) + defer strings.builder_destroy(&b) + label_write_roman(&b, n, upper) + got := strings.to_string(b) + testing.expectf(t, got == want, "roman(%v, upper=%v) = %q, wanted %q", n, upper, got, want) + } + check_alpha :: proc(t: ^testing.T, n: i64, upper: bool, want: string) { + b := strings.builder_make(0, 16) + defer strings.builder_destroy(&b) + label_write_alpha(&b, n, upper) + got := strings.to_string(b) + testing.expectf(t, got == want, "alpha(%v, upper=%v) = %q, wanted %q", n, upper, got, want) + } + + check_roman(t, 1, false, "i") + check_roman(t, 4, false, "iv") + check_roman(t, 9, false, "ix") + check_roman(t, 14, false, "xiv") + check_roman(t, 40, false, "xl") + check_roman(t, 90, false, "xc") + check_roman(t, 400, false, "cd") + check_roman(t, 1990, false, "mcmxc") + check_roman(t, 2024, true, "MMXXIV") + check_roman(t, 3999, true, "MMMCMXCIX") + check_roman(t, 0, false, "") + check_roman(t, -5, false, "") + // Beyond the cap the numeral would be thousands of Ms, so it is decimal. + check_roman(t, 100000, false, "100000") + + check_alpha(t, 1, false, "a") + check_alpha(t, 26, false, "z") + check_alpha(t, 27, false, "aa") + check_alpha(t, 52, false, "zz") + check_alpha(t, 53, false, "aaa") + check_alpha(t, 2, true, "B") + check_alpha(t, 0, false, "") + // The repetition count is capped: a file asking for page 10^9 must not ask + // for a 38-megabyte label. + long := strings.builder_make(0, 128) + defer strings.builder_destroy(&long) + label_write_alpha(&long, 1_000_000_000, false) + testing.expectf(t, len(strings.to_string(long)) == MAX_LABEL_LETTERS, + "a huge alphabetic label was %v bytes", len(strings.to_string(long))) +} diff --git a/page.odin b/page.odin new file mode 100644 index 0000000..eb4d3a6 --- /dev/null +++ b/page.odin @@ -0,0 +1,600 @@ +package pdf + +import "core:math" +import "core:mem" +import "core:mem/virtual" + +// The public rendering API: what a user of this library calls once a Document +// is open. +// +// doc, _ := pdf.open_file("thesis.pdf") +// page, _ := pdf.page_get(doc, 0) +// bmp, _ := pdf.render_page(&page, {ctm = pdf.scale_matrix(2, 2)}) +// defer pdf.bitmap_destroy(&bmp) +// +// Everything below this file is already written: `content.odin` walks the +// operators, the three devices turn them into pixels, text or a display list. +// What is here is the small amount that only a *page* knows -- where the page +// sits in device space, how big a bitmap it needs, which scratch memory the +// run may use, and that a page is its content stream *plus* its annotations. +// +// --------------------------------------------------------------------------- +// THE BASE MATRIX +// +// This is the piece everything else depends on and the easiest to get subtly +// wrong, so it is derived once, in `page_base_matrix`, and every entry point +// here goes through it. That is what makes a rendered bitmap, extracted text +// and a display list agree about where things are instead of being three +// nearly-identical derivations that disagree in one corner. +// +// PDF user space is y-UP with its origin at the bottom-left of the CROP box. +// A bitmap is y-DOWN with its origin at the top-left. So the base transform, +// in order: +// +// 1. translates the crop box's lower-left corner to the origin; +// 2. scales by /UserUnit, the size in points of one unit of page space; +// 3. flips y and applies /Rotate, mapping the page onto a device rectangle +// whose top-left corner is (0,0); +// 4. applies the caller's `ctm`. +// +// Steps 1-3 are fixed by the page. Step 4 is the caller's zoom, and is what +// `Render_Options.ctm`, `run_page`'s `ctm` and `page_display_list`'s `ctm` all +// are: a transform applied ON TOP of the flip and the rotation, never instead +// of them. `MATRIX_IDENTITY` therefore renders the page upright at one device +// pixel per point, which is what a caller expects to get by passing nothing. +// +// The four rotations are written out as constants rather than built with +// `rotate_matrix`, because cos(pi/2) is 6.1e-17 and not 0: a page with +// /Rotate 90 must land on exact pixel boundaries, not 6.1e-17 off them. +// +// --------------------------------------------------------------------------- +// MEMORY +// +// Three kinds, and they must not be confused: +// +// * The Document's arena holds everything parsed out of the file -- fonts, +// images, colour spaces, the decoded content bytes. It is never freed +// before `close`, so nothing per-render may go in it. +// * Per-page scratch -- the operand stack, paths under construction, glyph +// runs, tile lattices, the strings the content lexer materialises -- goes +// in a `virtual.Arena` that is a LOCAL of the entry point that started the +// render. It is created when the call starts and destroyed when it +// returns, so a 900-page loop peaks at one page's scratch rather than +// growing without bound. That is the arena ARCHITECTURE.md asks for, and +// `page_scratch_begin`/`page_scratch_end` are where it lives. +// * The result -- `render_page`'s Bitmap, `extract_text`'s string, +// `page_display_list`'s list -- belongs to the CALLER, comes from the +// allocator the caller named, and is freed by the caller. +// +// The devices themselves allocate from `context.allocator` and free everything +// in their own `*_destroy`; they are built and torn down inside one call. +// +// --------------------------------------------------------------------------- +// WHAT A PAGE IS +// +// A page is its content stream AND its annotations. `render_page`, +// `extract_text` and `page_display_list` all draw both, because a form field's +// value and a stamp are as much a part of what the page looks like as the text +// under them -- /Annots is in half the corpus. `annot.odin` owns the placement +// and this file owns the order: content first, annotations over it, in +// /Annots order. `run_page` is the one exception and says so. + +// --------------------------------------------------------------------------- +// Options +// --------------------------------------------------------------------------- + +// Render_Options tunes one call to `render_page` or `render_page_to`. The zero +// value is the sensible default for all of them: an opaque white page, no +// extra transform, annotations drawn. +Render_Options :: struct { + // Applied on top of the page flip and /Rotate, never instead of them. The + // zero Matrix is not a transform, so it is read as MATRIX_IDENTITY. + ctm: Matrix, + + // RGBA, straight alpha. An all-zero value means "unset" -- which is what a + // zeroed Render_Options carries -- and is filled in as opaque white for an + // opaque page and left fully transparent when `alpha` is set. Any other + // value is used as given, with its alpha forced to 255 unless `alpha` is + // set, because an opaque page cannot start out see-through. + background: [4]u8, + + // Start the page transparent and keep whatever alpha the content leaves. + // The bitmap is RGBA8 either way; this decides what is underneath. + alpha: bool, + + // Draw the content stream only. The default draws the page's annotation + // appearance streams over it, which is what a viewer shows. + no_annotations: bool, + + // Where the returned Bitmap's pixels come from. Zero means + // context.allocator. It is not used for any scratch memory. + allocator: mem.Allocator, +} + +// --------------------------------------------------------------------------- +// The base matrix +// --------------------------------------------------------------------------- + +// page_base_matrix maps the page's user space to device space, with `ctm` +// applied on top. +// +// It is the transform every entry point in this file renders through, and the +// one a caller needs to relate what came out of `render_page` to what is in +// the file: `transform_point(page_base_matrix(&page, ctm), p)` is where user +// point `p` lands in the bitmap, and the inverse (via `matrix_invert`) turns a +// pixel a user clicked on back into a point on the page. +// +// The origin is the CROP box's lower-left corner, not the media box's: the +// crop box is the rectangle a viewer displays, and `render_page` sizes its +// bitmap to it. /UserUnit and /Rotate are both folded in, so a page with +// /Rotate 90 comes back landscape and the right way up. +page_base_matrix :: proc(page: ^Page, ctm := MATRIX_IDENTITY) -> Matrix { + m := ctm + if !matrix_is_finite(m) { + // A caller-supplied matrix is not file data, but it can be arithmetic + // on file data. A NaN here would silently blank the page. + m = MATRIX_IDENTITY + } + if page == nil { + return m + } + + crop := page_bounds(page) + unit := page.user_unit + if !is_finite(unit) || unit <= 0 { + unit = 1 + } + + // The page's extent in device units before rotation. + w := rect_width(crop) * unit + h := rect_height(crop) * unit + if !is_finite(w) || !is_finite(h) { + return m + } + + // Crop origin to (0,0), then /UserUnit. + base := Matrix{unit, 0, 0, unit, -crop.x0 * unit, -crop.y0 * unit} + + // The flip and the rotation in one step. Each maps the [0,w] x [0,h] y-up + // rectangle onto the y-down device rectangle a viewer would show, whose + // top-left corner is the origin. Every one has determinant -1: the y-flip + // reverses orientation once, and a rotation does not reverse it again. + flip: Matrix + switch page.rotate { + case 90: + // Clockwise: the page's left edge becomes the top of the image, so the + // device is h wide and w tall. + flip = Matrix{0, 1, 1, 0, 0, 0} + case 180: + flip = Matrix{-1, 0, 0, 1, w, 0} + case 270: + flip = Matrix{0, -1, -1, 0, h, w} + case: + flip = Matrix{1, 0, 0, -1, 0, h} + } + + return matrix_mul(matrix_mul(base, flip), m) +} + +// page_render_size is the pixel size `render_page` allocates for `ctm`. +// +// The bitmap spans device space from (0,0) to the far corner of the page under +// the full transform, so for the transforms callers actually use -- a scale, +// or nothing -- it is exactly `page_size`, /Rotate and /UserUnit included. A +// `ctm` that pushes content to negative coordinates pushes it off the bitmap +// rather than moving the bitmap: that is what makes `render_page_to` usable +// for tiled rendering, where each tile's `ctm` carries the translation that +// selects it. +@(private) +page_render_size :: proc(page: ^Page, ctm: Matrix) -> (width, height: int, err: Error) { + if page == nil { + return 0, 0, .Invalid_Argument + } + box := transform_rect(page_base_matrix(page, ctm), page_bounds(page)) + if !rect_is_finite(box) { + return 0, 0, .Invalid_Argument + } + + // Rounded outward with the same epsilon page_size uses: a page 100.2 + // pixels wide needs 101 columns, but an exact 200 that floating point + // rendered as 200.00000000000003 must not claim a 201st. + limit := f64(MAX_BITMAP_PIXELS) + if box.x1 > limit || box.y1 > limit { + return 0, 0, .Bitmap_Too_Large + } + width = max(1, int(math.ceil(box.x1 - 1e-9))) + height = max(1, int(math.ceil(box.y1 - 1e-9))) + if width > MAX_BITMAP_PIXELS / height { + return 0, 0, .Bitmap_Too_Large + } + return width, height, .None +} + +// --------------------------------------------------------------------------- +// Scratch memory +// --------------------------------------------------------------------------- + +// Page_Scratch is the per-page arena. It is held by value in the caller's +// frame so that it dies with the call: see the memory note at the top. +@(private) +Page_Scratch :: struct { + arena: virtual.Arena, + alloc: mem.Allocator, +} + +@(private) +page_scratch_begin :: proc(s: ^Page_Scratch) -> Error { + if virtual.arena_init_growing(&s.arena) != nil { + return .Out_Of_Memory + } + s.alloc = virtual.arena_allocator(&s.arena) + return .None +} + +@(private) +page_scratch_end :: proc(s: ^Page_Scratch) { + virtual.arena_destroy(&s.arena) + s^ = {} +} + +// --------------------------------------------------------------------------- +// Running a page into a device +// --------------------------------------------------------------------------- + +// run_page runs the page's content stream into `dev`. +// +// `ctm` is applied on top of the page flip and /Rotate, exactly like +// `Render_Options.ctm`: pass MATRIX_IDENTITY to draw the page upright at one +// device pixel per point. This is the entry point for a caller with a device +// of their own -- a GPU backend, a bounding-box collector, an SVG writer. +// +// Annotations are NOT drawn: this runs the page, and an annotation is a +// separate object with its own appearance stream. There is no option here to +// say otherwise, which is deliberate -- a caller that wants a page's +// annotations as device calls records the whole page with `page_display_list` +// and replays it, and a caller that wants only the content stream has this. +// +// The device's `close` hook is called when the page is finished, which is what +// balances any group or clip the content left open. Nothing about a malformed +// content stream is fatal: everything that could not be run is recorded as a +// Repair on the Document and the rest still draws. +run_page :: proc(page: ^Page, dev: ^Device, ctm: Matrix) -> Error { + if page == nil || page.doc == nil { + return .Invalid_Argument + } + m := page_ctm_or_identity(ctm) + if !matrix_is_finite(m) { + return .Invalid_Argument + } + + scratch: Page_Scratch + page_scratch_begin(&scratch) or_return + defer page_scratch_end(&scratch) + + return page_run(page, dev, m, &scratch, false, false) +} + +// page_run is the one implementation the four public entry points share: run +// the content, then the annotations, then close the device. +// +// `text` selects the settings text extraction wants and rendering does not -- +// chiefly drawing optional content the catalog's default configuration hides, +// because hidden text is still text. +@(private) +page_run :: proc(page: ^Page, dev: ^Device, ctm: Matrix, scratch: ^Page_Scratch, + annots: bool, text: bool) -> Error { + doc := page.doc + base := page_base_matrix(page, ctm) + + // The soft-mask and inline-image budgets are the page's, not each run's: + // the annotations below are separate `run_content` calls and would + // otherwise each get a fresh one. This lives in the page's scratch arena, + // which is exactly the lifetime `Content_Options.shared` asks for. + shared := new(Content_Shared, scratch.alloc) + opts := Content_Options{ + allocator = scratch.alloc, + ignore_optional_content = text, + shared = shared, + } + + // The content bytes are the concatenation of /Contents, which is scratch: + // a page whose content is 40 MB of Flate must not leave 40 MB in the + // Document's arena for the rest of the document's life. + content, cerr := page_content(page, scratch.alloc) + err := cerr + if cerr == .None && len(content) > 0 { + rerr := run_content(doc, dev, content, page.resources, base, opts) + if rerr != .None { + err = rerr + } + } + + // Annotations are `annot.odin`'s: an appearance stream is a form XObject + // placed on the annotation's /Rect, and it runs through the same + // interpreter under the same CTM the content stream had. Sharing `opts` + // shares this page's scratch arena and its token budget with them. + if annots { + annot_run_page(doc, page, dev, base, opts) + } + + dev_close(dev) + return err +} + +// --------------------------------------------------------------------------- +// Rendering to a bitmap +// --------------------------------------------------------------------------- + +// render_page rasterises a page and returns the bitmap. +// +// The result is RGBA8 with STRAIGHT (un-premultiplied) alpha, sized to the +// page's crop box under `opts.ctm` with /Rotate and /UserUnit already applied, +// so a page with /Rotate 90 comes back landscape and the right way up. A +// request larger than MAX_BITMAP_PIXELS is refused with .Bitmap_Too_Large +// rather than attempted. +// +// THE BITMAP BELONGS TO THE CALLER. It is allocated from `opts.allocator` +// (context.allocator when that is zero) and must be released with +// `bitmap_destroy`. Nothing else the call touches survives it: the scratch +// arena and the raster device are both gone by the time it returns. +// +// A page that will not draw still returns a bitmap. Malformed content records +// Repairs on the Document and draws what it can, so a page that is 90% +// drawable comes back 90% drawn; an error return means the bitmap could not be +// made at all. +render_page :: proc(page: ^Page, opts := Render_Options{}) -> (Bitmap, Error) { + if page == nil || page.doc == nil { + return {}, .Invalid_Argument + } + ctm := page_ctm_or_identity(opts.ctm) + if !matrix_is_finite(ctm) { + return {}, .Invalid_Argument + } + + width, height, serr := page_render_size(page, ctm) + if serr != .None { + return {}, serr + } + + alloc := opts.allocator + if alloc.procedure == nil { + alloc = context.allocator + } + + bmp, berr := bitmap_make(width, height, 4, alloc) + if berr != .None { + return {}, berr + } + + resolved := opts + resolved.ctm = ctm + if rerr := render_page_to(page, &bmp, resolved); rerr != .None { + // The bitmap is still the caller's -- it is background-filled and + // holds whatever was drawn before the failure. Handing it back with + // the error is what lets a caller show a blank page instead of + // nothing. + return bmp, rerr + } + return bmp, .None +} + +// render_page_to rasterises a page into a bitmap the caller already has. +// +// It paints the background and produces exactly the picture `render_page` +// would, so the two can be checked against each other; the only difference is +// who allocated. Any component count is accepted (1 grey, 2 grey+alpha, 3 RGB, +// 4 RGBA) and the bitmap is left with straight alpha. +// +// `page_size(page, scale)` is the size `render_page` allocates for +// `scale_matrix(scale, scale)`, so a caller that wants the whole page allocates +// that and gets the same picture. +// +// The bitmap is NOT resized: what falls outside it is clipped. That is what +// makes tiled rendering work -- give each tile the transform that selects it, +// +// opts.ctm = matrix_mul(scale_matrix(4, 4), translate_matrix(-tx, -ty)) +// +// and each call fills one tile. +render_page_to :: proc(page: ^Page, bmp: ^Bitmap, opts := Render_Options{}) -> Error { + if page == nil || page.doc == nil || bmp == nil { + return .Invalid_Argument + } + if bmp.width <= 0 || bmp.height <= 0 || len(bmp.pixels) == 0 || bmp.n < 1 || bmp.n > 4 { + return .Invalid_Argument + } + ctm := page_ctm_or_identity(opts.ctm) + if !matrix_is_finite(ctm) { + return .Invalid_Argument + } + + page_fill_background(bmp, page_background(opts)) + + scratch: Page_Scratch + if serr := page_scratch_begin(&scratch); serr != .None { + // The background is painted, so the bitmap is a usable blank page; it + // must still leave here with straight alpha like every other path. + bitmap_unpremultiply(bmp) + return serr + } + defer page_scratch_end(&scratch) + + rd, derr := raster_device_make(bmp, Raster_Clip{0, 0, bmp.width, bmp.height}, + context.allocator, page.doc) + if derr != .None || rd == nil { + // The background is painted and the bitmap is valid; only the drawing + // is missing. + bitmap_unpremultiply(bmp) + return derr == .None ? .Out_Of_Memory : derr + } + defer raster_device_destroy(rd) + + err := page_run(page, raster_device_as_device(rd), ctm, &scratch, !opts.no_annotations, false) + + // Everything below the Device line works in premultiplied alpha; a caller + // uploading this to a texture wants it straight (bitmap.odin says so). + bitmap_unpremultiply(bmp) + return err +} + +// page_background resolves the options' background to an RGBA colour. +@(private) +page_background :: proc(opts: Render_Options) -> [4]u8 { + bg := opts.background + if bg == ([4]u8{0, 0, 0, 0}) { + // Unset, which is what a zeroed Render_Options carries. An opaque page + // defaults to white; an alpha page defaults to nothing at all. + return opts.alpha ? [4]u8{0, 0, 0, 0} : [4]u8{255, 255, 255, 255} + } + if !opts.alpha { + bg[3] = 255 + } + return bg +} + +// page_fill_background paints `rgba` and leaves the bitmap PREMULTIPLIED, +// which is the state every bitmap below the Device line is in. +// +// The colour goes through `blend_source_for` and `blend_quantize` rather than +// being reduced here, so the background of a greyscale target is the same grey +// a content stream painting that colour into the same bitmap would produce. +// Two answers to "how bright is this colour" is exactly the kind of seam this +// library has had to fix once already. +@(private) +page_fill_background :: proc(bmp: ^Bitmap, rgba: [4]u8) { + a := f64(rgba[3]) / 255 + src := blend_source_for(bmp.n, [3]f64{ + f64(rgba[0]) / 255, f64(rgba[1]) / 255, f64(rgba[2]) / 255, + }) + c0 := blend_quantize(src[0] * a) + c1 := blend_quantize(src[1] * a) + c2 := blend_quantize(src[2] * a) + + fill: [4]u8 + switch bmp.n { + case 1: fill = {c0, 0, 0, 0} + case 2: fill = {c0, rgba[3], 0, 0} + case 3: fill = {c0, c1, c2, 0} + case: fill = {c0, c1, c2, rgba[3]} + } + bitmap_fill(bmp, fill) + bmp.premultiplied = true +} + +// page_ctm_or_identity reads a caller's transform. The zero value of a Matrix is not a +// transform at all -- it collapses the page to a point -- so it is read as +// MATRIX_IDENTITY, which makes a zeroed Render_Options mean "no extra +// transform" rather than "draw nothing". It is named for this file rather +// than for Matrix because it is a convention of this API, not of the geometry. +@(private) +page_ctm_or_identity :: proc "contextless" (m: Matrix) -> Matrix { + zero: Matrix + return m == zero ? MATRIX_IDENTITY : m +} + +// --------------------------------------------------------------------------- +// Text extraction +// --------------------------------------------------------------------------- + +// extract_text returns the page's text, reconstructed in reading order. +// +// It is the same interpreter run `render_page` performs, into a Text_Device +// instead of a rasteriser, which is why the text a page extracts to always +// agrees with the text it draws: invisible OCR text under a scan comes out, +// text drawn as paths does not, and a glyph's position is the position it +// would have been painted at. Spans are joined with nothing, lines with a +// newline and blocks with a blank line, and there is no trailing newline. +// +// Annotation appearance streams are included: the value a form field shows is +// text on the page as far as a reader is concerned. Optional content that the +// catalog's default configuration hides is included too -- hidden text is +// still text. +// +// THE STRING BELONGS TO THE CALLER: it is allocated with `allocator` and is +// released with `delete(s, allocator)`. +extract_text :: proc(page: ^Page, allocator := context.allocator) -> (string, Error) { + if page == nil || page.doc == nil { + return "", .Invalid_Argument + } + + td, terr := text_device_make(context.allocator) + if terr != .None || td == nil { + return "", terr == .None ? .Out_Of_Memory : terr + } + defer text_device_destroy(td) + + scratch: Page_Scratch + if serr := page_scratch_begin(&scratch); serr != .None { + return "", serr + } + defer page_scratch_end(&scratch) + + // The base matrix is the same one render_page uses, so the device sees + // glyphs where a reader would see them: x to the right, y down the page. + // Reading order falls out of that and must not be re-derived here. + err := page_run(page, text_device_as_device(td), MATRIX_IDENTITY, &scratch, true, true) + + return text_device_result(td, allocator), err +} + +// --------------------------------------------------------------------------- +// Display lists +// --------------------------------------------------------------------------- + +// page_display_list records the page's drawing calls instead of performing +// them, for a caller with a backend of their own: replaying the list into a +// device produces what running the page into that device directly would. +// +// `ctm` is applied on top of the page flip and /Rotate, exactly as everywhere +// else here, so a list recorded with MATRIX_IDENTITY is in the same device +// space `render_page` rasterises into and `display_list_bounds` can be +// compared with `page_size`. Annotation appearances are recorded too, the way +// `render_page` draws them: the list is the whole page, not just its content +// stream. +// +// THE LIST BELONGS TO THE CALLER. Everything in it, and the Display_List +// itself, comes from `allocator`: +// +// dl, _ := pdf.page_display_list(&page, pdf.MATRIX_IDENTITY) +// defer { pdf.display_list_destroy(dl); free(dl) } +// +// The Fonts, Images, Shadings and Colorspaces it points at belong to the +// Document and stay valid for as long as it is open. +page_display_list :: proc(page: ^Page, ctm: Matrix, allocator := context.allocator) -> (^Display_List, Error) { + if page == nil || page.doc == nil { + return nil, .Invalid_Argument + } + m := page_ctm_or_identity(ctm) + if !matrix_is_finite(m) { + return nil, .Invalid_Argument + } + + ld, lerr := list_device_make(allocator) + if lerr != .None || ld == nil { + return nil, lerr == .None ? .Out_Of_Memory : lerr + } + + dl, aerr := new(Display_List, allocator) + if aerr != nil || dl == nil { + list_device_destroy(ld) + return nil, .Out_Of_Memory + } + + scratch: Page_Scratch + if serr := page_scratch_begin(&scratch); serr != .None { + free(dl, allocator) + list_device_destroy(ld) + return nil, serr + } + defer page_scratch_end(&scratch) + + err := page_run(page, list_device_as_device(ld), m, &scratch, true, false) + + // The list is moved out of the device, which is then torn down: the + // recorded commands own their copies and outlive the recorder. Zeroing the + // device's copy is what stops `list_device_destroy` freeing them. + dl^ = ld.list + ld.list = {} + list_device_destroy(ld) + + return dl, err +} diff --git a/page_test.odin b/page_test.odin new file mode 100644 index 0000000..f4908d4 --- /dev/null +++ b/page_test.odin @@ -0,0 +1,1696 @@ +package pdf + +import "core:fmt" +import "core:strings" +import "core:testing" + +// Tests for the public rendering API. +// +// This is the first thing in the library that produces a picture, so it is +// tested as pictures: a small PDF is built with the `pb_*` builders, rendered, +// and the PIXELS are asserted -- where the ink is, what colour it is, and that +// everywhere else is untouched. A geometry bug that a matrix comparison would +// let through (an off-by-one flip, a rotation the wrong way round, a crop box +// applied twice) moves the ink to a corner these tests name, and they fail. +// +// `pt_ink` is why that is short to write: it reports the bounding box of every +// pixel that differs from the background, so "the square is at x 10..60, y +// 140..190 and nowhere else" is one assertion rather than forty thousand. + +// --------------------------------------------------------------------------- +// Building a page +// --------------------------------------------------------------------------- + +// pt_build assembles a one-page document: catalog, page tree, page, and a +// content stream. `page_extra` is spliced into the page dictionary, and +// `extra` supplies the bodies of objects 5, 6, ... for tests that need a font, +// a form or an annotation. +// Pt_Obj is one of the extra objects a test needs. A stream object is spelled +// out rather than written by hand so that its /Length is always the length the +// payload actually has -- a test that renders nothing because it miscounted a +// dictionary is a test that proves nothing. +@(private = "file") +Pt_Obj :: struct { + body: string, + payload: string, + stream: bool, +} + +@(private = "file") +pt_plain :: proc(body: string) -> Pt_Obj { + return Pt_Obj{body = body} +} + +@(private = "file") +pt_stream :: proc(dict_head, payload: string) -> Pt_Obj { + return Pt_Obj{body = dict_head, payload = payload, stream = true} +} + +@(private = "file") +pt_build :: proc(page_extra: string, content: string, extra: []Pt_Obj = nil, + catalog_extra := "") -> Pdf_Builder { + pb := pb_make() + catalog := strings.concatenate({"<< /Type /Catalog /Pages 2 0 R ", catalog_extra, " >>"}) + defer delete(catalog) + pb_obj(&pb, 1, catalog) + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + + body := strings.concatenate({"<< /Type /Page /Parent 2 0 R /Contents 4 0 R ", page_extra, " >>"}) + defer delete(body) + pb_obj(&pb, 3, body) + pb_stream_obj(&pb, 4, "", transmute([]byte)content) + + for o, i in extra { + if o.stream { + pb_stream_obj(&pb, u32(5 + i), o.body, transmute([]byte)o.payload) + } else { + pb_obj(&pb, u32(5 + i), o.body) + } + } + + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 9 /Root 1 0 R >>", at) + return pb +} + +// pt_open opens what pt_build made, walking the page tree for real. +@(private = "file") +pt_open :: proc(t: ^testing.T, pb: ^Pdf_Builder, loc := #caller_location) -> ^Document { + doc, err := open_bytes(pb.buf[:], Open_Options{}) + testing.expect_value(t, err, Error.None, loc = loc) + return doc +} + +// pt_page builds, opens and fetches page 0 in one step. The builder is +// destroyed by the caller through `pt_done`, because the Document borrows its +// bytes. +@(private = "file") +Pt_Fixture :: struct { + pb: Pdf_Builder, + doc: ^Document, + page: Page, +} + +@(private = "file") +pt_fixture :: proc(t: ^testing.T, page_extra: string, content: string, + extra: []Pt_Obj = nil, catalog_extra := "", + loc := #caller_location) -> (f: Pt_Fixture, ok: bool) { + f.pb = pt_build(page_extra, content, extra, catalog_extra) + f.doc = pt_open(t, &f.pb, loc) + if f.doc == nil { + pb_destroy(&f.pb) + return f, false + } + if !testing.expectf(t, page_count(f.doc) == 1, "wanted 1 page, got %d", page_count(f.doc), loc = loc) { + close(f.doc) + pb_destroy(&f.pb) + return f, false + } + err: Error + f.page, err = page_get(f.doc, 0) + testing.expect_value(t, err, Error.None, loc = loc) + return f, true +} + +@(private = "file") +pt_done :: proc(f: ^Pt_Fixture) { + close(f.doc) + pb_destroy(&f.pb) +} + +// --------------------------------------------------------------------------- +// Looking at pixels +// --------------------------------------------------------------------------- + +@(private = "file") +pt_px :: proc(bmp: ^Bitmap, x, y: int) -> [4]u8 { + off, ok := bitmap_pixel_offset(bmp, x, y) + if !ok { + return {} + } + out: [4]u8 + for c in 0 ..< bmp.n { + out[c] = bmp.pixels[off + c] + } + return out +} + +@(private = "file") +pt_expect_px :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, what: string, + loc := #caller_location) { + got := pt_px(bmp, x, y) + testing.expectf(t, got == want, "%s: pixel (%d,%d) is %v, wanted %v", what, x, y, got, want, + loc = loc) +} + +// Pt_Box is a half-open pixel rectangle: x0..x1-1 inclusive. +@(private = "file") +Pt_Box :: struct { + x0, y0, x1, y1: int, +} + +// pt_ink is the bounding box of every pixel that is not `bg`. An empty result +// is {0,0,0,0}, which no real box can be. +@(private = "file") +pt_ink :: proc(bmp: ^Bitmap, bg: [4]u8) -> (box: Pt_Box, count: int) { + first := true + for y in 0 ..< bmp.height { + for x in 0 ..< bmp.width { + if pt_px(bmp, x, y) == bg { + continue + } + count += 1 + if first { + box = Pt_Box{x, y, x + 1, y + 1} + first = false + continue + } + box.x0 = min(box.x0, x) + box.y0 = min(box.y0, y) + box.x1 = max(box.x1, x + 1) + box.y1 = max(box.y1, y + 1) + } + } + return +} + +@(private = "file") +PT_WHITE :: [4]u8{255, 255, 255, 255} +@(private = "file") +PT_BLUE :: [4]u8{0, 0, 255, 255} +@(private = "file") +PT_RED :: [4]u8{255, 0, 0, 255} + +@(private = "file") +pt_expect_ink :: proc(t: ^testing.T, bmp: ^Bitmap, want: Pt_Box, color: [4]u8, + what: string, loc := #caller_location) { + box, count := pt_ink(bmp, PT_WHITE) + testing.expectf(t, box == want, "%s: ink box %v, wanted %v", what, box, want, loc = loc) + // Every pixel inside the box must be the colour, and the count must be the + // box's area -- which is what rules out a box that is right only because + // a stray pixel in the far corner stretched it. + area := (want.x1 - want.x0) * (want.y1 - want.y0) + testing.expectf(t, count == area, "%s: %d inked pixels, wanted %d", what, count, area, loc = loc) + if area > 0 { + pt_expect_px(t, bmp, want.x0, want.y0, color, what, loc) + pt_expect_px(t, bmp, want.x1 - 1, want.y1 - 1, color, what, loc) + pt_expect_px(t, bmp, (want.x0 + want.x1) / 2, (want.y0 + want.y1) / 2, color, what, loc) + } +} + +// The square every geometry test below draws: 50 x 50 user units with its +// lower-left corner at (10,10), which is the corner PDF calls the bottom-left +// and a bitmap calls the bottom-left too -- only after the flip. +@(private = "file") +PT_SQUARE :: "0 0 1 rg 10 10 50 50 re f" + +// The appearance stream the annotation tests use: a 10 x 10 form that fills +// its whole /BBox red, so wherever it is fitted the result is a solid +// rectangle whose edges are exactly the annotation's /Rect. +@(private = "file") +PT_ANNOT_FORM := Pt_Obj{ + body = "/Type /XObject /Subtype /Form /BBox [0 0 10 10]", + payload = "1 0 0 rg 0 0 10 10 re f", + stream = true, +} + +// --------------------------------------------------------------------------- +// The base matrix +// --------------------------------------------------------------------------- + +@(private = "file") +pt_expect_point :: proc(t: ^testing.T, m: Matrix, p: Point, want: Point, what: string, + loc := #caller_location) { + got := transform_point(m, p) + ok := abs(got.x - want.x) < 1e-9 && abs(got.y - want.y) < 1e-9 + testing.expectf(t, ok, "%s: %v maps to %v, wanted %v", what, p, got, want, loc = loc) +} + +@(test) +test_page_base_matrix_flip :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 100]", "") + if !ok { + return + } + defer pt_done(&f) + + m := page_base_matrix(&f.page) + // y-up, origin bottom-left -> y-down, origin top-left. + pt_expect_point(t, m, {0, 0}, {0, 100}, "user origin is the bottom-left pixel") + pt_expect_point(t, m, {0, 100}, {0, 0}, "the top-left corner is the device origin") + pt_expect_point(t, m, {200, 100}, {200, 0}, "top-right") + pt_expect_point(t, m, {200, 0}, {200, 100}, "bottom-right") + + // The caller's ctm sits on top of the flip, not instead of it. + m2 := page_base_matrix(&f.page, scale_matrix(2, 3)) + pt_expect_point(t, m2, {0, 0}, {0, 300}, "scaled bottom-left") + pt_expect_point(t, m2, {200, 100}, {400, 0}, "scaled top-right") + + // A matrix that is not a matrix must not silently blank the page. + nan := Matrix{1, 0, 0, 1, 0, 0} + nan.e = f64(0h7FF8000000000000) + m3 := page_base_matrix(&f.page, nan) + testing.expect(t, matrix_is_finite(m3), "a non-finite ctm must be dropped, not propagated") + testing.expect(t, page_base_matrix(nil) == MATRIX_IDENTITY, "no page, no transform") +} + +@(test) +test_page_base_matrix_crop_and_unit :: proc(t: ^testing.T) { + // The origin is the CROP box's corner, not the media box's. + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /CropBox [50 40 150 140]", "") + if !ok { + return + } + defer pt_done(&f) + + m := page_base_matrix(&f.page) + pt_expect_point(t, m, {50, 40}, {0, 100}, "crop bottom-left is the bottom-left pixel") + pt_expect_point(t, m, {150, 140}, {100, 0}, "crop top-right is the device origin") + + // /UserUnit scales the page: page_size honours it, so the matrix must too. + g, ok2 := pt_fixture(t, "/MediaBox [0 0 100 100] /UserUnit 3", "") + if !ok2 { + return + } + defer pt_done(&g) + mu := page_base_matrix(&g.page) + pt_expect_point(t, mu, {0, 0}, {0, 300}, "UserUnit 3 makes the page 300 units tall") + pt_expect_point(t, mu, {100, 100}, {300, 0}, "UserUnit 3 makes the page 300 units wide") +} + +@(test) +test_page_base_matrix_rotations :: proc(t: ^testing.T) { + // A 200 x 100 page: landscape becomes portrait and back. + Case :: struct { + rotate: int, + // Where the user-space corners (0,0) and (200,100) land. + ll, tr: Point, + w, h: int, + } + cases := []Case{ + {0, {0, 100}, {200, 0}, 200, 100}, + {90, {0, 0}, {100, 200}, 100, 200}, + {180, {200, 0}, {0, 100}, 200, 100}, + {270, {100, 200}, {0, 0}, 100, 200}, + } + + for c in cases { + extra := fmt.tprintf("/MediaBox [0 0 200 100] /Rotate %d", c.rotate) + f, ok := pt_fixture(t, extra, "") + if !ok { + continue + } + defer pt_done(&f) + + m := page_base_matrix(&f.page) + pt_expect_point(t, m, {0, 0}, c.ll, fmt.tprintf("rotate %d: user (0,0)", c.rotate)) + pt_expect_point(t, m, {200, 100}, c.tr, fmt.tprintf("rotate %d: user (200,100)", c.rotate)) + + // Every rotation flips handedness exactly once -- that is the y-flip + // and nothing else. A rotation built the wrong way round would show + // up here as +1. + testing.expectf(t, abs(matrix_determinant(m) + 1) < 1e-9, + "rotate %d: determinant %v, wanted -1", c.rotate, matrix_determinant(m)) + + w, h, serr := page_render_size(&f.page, MATRIX_IDENTITY) + testing.expect_value(t, serr, Error.None) + testing.expectf(t, w == c.w && h == c.h, "rotate %d: size %dx%d, wanted %dx%d", + c.rotate, w, h, c.w, c.h) + } +} + +// page_size is the documented way to ask how big a page is; the renderer must +// not have a second opinion. +@(test) +test_page_render_size_agrees_with_page_size :: proc(t: ^testing.T) { + for extra in ([]string{ + "/MediaBox [0 0 200 100]", + "/MediaBox [0 0 200 100] /Rotate 90", + "/MediaBox [0 0 200 100] /Rotate 270", + "/MediaBox [0 0 612 792] /CropBox [10 20 300 400]", + "/MediaBox [0 0 100 100] /UserUnit 2", + }) { + f, ok := pt_fixture(t, extra, "") + if !ok { + continue + } + defer pt_done(&f) + + for s in ([]f64{1, 2, 0.5, 4}) { + pw, ph := page_size(&f.page, s) + rw, rh, err := page_render_size(&f.page, scale_matrix(s, s)) + testing.expect_value(t, err, Error.None) + testing.expectf(t, rw == pw && rh == ph, + "%s at %vx: render_page %dx%d, page_size %dx%d", + extra, s, rw, rh, pw, ph) + } + } +} + +// --------------------------------------------------------------------------- +// Pixels +// --------------------------------------------------------------------------- + +@(test) +test_render_page_square :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200]", PT_SQUARE) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 200 && bmp.height == 200, "size %dx%d", bmp.width, bmp.height) + testing.expect_value(t, bmp.n, 4) + testing.expect(t, !bmp.premultiplied, "the bitmap handed back is straight alpha") + + // The square is at user (10,10)-(60,60), the BOTTOM-left of the page, so + // in the bitmap it is at the bottom: y 140..190. A missing flip would put + // it at y 10..60 and this is the assertion that catches it. + pt_expect_ink(t, &bmp, Pt_Box{10, 140, 60, 190}, PT_BLUE, "square") + pt_expect_px(t, &bmp, 0, 0, PT_WHITE, "top-left corner is background") + pt_expect_px(t, &bmp, 30, 30, PT_WHITE, "above the square is background") + pt_expect_px(t, &bmp, 199, 199, PT_WHITE, "bottom-right corner is background") +} + +@(test) +test_render_page_scaled :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200]", PT_SQUARE) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page, Render_Options{ctm = scale_matrix(2, 2)}) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 400 && bmp.height == 400, "size %dx%d", bmp.width, bmp.height) + pt_expect_ink(t, &bmp, Pt_Box{20, 280, 120, 380}, PT_BLUE, "square at 2x") +} + +@(test) +test_render_page_rotations :: proc(t: ^testing.T) { + // The same square, and the corner it must end up in for each /Rotate. + Case :: struct { + rotate: int, + want: Pt_Box, + what: string, + } + cases := []Case{ + {0, {10, 140, 60, 190}, "rotate 0: bottom-left"}, + {90, {10, 10, 60, 60}, "rotate 90: top-left"}, + {180, {140, 10, 190, 60}, "rotate 180: top-right"}, + {270, {140, 140, 190, 190}, "rotate 270: bottom-right"}, + } + for c in cases { + extra := fmt.tprintf("/MediaBox [0 0 200 200] /Rotate %d", c.rotate) + f, ok := pt_fixture(t, extra, PT_SQUARE) + if !ok { + continue + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 200 && bmp.height == 200, "%s: size %dx%d", + c.what, bmp.width, bmp.height) + pt_expect_ink(t, &bmp, c.want, PT_BLUE, c.what) + } +} + +// A /Rotate 90 page must come out landscape, not just rotated in place. +@(test) +test_render_page_rotate_changes_shape :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 100] /Rotate 90", "0 0 1 rg 0 0 20 10 re f") + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 100 && bmp.height == 200, + "a 200x100 page rotated 90 is 100x200, got %dx%d", bmp.width, bmp.height) + // User (0..20, 0..10) with dx = Y, dy = X. + pt_expect_ink(t, &bmp, Pt_Box{0, 0, 10, 20}, PT_BLUE, "rotated landscape square") +} + +@(test) +test_render_page_crop_box :: proc(t: ^testing.T) { + // The bitmap is the crop box, and user space is measured from its corner. + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /CropBox [50 50 150 150]", + "0 0 1 rg 60 60 50 50 re f") + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 100 && bmp.height == 100, "size %dx%d", bmp.width, bmp.height) + pt_expect_ink(t, &bmp, Pt_Box{10, 40, 60, 90}, PT_BLUE, "square inside an offset crop box") +} + +// Content outside the crop box is off the bitmap, which is the whole point of +// cropping: a scanner's registration marks in the margin must not appear. +@(test) +test_render_page_crop_box_clips :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /CropBox [50 50 150 150]", + "0 0 1 rg 0 0 20 20 re f") + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + testing.expectf(t, bmp.width == 100 && bmp.height == 100, + "the bitmap is the crop box, got %dx%d", bmp.width, bmp.height) + _, count := pt_ink(&bmp, PT_WHITE) + testing.expectf(t, count == 0, "%d pixels drawn outside the crop box", count) +} + +@(test) +test_render_page_empty_and_missing_content :: proc(t: ^testing.T) { + // A page whose content stream is empty. + f, ok := pt_fixture(t, "/MediaBox [0 0 50 40]", "") + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + testing.expectf(t, bmp.width == 50 && bmp.height == 40, "size %dx%d", bmp.width, bmp.height) + _, count := pt_ink(&bmp, PT_WHITE) + testing.expect_value(t, count, 0) + + // And a page with no /Contents key at all: blank, not an error. + pb := pb_make() + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 30 20] >>") + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", at) + defer pb_destroy(&pb) + + doc := pt_open(t, &pb) + if doc == nil { + return + } + defer close(doc) + page, perr := page_get(doc, 0) + testing.expect_value(t, perr, Error.None) + + bmp2, err2 := render_page(&page) + testing.expect_value(t, err2, Error.None) + defer bitmap_destroy(&bmp2) + testing.expectf(t, bmp2.width == 30 && bmp2.height == 20, "size %dx%d", bmp2.width, bmp2.height) + _, c2 := pt_ink(&bmp2, PT_WHITE) + testing.expect_value(t, c2, 0) +} + +// A page that is 90% drawable draws 90%. +@(test) +test_render_page_corrupt_content :: proc(t: ^testing.T) { + corrupt := strings.concatenate({ + PT_SQUARE, + // Operator soup: stray delimiters, an unbalanced Q, an unknown + // operator with operands, and a truncated string. + "\n]] >> 4 5 zzzz Q Q Q\n1 0 0 rg 100 100 50 (unterminated", + }) + defer delete(corrupt) + + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200]", corrupt) + if !ok { + return + } + defer pt_done(&f) + + before := len(f.doc.repairs) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + // Everything before the damage is still on the page. + pt_expect_ink(t, &bmp, Pt_Box{10, 140, 60, 190}, PT_BLUE, "square before the corruption") + testing.expect(t, len(f.doc.repairs) > before, + "a corrupt content stream must record a repair") +} + +// --------------------------------------------------------------------------- +// Options +// --------------------------------------------------------------------------- + +@(test) +test_render_page_background_and_alpha :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 40 40]", "0 0 1 rg 10 10 20 20 re f") + if !ok { + return + } + defer pt_done(&f) + + // A named opaque background. + green := [4]u8{0, 128, 0, 255} + b1, e1 := render_page(&f.page, Render_Options{background = green}) + testing.expect_value(t, e1, Error.None) + defer bitmap_destroy(&b1) + pt_expect_px(t, &b1, 0, 0, green, "background") + pt_expect_px(t, &b1, 20, 20, PT_BLUE, "square over the background") + + // alpha: the page starts transparent and stays transparent where nothing + // painted. The square is opaque and its colour survives the round trip + // through premultiplication. + b2, e2 := render_page(&f.page, Render_Options{alpha = true}) + testing.expect_value(t, e2, Error.None) + defer bitmap_destroy(&b2) + pt_expect_px(t, &b2, 0, 0, [4]u8{0, 0, 0, 0}, "transparent background") + pt_expect_px(t, &b2, 20, 20, PT_BLUE, "opaque square on a transparent page") + + // A translucent background is stored straight, not premultiplied. + half := [4]u8{200, 100, 50, 128} + b3, e3 := render_page(&f.page, Render_Options{alpha = true, background = half}) + testing.expect_value(t, e3, Error.None) + defer bitmap_destroy(&b3) + got := pt_px(&b3, 0, 0) + // Premultiply-then-unpremultiply is lossy by at most one step of 255/128. + for c in 0 ..< 3 { + testing.expectf(t, abs(int(got[c]) - int(half[c])) <= 2, + "translucent background component %d is %d, wanted about %d", + c, got[c], half[c]) + } + testing.expect_value(t, got[3], u8(128)) + + // Without `alpha`, a background with a transparent alpha is forced opaque: + // an opaque page cannot start out see-through. + b4, e4 := render_page(&f.page, Render_Options{background = [4]u8{10, 20, 30, 0}}) + testing.expect_value(t, e4, Error.None) + defer bitmap_destroy(&b4) + pt_expect_px(t, &b4, 0, 0, [4]u8{10, 20, 30, 255}, "opaque page background") +} + +@(test) +test_render_page_too_large :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 20000 20000]", "") + if !ok { + return + } + defer pt_done(&f) + + // 20000 x 20000 is 4 x 10^8 pixels, past MAX_BITMAP_PIXELS. + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.Bitmap_Too_Large) + testing.expect(t, bmp.pixels == nil, "nothing is allocated for a refused bitmap") + + // And a scale that would overflow the multiplication rather than the + // product must be refused too, not wrapped. + bmp2, err2 := render_page(&f.page, Render_Options{ctm = scale_matrix(1e30, 1e30)}) + testing.expect_value(t, err2, Error.Bitmap_Too_Large) + testing.expect(t, bmp2.pixels == nil, "") + + // A modest scale still works. + bmp3, err3 := render_page(&f.page, Render_Options{ctm = scale_matrix(0.01, 0.01)}) + testing.expect_value(t, err3, Error.None) + defer bitmap_destroy(&bmp3) + testing.expectf(t, bmp3.width == 200 && bmp3.height == 200, "size %dx%d", bmp3.width, bmp3.height) +} + +@(test) +test_render_page_arguments :: proc(t: ^testing.T) { + _, err := render_page(nil) + testing.expect_value(t, err, Error.Invalid_Argument) + + f, ok := pt_fixture(t, "/MediaBox [0 0 20 20]", "") + if !ok { + return + } + defer pt_done(&f) + + nan := MATRIX_IDENTITY + nan.a = f64(0h7FF8000000000000) + _, e2 := render_page(&f.page, Render_Options{ctm = nan}) + testing.expect_value(t, e2, Error.Invalid_Argument) + + testing.expect_value(t, render_page_to(&f.page, nil), Error.Invalid_Argument) + empty: Bitmap + testing.expect_value(t, render_page_to(&f.page, &empty), Error.Invalid_Argument) + testing.expect_value(t, run_page(nil, nil, MATRIX_IDENTITY), Error.Invalid_Argument) + testing.expect_value(t, run_page(&f.page, nil, nan), Error.Invalid_Argument) + + _, dlerr := page_display_list(nil, MATRIX_IDENTITY) + testing.expect_value(t, dlerr, Error.Invalid_Argument) + _, terr := extract_text(nil) + testing.expect_value(t, terr, Error.Invalid_Argument) + + // A nil Device runs the whole interpreter and draws nothing, which is how + // a caller asks only for the repairs a page produces. + testing.expect_value(t, run_page(&f.page, nil, MATRIX_IDENTITY), Error.None) +} + +// --------------------------------------------------------------------------- +// render_page_to +// --------------------------------------------------------------------------- + +@(test) +test_render_page_to_matches_render_page :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 60 50]", "0 0 1 rg 10 10 20 20 re f 1 0 0 rg 35 5 10 40 re f") + if !ok { + return + } + defer pt_done(&f) + + a, ea := render_page(&f.page) + testing.expect_value(t, ea, Error.None) + defer bitmap_destroy(&a) + + b, berr := bitmap_make(a.width, a.height, 4) + testing.expect_value(t, berr, Error.None) + defer bitmap_destroy(&b) + testing.expect_value(t, render_page_to(&f.page, &b), Error.None) + + same := len(a.pixels) == len(b.pixels) + if same { + for i in 0 ..< len(a.pixels) { + if a.pixels[i] != b.pixels[i] { + same = false + testing.expectf(t, false, "byte %d differs: %d vs %d", i, a.pixels[i], b.pixels[i]) + break + } + } + } + testing.expect(t, same, "render_page_to must paint what render_page paints") +} + +// The transform is applied literally, so a translation selects a tile rather +// than being cancelled out. That is what makes render_page_to useful for +// rendering a big page in pieces. +@(test) +test_render_page_to_tile :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200]", PT_SQUARE) + if !ok { + return + } + defer pt_done(&f) + + // The square is at device (10,140)-(60,190). Ask for the 20x20 tile whose + // top-left corner is (10,140) and the whole tile must be blue. + tile, terr := bitmap_make(20, 20, 4) + testing.expect_value(t, terr, Error.None) + defer bitmap_destroy(&tile) + + opts := Render_Options{ctm = translate_matrix(-10, -140)} + testing.expect_value(t, render_page_to(&f.page, &tile, opts), Error.None) + pt_expect_ink(t, &tile, Pt_Box{0, 0, 20, 20}, PT_BLUE, "tile inside the square") + + // A tile the square does not reach is background. + tile2, terr2 := bitmap_make(20, 20, 4) + testing.expect_value(t, terr2, Error.None) + defer bitmap_destroy(&tile2) + opts2 := Render_Options{ctm = translate_matrix(-100, -10)} + testing.expect_value(t, render_page_to(&f.page, &tile2, opts2), Error.None) + _, count := pt_ink(&tile2, PT_WHITE) + testing.expect_value(t, count, 0) +} + +// render_page_to takes the bitmap it is given: grey, grey+alpha and RGB all +// work, which is what a caller with an existing surface needs. +@(test) +test_render_page_to_component_counts :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 40 40]", "0 0 0 rg 10 10 20 20 re f") + if !ok { + return + } + defer pt_done(&f) + + for n in 1 ..= 4 { + bmp, err := bitmap_make(40, 40, n) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + testing.expect_value(t, render_page_to(&f.page, &bmp), Error.None) + + corner := pt_px(&bmp, 0, 0) + inside := pt_px(&bmp, 20, 20) + testing.expectf(t, corner[0] == 255, "n=%d: white background, got %v", n, corner) + testing.expectf(t, inside[0] == 0, "n=%d: black square, got %v", n, inside) + if n == 2 || n == 4 { + testing.expectf(t, corner[n - 1] == 255, "n=%d: opaque background, got %v", n, corner) + } + testing.expect(t, !bmp.premultiplied, "the bitmap is left straight") + } +} + +// A greyscale target has to reduce a colour to one component, and so does the +// compositor. If they disagree, a page painted in the background's own colour +// shows a seam down the middle of a flat field. Half of this page is painted +// by the background and half by the content, in the same blue, and the two +// halves must be the same byte. +@(test) +test_render_page_to_grey_background_matches_content :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 20 20]", "0 0 1 rg 0 0 10 20 re f") + if !ok { + return + } + defer pt_done(&f) + + bmp, berr := bitmap_make(20, 20, 1) + testing.expect_value(t, berr, Error.None) + defer bitmap_destroy(&bmp) + + opts := Render_Options{background = [4]u8{0, 0, 255, 255}} + testing.expect_value(t, render_page_to(&f.page, &bmp, opts), Error.None) + + painted := pt_px(&bmp, 5, 10) + background := pt_px(&bmp, 15, 10) + testing.expectf(t, painted[0] == background[0], + "painted blue is grey %d, the same blue as a background is %d", + painted[0], background[0]) + // 0.11 is the blue weight the format gives for luminosity (11.3.5.3). + testing.expect_value(t, background[0], u8(28)) +} + +// --------------------------------------------------------------------------- +// Text +// --------------------------------------------------------------------------- + +@(private = "file") +PT_HELV :: "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>" + +// Object 5 is the font in every text test below, so the page dictionary that +// names it is a constant rather than something each test concatenates. +@(private = "file") +PT_TEXT_PAGE :: "/MediaBox [0 0 300 200] /Resources << /Font << /F1 5 0 R >> >>" +@(private = "file") +PT_TEXT_PAGE_SQ :: "/MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >>" + +// The round trip: draw a string with a standard-14 font, extract it back. It +// catches an enormous class of bugs at once -- the encoding, the widths, the +// text matrix, the base matrix's flip, and the device's reading order. +@(test) +test_extract_text_round_trip :: proc(t: ^testing.T) { + content := "BT /F1 24 Tf 20 100 Td (Hello World) Tj ET" + f, ok := pt_fixture(t, PT_TEXT_PAGE, + content, {pt_plain(PT_HELV)}) + if !ok { + return + } + defer pt_done(&f) + + s, err := extract_text(&f.page) + testing.expect_value(t, err, Error.None) + defer delete(s) + testing.expect_value(t, s, "Hello World") +} + +// Two lines, and the one drawn HIGHER on the page comes out first. +// +// This pins that both runs reach the device with their positions intact and in +// the right relation -- a `Td` dropped, a text matrix rebuilt per run, a page +// run that stopped at the first `ET`. It does NOT pin the y-flip: the text +// device works out the writing direction from the glyphs themselves and reads +// an upside-down page in its own order, which is exactly what it should do for +// a page with /Rotate, and which the pixel tests above are what pin instead. +@(test) +test_extract_text_reading_order :: proc(t: ^testing.T) { + content := "BT /F1 12 Tf 20 120 Td (Top) Tj 0 -15 Td (Bottom) Tj ET" + f, ok := pt_fixture(t, PT_TEXT_PAGE, + content, {pt_plain(PT_HELV)}) + if !ok { + return + } + defer pt_done(&f) + + s, err := extract_text(&f.page) + testing.expect_value(t, err, Error.None) + defer delete(s) + + top := strings.index(s, "Top") + bottom := strings.index(s, "Bottom") + testing.expectf(t, top >= 0 && bottom > top, + "wanted Top before Bottom, got %q", s) +} + +@(test) +test_extract_text_empty_page :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 100 100]", "0 0 1 rg 10 10 20 20 re f") + if !ok { + return + } + defer pt_done(&f) + + s, err := extract_text(&f.page) + testing.expect_value(t, err, Error.None) + defer delete(s) + testing.expect_value(t, s, "") +} + +// Invisible text -- render mode 3, the OCR layer under a scan -- paints +// nothing and extracts everything. Both halves are asserted, because the two +// devices run the same interpreter and a mistake here would show up as text +// that is drawn or ink that is not. +@(test) +test_extract_text_invisible :: proc(t: ^testing.T) { + content := "BT 3 Tr /F1 24 Tf 20 100 Td (Scanned) Tj ET" + f, ok := pt_fixture(t, PT_TEXT_PAGE, + content, {pt_plain(PT_HELV)}) + if !ok { + return + } + defer pt_done(&f) + + s, err := extract_text(&f.page) + testing.expect_value(t, err, Error.None) + defer delete(s) + testing.expect_value(t, s, "Scanned") + + bmp, rerr := render_page(&f.page) + testing.expect_value(t, rerr, Error.None) + defer bitmap_destroy(&bmp) + _, count := pt_ink(&bmp, PT_WHITE) + testing.expectf(t, count == 0, "invisible text painted %d pixels", count) +} + +// Visible text draws where the base matrix and the text matrix put it: on the +// baseline, above it, at the pen position -- not upside down at the top of the +// page. A Type 3 font is what makes that assertable in pixels, because its +// glyphs are content streams and the library has no outlines for the standard +// 14: this one draws its whole em square, so "where the glyph is" is a +// rectangle a test can name. +@(private = "file") +PT_T3_FONT :: "<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] " + + "/FontMatrix [0.001 0 0 0.001 0 0] /CharProcs << /sq 6 0 R >> " + + "/Encoding << /Type /Encoding /Differences [65 /sq] >> " + + "/FirstChar 65 /LastChar 65 /Widths [1000] >>" + +@(test) +test_render_page_text_lands_on_the_baseline :: proc(t: ^testing.T) { + glyph := pt_stream("", "1000 0 0 0 1000 1000 d1 0 0 1000 1000 re f") + content := "0 0 1 rg BT /F1 20 Tf 20 100 Td (A) Tj ET" + f, ok := pt_fixture(t, PT_TEXT_PAGE_SQ, content, {pt_plain(PT_T3_FONT), glyph}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + // The em square is 1000 glyph units, which /FontMatrix and a size of 20 + // make 20 device units. Its baseline is user y=100 -- device y=100 -- and + // it sits ABOVE it, at device y 80..100. Without the flip it would be at + // y 100..120, and upside down. + pt_expect_ink(t, &bmp, Pt_Box{20, 80, 40, 100}, PT_BLUE, "a Type 3 glyph on its baseline") +} + +// The same glyph, twice: the second is placed by the advance the font declares, +// which is what proves the text matrix is carried between glyphs rather than +// rebuilt from the Td. +@(test) +test_render_page_text_advances :: proc(t: ^testing.T) { + glyph := pt_stream("", "1000 0 0 0 1000 1000 d1 0 0 1000 1000 re f") + content := "0 0 1 rg BT /F1 20 Tf 20 100 Td (AA) Tj ET" + f, ok := pt_fixture(t, PT_TEXT_PAGE_SQ, content, {pt_plain(PT_T3_FONT), glyph}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + pt_expect_ink(t, &bmp, Pt_Box{20, 80, 60, 100}, PT_BLUE, "two glyphs, one advance apart") +} + +// --------------------------------------------------------------------------- +// Display lists +// --------------------------------------------------------------------------- + +// The identity that makes a recording worth having: replaying a page's list +// into the rasteriser produces the bitmap rasterising it directly produces. +@(test) +test_page_display_list_replays_to_the_same_pixels :: proc(t: ^testing.T) { + content := "0 0 1 rg 10 10 50 50 re f 1 0 0 rg 100 100 40 60 re f 0 1 0 RG 4 w 20 150 m 180 150 l S" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200]", content) + if !ok { + return + } + defer pt_done(&f) + + direct, derr := render_page(&f.page) + testing.expect_value(t, derr, Error.None) + defer bitmap_destroy(&direct) + + dl, lerr := page_display_list(&f.page, MATRIX_IDENTITY) + testing.expect_value(t, lerr, Error.None) + if dl == nil { + return + } + defer { + display_list_destroy(dl) + free(dl) + } + testing.expect(t, len(dl.cmds) > 0, "the list must record something") + testing.expect(t, dl.closed, "the page is finished, so the list is closed") + testing.expect(t, !dl.overflow, "") + + replay, rerr := bitmap_make(direct.width, direct.height, 4) + testing.expect_value(t, rerr, Error.None) + defer bitmap_destroy(&replay) + page_fill_background(&replay, PT_WHITE) + + rd, rderr := raster_device_make(&replay, Raster_Clip{0, 0, replay.width, replay.height}, + context.allocator, f.doc) + testing.expect_value(t, rderr, Error.None) + if rd == nil { + return + } + testing.expect_value(t, display_list_run(dl, raster_device_as_device(rd)), Error.None) + dev_close(raster_device_as_device(rd)) + raster_device_destroy(rd) + bitmap_unpremultiply(&replay) + + diff := 0 + for i in 0 ..< min(len(direct.pixels), len(replay.pixels)) { + if direct.pixels[i] != replay.pixels[i] { + diff += 1 + } + } + testing.expectf(t, diff == 0, "%d of %d bytes differ between a direct render and a replay", + diff, len(direct.pixels)) + + // The list is in the same device space the bitmap is, so its bounds sit + // inside the page. + b := display_list_bounds(dl) + testing.expectf(t, b.x0 >= -1 && b.y0 >= -1 && b.x1 <= 201 && b.y1 <= 201, + "list bounds %v are not in page space", b) +} + +// A soft mask over a transparency group, through the whole stack: the +// interpreter hands the OUTER mask to `begin_group` because 11.6.6 makes it +// the group's RESULT that the mask applies to, the recorder has to carry it, +// and the replay has to hand the same mask to the second device. A list that +// dropped it replayed the group solid while the direct render faded it. +@(test) +test_page_display_list_replays_a_masked_group :: proc(t: ^testing.T) { + extra := []Pt_Obj{ + pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 100 100] " + + "/Group << /S /Transparency /CS /DeviceGray >>", + "0.5 g 0 0 100 100 re f"), + pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 50 100] " + + "/Group << /S /Transparency /CS /DeviceRGB >>", + "0 0 0 rg 0 0 50 100 re f"), + } + f, ok := pt_fixture(t, + "/MediaBox [0 0 100 100] /Resources << /ExtGState << /GS << /Type /ExtGState " + + "/SMask << /S /Luminosity /G 5 0 R >> >> >> /XObject << /Gp 6 0 R >> >>", + "q /GS gs /Gp Do Q", extra) + if !ok { + return + } + defer pt_done(&f) + + direct, derr := render_page(&f.page) + testing.expect_value(t, derr, Error.None) + defer bitmap_destroy(&direct) + // Mid-grey luminosity over black: half the group's result reaches the page. + pt_expect_px(t, &direct, 25, 50, {127, 127, 127, 255}, "the group under a 50% mask") + pt_expect_px(t, &direct, 75, 50, PT_WHITE, "outside the group") + + dl, lerr := page_display_list(&f.page, MATRIX_IDENTITY) + testing.expect_value(t, lerr, Error.None) + if dl == nil { + return + } + defer { + display_list_destroy(dl) + free(dl) + } + + replay, rerr := bitmap_make(direct.width, direct.height, 4) + testing.expect_value(t, rerr, Error.None) + defer bitmap_destroy(&replay) + page_fill_background(&replay, PT_WHITE) + + rd, rderr := raster_device_make(&replay, Raster_Clip{0, 0, replay.width, replay.height}, + context.allocator, f.doc) + testing.expect_value(t, rderr, Error.None) + if rd == nil { + return + } + testing.expect_value(t, display_list_run(dl, raster_device_as_device(rd)), Error.None) + dev_close(raster_device_as_device(rd)) + raster_device_destroy(rd) + bitmap_unpremultiply(&replay) + + diff := 0 + for i in 0 ..< min(len(direct.pixels), len(replay.pixels)) { + if direct.pixels[i] != replay.pixels[i] { + diff += 1 + } + } + testing.expectf(t, diff == 0, "%d of %d bytes differ between a direct render and a replay", + diff, len(direct.pixels)) +} + +// The list survives the recorder: everything it points at was copied. +@(test) +test_page_display_list_owns_its_copies :: proc(t: ^testing.T) { + f, ok := pt_fixture(t, "/MediaBox [0 0 100 100]", "0 0 1 rg 10 10 20 20 re f") + if !ok { + return + } + defer pt_done(&f) + + dl, err := page_display_list(&f.page, scale_matrix(2, 2)) + testing.expect_value(t, err, Error.None) + if dl == nil { + return + } + defer { + display_list_destroy(dl) + free(dl) + } + + found := false + for cmd in dl.cmds { + #partial switch v in cmd { + case Display_Fill_Path: + found = true + testing.expect(t, v.path != nil && len(v.path.points) > 0, "the path was copied") + // 2x on top of the flip: the square's user (10,10) is device + // (20,160) on a 200-tall bitmap. + r := path_tight_bounds(v.path, v.ctm) + testing.expectf(t, abs(r.x0 - 20) < 1e-6 && abs(r.y1 - 180) < 1e-6, + "recorded fill at %v", r) + } + } + testing.expect(t, found, "the fill must be recorded") +} + +// --------------------------------------------------------------------------- +// run_page +// --------------------------------------------------------------------------- + +// run_page runs the content and closes the device; it does not draw +// annotations, which is what separates it from render_page. +@(test) +test_run_page_into_a_device :: proc(t: ^testing.T) { + annot := "<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", PT_SQUARE, + {pt_plain(annot), PT_ANNOT_FORM}) + if !ok { + return + } + defer pt_done(&f) + + ld, lerr := list_device_make() + testing.expect_value(t, lerr, Error.None) + if ld == nil { + return + } + defer list_device_destroy(ld) + + testing.expect_value(t, run_page(&f.page, list_device_as_device(ld), MATRIX_IDENTITY), Error.None) + dl := list_device_result(ld) + testing.expect(t, dl.closed, "run_page closes the device when the page is finished") + + fills := 0 + for cmd in dl.cmds { + #partial switch _ in cmd { + case Display_Fill_Path: + fills += 1 + } + } + testing.expectf(t, fills == 1, "run_page draws the content only, got %d fills", fills) +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + + +@(test) +test_render_page_annotation_appearance :: proc(t: ^testing.T) { + // The form's 10x10 /BBox is fitted to the annotation's 50x50 /Rect, so the + // whole rectangle comes out red wherever /Rect puts it. + annot := "<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", "", + {pt_plain(annot), PT_ANNOT_FORM}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + pt_expect_ink(t, &bmp, Pt_Box{100, 50, 150, 100}, PT_RED, "annotation appearance") + + // Turned off by the option. + b2, e2 := render_page(&f.page, Render_Options{no_annotations = true}) + testing.expect_value(t, e2, Error.None) + defer bitmap_destroy(&b2) + _, count := pt_ink(&b2, PT_WHITE) + testing.expectf(t, count == 0, "no_annotations still drew %d pixels", count) +} + +@(test) +test_render_page_annotation_flags_and_kinds :: proc(t: ^testing.T) { + Case :: struct { + annot: string, + drawn: bool, + what: string, + } + cases := []Case{ + {"<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /F 2 /AP << /N 6 0 R >> >>", + false, "hidden"}, + {"<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /F 32 /AP << /N 6 0 R >> >>", + false, "noview"}, + {"<< /Type /Annot /Subtype /Popup /Rect [100 100 150 150] /AP << /N 6 0 R >> >>", + false, "popup"}, + {"<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] >>", + false, "no appearance stream"}, + {"<< /Type /Annot /Subtype /Square /Rect [100 100 100 150] /AP << /N 6 0 R >> >>", + false, "empty rectangle"}, + {"<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /F 4 /AP << /N 6 0 R >> >>", + true, "print flag set"}, + // /AP /N as a dictionary of states, selected by /AS. + {"<< /Type /Annot /Subtype /Widget /Rect [100 100 150 150] /AS /On /AP << /N << /On 6 0 R /Off 7 0 R >> >> >>", + true, "state selected by /AS"}, + // Same, with /AS naming the state that draws nothing. + {"<< /Type /Annot /Subtype /Widget /Rect [100 100 150 150] /AS /Off /AP << /N << /On 6 0 R /Off 7 0 R >> >> >>", + false, "the other state"}, + // A single-state dictionary with no /AS: every viewer draws it. + {"<< /Type /Annot /Subtype /Widget /Rect [100 100 150 150] /AP << /N << /On 6 0 R >> >> >>", + true, "single state, no /AS"}, + } + + empty_form := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 10 10]", "") + for c in cases { + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", "", + {pt_plain(c.annot), PT_ANNOT_FORM, empty_form}) + if !ok { + continue + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + _, count := pt_ink(&bmp, PT_WHITE) + if c.drawn { + testing.expectf(t, count == 2500, "%s: %d pixels drawn, wanted 2500", c.what, count) + } else { + testing.expectf(t, count == 0, "%s: %d pixels drawn, wanted none", c.what, count) + } + } +} + +// Two annotations, and each must be drawn under its own clip: the /BBox clip +// the first one pushes has to be popped before the second runs, or the second +// is clipped to the first one's rectangle and never appears. +@(test) +test_render_page_two_annotations :: proc(t: ^testing.T) { + a := "<< /Type /Annot /Subtype /Square /Rect [10 10 60 60] /AP << /N 6 0 R >> >>" + b := "<< /Type /Annot /Subtype /Square /Rect [120 120 170 170] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R 7 0 R]", "", + {pt_plain(a), PT_ANNOT_FORM, pt_plain(b)}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + _, count := pt_ink(&bmp, PT_WHITE) + testing.expectf(t, count == 5000, "%d pixels drawn, wanted two 50x50 squares", count) + pt_expect_px(t, &bmp, 35, 165, PT_RED, "the first annotation") + pt_expect_px(t, &bmp, 145, 55, PT_RED, "the second annotation") + pt_expect_px(t, &bmp, 90, 110, PT_WHITE, "between them") +} + +// A form XObject's /BBox clips its contents, and an appearance stream is a +// form. This one paints well outside its box, and only the box may show. +@(test) +test_render_page_annotation_bbox_clips :: proc(t: ^testing.T) { + form := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 10 10]", + "1 0 0 rg -5 -5 20 20 re f") + annot := "<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", "", {pt_plain(annot), form}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + // Unclipped this would be user 75..175, which is device x 75..175, + // y 25..125 -- four times the area. + pt_expect_ink(t, &bmp, Pt_Box{100, 50, 150, 100}, PT_RED, "appearance clipped to its /BBox") +} + +// /Matrix rotates the appearance inside its box, and the fitted result still +// fills /Rect exactly (ISO 32000-2, 12.5.5). +@(test) +test_render_page_annotation_matrix :: proc(t: ^testing.T) { + form := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 20 10] /Matrix [0 1 -1 0 0 0]", + "1 0 0 rg 0 0 20 10 re f") + annot := "<< /Type /Annot /Subtype /Square /Rect [50 50 90 90] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", "", {pt_plain(annot), form}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + + // The rotated 20x10 box is 10x20, stretched to the 40x40 /Rect: still the + // whole rectangle, at device y 200-90..200-50. + pt_expect_ink(t, &bmp, Pt_Box{50, 110, 90, 150}, PT_RED, "rotated appearance fitted to /Rect") +} + +// Text inside an annotation's appearance stream is text on the page: a form +// field's value is what a reader sees, whatever the content stream says. +// +// The two appearances also pin where an appearance's resources come from. The +// first names /FA, which only the appearance's OWN /Resources defines, and it +// draws. The second names /F1, which only the PAGE defines, and it draws +// nothing: an annotation is not page content and inherits no resources from it +// (ISO 32000-2, 12.5.5). That is the one place an appearance stream is not +// treated exactly like a form XObject reached by `Do`, and it is deliberate -- +// see the note in annot.odin. +@(test) +test_extract_text_includes_annotations :: proc(t: ^testing.T) { + own := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 100 20] /Resources << /Font << /FA 5 0 R >> >>", + "BT /FA 12 Tf 2 4 Td (Ownfont) Tj ET") + inherited := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 100 20]", + "BT /F1 12 Tf 2 4 Td (Pagefont) Tj ET") + annot_a := "<< /Type /Annot /Subtype /Widget /Rect [20 100 120 120] /AP << /N 6 0 R >> >>" + annot_b := "<< /Type /Annot /Subtype /Widget /Rect [20 40 120 60] /AP << /N 8 0 R >> >>" + + f, ok := pt_fixture(t, + "/MediaBox [0 0 200 200] /Annots [7 0 R 9 0 R] /Resources << /Font << /F1 5 0 R >> >>", + "", {pt_plain(PT_HELV), own, pt_plain(annot_a), inherited, pt_plain(annot_b)}) + if !ok { + return + } + defer pt_done(&f) + + s, err := extract_text(&f.page) + testing.expect_value(t, err, Error.None) + defer delete(s) + testing.expectf(t, strings.contains(s, "Ownfont"), + "an appearance's own /Resources were not used: %q", s) + testing.expectf(t, !strings.contains(s, "Pagefont"), + "an appearance resolved a name out of the page's resources: %q", s) +} + +// Optional content the catalog's default configuration switches off is not +// drawn -- and IS extracted. A layer a viewer hides is still text in the file, +// and a search that could not find it would be wrong; that split is the whole +// reason extract_text and render_page do not share one set of interpreter +// options. +@(test) +test_extract_text_hidden_layer :: proc(t: ^testing.T) { + ocg := "<< /Type /OCG /Name (Watermark) >>" + content := "/OC /L1 BDC 0 0 1 rg 10 10 50 50 re f BT /F1 24 Tf 20 100 Td (Draft) Tj ET EMC" + f, ok := pt_fixture(t, + "/MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> /Properties << /L1 6 0 R >> >>", + content, {pt_plain(PT_HELV), pt_plain(ocg)}, + "/OCProperties << /OCGs [6 0 R] /D << /OFF [6 0 R] >> >>") + if !ok { + return + } + defer pt_done(&f) + + bmp, rerr := render_page(&f.page) + testing.expect_value(t, rerr, Error.None) + defer bitmap_destroy(&bmp) + _, count := pt_ink(&bmp, PT_WHITE) + testing.expectf(t, count == 0, "a switched-off layer painted %d pixels", count) + + s, terr := extract_text(&f.page) + testing.expect_value(t, terr, Error.None) + defer delete(s) + testing.expect_value(t, s, "Draft") +} + +// --------------------------------------------------------------------------- +// Scratch memory +// --------------------------------------------------------------------------- + +// The Document's arena must not grow with every render: per-page scratch lives +// in an arena that dies with the call. Rendering the same page repeatedly is +// the shape of a 900-page render, and the check has teeth because the content +// stream allocates thousands of times through the interpreter. +@(test) +test_render_page_does_not_grow_the_document :: proc(t: ^testing.T) { + content := "BT /F1 12 Tf 20 100 Td (some text to lay out) Tj ET 0 0 1 rg 10 10 50 50 re f" + f, ok := pt_fixture(t, PT_TEXT_PAGE_SQ, + content, {pt_plain(PT_HELV)}) + if !ok { + return + } + defer pt_done(&f) + + warm, e0 := render_page(&f.page) + testing.expect_value(t, e0, Error.None) + bitmap_destroy(&warm) + + // Everything the file needs -- the font, the decoded content -- is cached + // by now, so any further growth is per-render scratch that leaked into the + // Document. + before := document_memory_used(f.doc) + for _ in 0 ..< 8 { + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + bitmap_destroy(&bmp) + } + after := document_memory_used(f.doc) + testing.expectf(t, after == before, + "eight renders grew the Document arena from %d to %d bytes", before, after) +} + +// --------------------------------------------------------------------------- +// Robustness +// --------------------------------------------------------------------------- + +// An appearance stream that draws itself. The interpreter's visited set stops +// the recursion; what this pins is that entering an appearance from here goes +// through the same guard rather than around it. +@(test) +test_render_page_self_referential_appearance :: proc(t: ^testing.T) { + form := pt_stream("/Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources << /XObject << /X0 6 0 R >> >>", + "1 0 0 rg 0 0 10 10 re f /X0 Do") + annot := "<< /Type /Annot /Subtype /Square /Rect [100 100 150 150] /AP << /N 6 0 R >> >>" + f, ok := pt_fixture(t, "/MediaBox [0 0 200 200] /Annots [5 0 R]", "", {pt_plain(annot), form}) + if !ok { + return + } + defer pt_done(&f) + + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + // The outer copy is drawn; the inner one is refused. + pt_expect_ink(t, &bmp, Pt_Box{100, 50, 150, 100}, PT_RED, "a self-referential appearance") +} + +// Truncated and bit-flipped files. The property is that reaching the end of +// this test is possible at all: no crash, no hang, no unbounded allocation, +// whatever the bytes are. A page is the most attacker-controlled data in a +// PDF, and render_page is the entry point that walks all of it. +@(test) +test_render_page_fuzz :: proc(t: ^testing.T) { + pb := pt_build("/MediaBox [0 0 40 40] /Resources << /Font << /F1 5 0 R >> >>", + "0 0 1 rg 5 5 20 20 re f BT /F1 8 Tf 4 30 Td (hi) Tj ET", + {pt_plain(PT_HELV)}) + defer pb_destroy(&pb) + original := pb.buf[:] + + drive :: proc(data: []byte) { + doc, err := open_bytes(data, Open_Options{}) + if err != .None || doc == nil { + return + } + defer close(doc) + if page_count(doc) == 0 { + return + } + page, perr := page_get(doc, 0) + if perr != .None { + return + } + bmp, rerr := render_page(&page, Render_Options{ctm = scale_matrix(0.5, 0.5)}) + if rerr == .None { + bitmap_destroy(&bmp) + } + s, terr := extract_text(&page) + if terr == .None { + delete(s) + } + dl, lerr := page_display_list(&page, MATRIX_IDENTITY) + if lerr == .None && dl != nil { + display_list_destroy(dl) + free(dl) + } + } + + // Truncation, at every 16th byte. + for n := 8; n < len(original); n += 16 { + drive(original[:n]) + } + + // Bit flips, one byte at a time, over a copy. + scratch := make([]byte, len(original)) + defer delete(scratch) + for at := 0; at < len(original); at += 7 { + copy(scratch, original) + scratch[at] ~= 0xFF + drive(scratch) + } + + testing.expect(t, true, "reaching this line is the property under test") +} + +// An inline image is the one thing a content stream builds that outlives the +// run: a display list points at it, and a caller replays that list long after +// the page's scratch arena is gone. Everything the Image reads afterwards has +// to be Document memory, and its dictionary's KEYS are the part that is easy +// to miss -- the lexer slices a name that needs no unescaping straight out of +// the bytes it read, and for an inline image those bytes are the page's +// content, which is scratch. Decoding reads /Filter out of that dictionary, so +// a list replayed after the page had finished read unmapped address space and +// took the process with it. +@(test) +test_page_display_list_keeps_an_inline_image :: proc(t: ^testing.T) { + content := "q 40 0 0 40 10 10 cm BI /W 2 /H 1 /BPC 8 /CS /G /F /AHx ID 00ff> EI Q" + f, ok := pt_fixture(t, "/MediaBox [0 0 100 100]", content) + if !ok { + return + } + defer pt_done(&f) + + dl, lerr := page_display_list(&f.page, MATRIX_IDENTITY) + testing.expect_value(t, lerr, Error.None) + if dl == nil { + return + } + defer { + display_list_destroy(dl) + free(dl) + } + + img: ^Image + for cmd in dl.cmds { + if v, is_image := cmd.(Display_Fill_Image); is_image { + img = v.img + } + } + if !testing.expect(t, img != nil, "the inline image has to reach the list") { + return + } + + // The scratch arena is gone. The dictionary still has to read. + testing.expect_value(t, dict_int(f.doc, img.stream.dict, "W", 0), 2) + testing.expect_value(t, dict_int(f.doc, img.stream.dict, "H", 0), 1) + testing.expect(t, name_is(dict_get(f.doc, img.stream.dict, "F"), "AHx"), + "the filter name has to survive with it") + + // And it still decodes, which is what a replay does with it: two pixels, + // black then white, over the middle of the page. + replay, rerr := bitmap_make(100, 100, 4) + testing.expect_value(t, rerr, Error.None) + defer bitmap_destroy(&replay) + page_fill_background(&replay, PT_WHITE) + rd, rderr := raster_device_make(&replay, Raster_Clip{0, 0, 100, 100}, + context.allocator, f.doc) + testing.expect_value(t, rderr, Error.None) + if rd == nil { + return + } + testing.expect_value(t, display_list_run(dl, raster_device_as_device(rd)), Error.None) + dev_close(raster_device_as_device(rd)) + raster_device_destroy(rd) + bitmap_unpremultiply(&replay) + + // The dark half of the image, smoothed into the light one -- a 2x1 image + // stretched across 40 points is a gradient, and `rd_fill_image` says so. + box, count := pt_ink(&replay, PT_WHITE) + testing.expectf(t, count > 300, "the replayed image drew %d pixels", count) + testing.expectf(t, box.x0 == 10 && box.y0 == 50 && box.y1 == 90, + "the image landed at %v, not on the 40-point square at (10,10)", box) +} + +// --------------------------------------------------------------------------- +// Three rules that only a whole page shows +// +// Each of the three below is enforced one layer at a time by a unit test in +// `content_test.odin` or `device_raster_test.odin`. They are repeated here as +// pages because that is the form the defect took: not a wrong return value but +// a picture, and each one reached the picture through a different layer -- +// the interpreter's clip stack, its marked-content stack, and the device's +// group compositing. +// --------------------------------------------------------------------------- + +// A form's /BBox is a clip on its contents (8.10.2), so a box with no area +// draws nothing at all. Reading "empty" as "no box to clip to" inverted that: +// the form painted its whole content over the page, unclipped. +@(test) +test_render_page_zero_area_form_bbox_draws_nothing :: proc(t: ^testing.T) { + zero := Pt_Obj{ + body = "/Type /XObject /Subtype /Form /BBox [0 0 0 0]", + payload = "0 0 20 20 re f", + stream = true, + } + // The same fill under a box that HAS area, which is what says the clip is + // a real clip and not a blanket refusal to draw the form. + two := Pt_Obj{ + body = "/Type /XObject /Subtype /Form /BBox [0 0 2 2]", + payload = "0 0 20 20 re f", + stream = true, + } + page := "/MediaBox [0 0 100 100] /Resources << /XObject << /Zero 5 0 R /Two 6 0 R >> >>" + + { + f, ok := pt_fixture(t, page, "/Zero Do", {zero, two}) + if !ok { + return + } + defer pt_done(&f) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + pt_expect_px(t, &bmp, 50, 50, PT_WHITE, "the middle of the page") + pt_expect_ink(t, &bmp, Pt_Box{}, PT_WHITE, "a zero-area /BBox draws nothing") + } + { + f, ok := pt_fixture(t, page, "/Two Do", {zero, two}) + if !ok { + return + } + defer pt_done(&f) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + // Bottom-left after the page flip: PDF y 0..2 is device y 98..100. + pt_expect_ink(t, &bmp, Pt_Box{0, 98, 2, 100}, [4]u8{0, 0, 0, 255}, + "a 2x2 /BBox clips the same 20x20 fill to 2x2") + } +} + +// A BDC that arrives without its two operands still OPENS a marked-content +// section. Opening nothing left its EMC to close the ENCLOSING one, so the +// rest of a hidden optional-content section became visible. +@(test) +test_render_page_short_bdc_keeps_hidden_content_hidden :: proc(t: ^testing.T) { + ocg := pt_plain("<< /Type /OCG /Name (L1) >>") + cat := "/OCProperties << /OCGs [5 0 R] /D << /OFF [5 0 R] >> >>" + page := "/MediaBox [0 0 100 100] /Resources << /Properties << /L1 5 0 R >> >>" + + { + // Both sections are still open where the fill is, and the outer one is + // switched off. + f, ok := pt_fixture(t, page, + "/OC /L1 BDC /Foo BDC EMC 0 0 1 rg 0 0 100 100 re f EMC", {ocg}, cat) + if !ok { + return + } + defer pt_done(&f) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + pt_expect_px(t, &bmp, 50, 50, PT_WHITE, "the middle of a page whose only fill is hidden") + pt_expect_ink(t, &bmp, Pt_Box{}, PT_WHITE, "a short BDC does not end the hidden section") + } + { + // The control: two EMCs really do close both, and the same fill draws. + f, ok := pt_fixture(t, page, + "/OC /L1 BDC /Foo BDC EMC EMC 0 0 1 rg 0 0 100 100 re f", {ocg}, cat) + if !ok { + return + } + defer pt_done(&f) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + pt_expect_ink(t, &bmp, Pt_Box{0, 0, 100, 100}, PT_BLUE, + "outside both sections the fill is visible again") + } +} + +// A non-isolated transparency group is seeded with a copy of its backdrop, so +// its finished bitmap already IS "contents over backdrop". Compositing that +// over the backdrop a second time blends the backdrop with itself: `C over C` +// under Normal takes alpha 0.5 to 0.75. /I defaults to false, so this is the +// path nearly every real group takes, and the backdrop is non-opaque whenever +// the group is nested inside another one -- as here, where the inner group +// draws nothing at all and still used to darken the half-red beneath it. +@(test) +test_render_page_nested_non_isolated_group_keeps_its_backdrop :: proc(t: ^testing.T) { + inner := Pt_Obj{ + body = "/Type /XObject /Subtype /Form /BBox [0 0 100 100] " + + "/Group << /S /Transparency /CS /DeviceRGB >>", + payload = "q Q", + stream = true, + } + outer :: "/Type /XObject /Subtype /Form /BBox [0 0 100 100] " + + "/Group << /S /Transparency /CS /DeviceRGB /I true >> " + + "/Resources << /ExtGState << /GA << /ca 0.5 >> >> /XObject << /In 6 0 R >> >>" + paint :: "q /GA gs 1 0 0 rg 0 0 100 100 re f Q " + page :: "/MediaBox [0 0 100 100] /Resources << /XObject << /Ou 5 0 R >> >>" + + render :: proc(t: ^testing.T, page, ou_payload: string, inner: Pt_Obj) -> (Bitmap, bool) { + f, ok := pt_fixture(t, page, "/Ou Do", + {Pt_Obj{body = outer, payload = ou_payload, stream = true}, inner}) + if !ok { + return {}, false + } + defer pt_done(&f) + bmp, err := render_page(&f.page) + testing.expect_value(t, err, Error.None) + return bmp, err == .None + } + + with_group, ok1 := render(t, page, paint + "/In Do", inner) + defer bitmap_destroy(&with_group) + without, ok2 := render(t, page, paint, inner) + defer bitmap_destroy(&without) + if !ok1 || !ok2 { + return + } + + // Half a red over white, and nothing the empty group does may change it. + pt_expect_px(t, &with_group, 50, 50, [4]u8{255, 127, 127, 255}, + "an empty non-isolated group over a half-transparent backdrop") + for y in 0 ..< with_group.height { + for x in 0 ..< with_group.width { + a := pt_px(&with_group, x, y) + b := pt_px(&without, x, y) + if a != b { + testing.expectf(t, false, + "(%d,%d): with the empty inner group %v, without it %v", x, y, a, b) + return + } + } + } +} diff --git a/pagetree.odin b/pagetree.odin new file mode 100644 index 0000000..193a4c7 --- /dev/null +++ b/pagetree.odin @@ -0,0 +1,471 @@ +package pdf + +import "core:math" + +// --------------------------------------------------------------------------- +// Page tree -- ISO 32000-2, 7.7.3 +// +// The catalog's /Pages is the root of a tree whose interior nodes (/Type +// /Pages) carry /Kids and whose leaves (/Type /Page) are the pages. Four +// attributes -- /Resources, /MediaBox, /CropBox and /Rotate -- are inherited +// from the nearest ancestor that supplies them (table 30). +// +// The tree is flattened once, at open, into `doc.pages`. Inheritance is +// resolved on the way *down*, by carrying the ancestors' values along the path +// and writing whichever ones the leaf lacks into the leaf's own dictionary. +// Doing it downwards rather than by following /Parent upwards from each page is +// what makes a wrong or cyclic /Parent chain harmless: the attributes come from +// the path actually taken to reach the page, which is the path the file's own +// structure defines. It also means every later reader of a page dictionary -- +// the content interpreter, the annotation code -- sees a complete dictionary +// and never has to know inheritance exists. +// --------------------------------------------------------------------------- + +// Page is one page of the document with its inherited attributes resolved. +// +// A Page borrows from the Document: `dict` and `resources` live in the +// Document's arena and become invalid when `close` is called. It is a value, +// cheap to copy, and holds no state of its own -- fetching the same index twice +// yields two equal Pages. +Page :: struct { + doc: ^Document, + // Position in the flattened page list, 0-based. + index: int, + // The page's object number, or {0,0} when /Kids held the page as a direct + // dictionary rather than a reference. + ref: Ref, + dict: ^Dict, + // /Resources after inheritance. nil for a page that names none anywhere in + // its ancestry, which is legal and means "no resources"; the content + // interpreter treats every name lookup in it as a miss. + resources: ^Dict, + // Always finite and non-empty: a page whose /MediaBox is missing, damaged + // or degenerate gets DEFAULT_PAGE_RECT instead. + media_box: Rect, + // /CropBox intersected with /MediaBox, falling back to /MediaBox. This is + // the rectangle a viewer displays. + crop_box: Rect, + // Clockwise, normalised to one of 0, 90, 180, 270. + rotate: int, + // /UserUnit: the size in points of one unit of page space, default 1. A + // large-format drawing uses it to stay inside the format's 14400-point + // limit, so ignoring it renders such a page at the wrong scale. + user_unit: f64, +} + +// --------------------------------------------------------------------------- +// Building the flattened list +// --------------------------------------------------------------------------- + +// Inherited carries the four inheritable attributes down the walk as +// unresolved objects. They are copied verbatim -- a Ref stays a Ref -- so that +// writing one into a leaf costs nothing and shares rather than duplicates. +@(private) +Inherited :: struct { + resources: Object, + media_box: Object, + crop_box: Object, + rotate: Object, +} + +// pagetree_build flattens the page tree into doc.pages and doc.page_refs. +// +// It returns .No_Pages only when both the tree walk and the fallback scan came +// up empty, because a document with no page objects anywhere in it is the one +// case where there is genuinely nothing to hand back. +@(private) +pagetree_build :: proc(doc: ^Document) -> Error { + if doc == nil { return .Invalid_Argument } + + clear(&doc.pages) + clear(&doc.page_refs) + + if root := dict_dict(doc, doc.catalog, "Pages"); root != nil { + // Only interior nodes go in the visited set. A leaf cannot close a + // cycle -- it has no kids to recurse through -- and files that + // reference one page object from two places are relying on it + // appearing twice, so marking leaves visited would silently drop + // pages. + // Deliberately *not* the Document arena: this is scratch for the walk, + // and the arena is never reclaimed before `close`. Document-arena + // memory is also never freed, so a set allocated there could not be + // released here without breaking that rule. + visited := make(map[u32]bool, 64, context.allocator) + defer delete(visited) + + // Fuel bounds the total node visits, not just the depth. A tree can be + // shallow, acyclic and still enormous if /Kids arrays are shared + // between parents, which is how a small file expands into a very long + // walk. + fuel := MAX_PAGES + root_ref, _ := dict_ref(doc.catalog, "Pages") + pagetree_walk(doc, root, root_ref, Inherited{}, 0, &visited, &fuel) + } + + // No catalog, an unusable /Pages, or a tree whose leaves were all + // unreadable. Objects are still objects: look for pages directly. + if len(doc.pages) == 0 { + pagetree_scan(doc) + } + + if len(doc.pages) == 0 { + return .No_Pages + } + return .None +} + +// pagetree_walk visits one node, appending any leaves beneath it. +@(private) +pagetree_walk :: proc( + doc: ^Document, + node: ^Dict, + node_ref: Ref, + inherited: Inherited, + depth: int, + visited: ^map[u32]bool, + fuel: ^int, +) { + if node == nil { return } + if depth >= MAX_DEPTH { + add_repair(doc, .Missing_Page_Node, 0, node_ref) + return + } + if fuel^ <= 0 { return } + fuel^ -= 1 + if len(doc.pages) >= MAX_PAGES { return } + + type := dict_type(doc, node) + kids := dict_array(doc, node, "Kids") + + // /Type is checked before /Kids because a leaf that also carries a stray + // /Kids -- a page edited by a tool that did not clean up after itself -- + // is a page, not an interior node. + if type != "Page" && array_len(kids) > 0 { + if node_ref.num != 0 { + if visited[node_ref.num] or_else false { + add_repair(doc, .Cyclic_Page_Tree, 0, node_ref) + return + } + visited[node_ref.num] = true + } + + down := pagetree_inherit(node, inherited) + + for i in 0 ..< array_len(kids) { + if fuel^ <= 0 { break } + raw := array_get_raw(kids, i) + kid_ref, _ := to_ref(raw) + kid, ok := to_dict(resolve(doc, raw)) + if !ok { + add_repair(doc, .Missing_Page_Node, 0, kid_ref) + continue + } + pagetree_walk(doc, kid, kid_ref, down, depth + 1, visited, fuel) + } + + // Unmarked on the way out, so that a node legitimately reachable by two + // different paths is walked for each of them. Only a node reachable + // from *itself* is a cycle. This cannot be a `defer` at the point the + // mark is set: that scope closes before the kids are walked. + if node_ref.num != 0 { + delete_key(visited, node_ref.num) + } + return + } + + // An interior node with no kids contributes nothing. Anything else is a + // leaf: /Type /Page, and also the untyped dictionaries that turn up in + // /Kids from producers that omit it, which are pages by position. + if type == "Pages" { return } + + pagetree_add_leaf(doc, node, node_ref, inherited) +} + +// pagetree_inherit returns the inherited set as seen by this node's children: +// the parent's values, overridden by whichever ones this node supplies. +@(private) +pagetree_inherit :: proc(node: ^Dict, parent: Inherited) -> (out: Inherited) { + out = parent + if dict_has(node, "Resources") { out.resources = dict_get_raw(node, "Resources") } + if dict_has(node, "MediaBox") { out.media_box = dict_get_raw(node, "MediaBox") } + if dict_has(node, "CropBox") { out.crop_box = dict_get_raw(node, "CropBox") } + if dict_has(node, "Rotate") { out.rotate = dict_get_raw(node, "Rotate") } + return +} + +// pagetree_add_leaf records one page, first filling in the inheritable keys it +// does not define for itself. +@(private) +pagetree_add_leaf :: proc(doc: ^Document, leaf: ^Dict, ref: Ref, inherited: Inherited) { + if len(doc.pages) >= MAX_PAGES { return } + + if !dict_has(leaf, "Resources") && inherited.resources != nil { + dict_set(leaf, "Resources", inherited.resources) + } + if !dict_has(leaf, "MediaBox") && inherited.media_box != nil { + dict_set(leaf, "MediaBox", inherited.media_box) + } + if !dict_has(leaf, "CropBox") && inherited.crop_box != nil { + dict_set(leaf, "CropBox", inherited.crop_box) + } + if !dict_has(leaf, "Rotate") && inherited.rotate != nil { + dict_set(leaf, "Rotate", inherited.rotate) + } + + append(&doc.pages, leaf) + append(&doc.page_refs, ref) +} + +// pagetree_scan finds pages by looking at every object in the file, for +// documents whose catalog or page tree is unusable. +// +// The order is object number order, which is not document order. It is the only +// ordering available once the tree is gone, and it is right far more often than +// it is wrong: nearly every producer writes pages in order. +@(private) +pagetree_scan :: proc(doc: ^Document) { + for num in 1 ..< len(doc.xref) { + if len(doc.pages) >= MAX_PAGES { break } + ref := Ref{u32(num), doc.xref[num].gen} + d, ok := to_dict(get_object(doc, ref)) + if !ok { continue } + // Without a tree to place them, only an explicit /Type /Page is + // trustworthy: a resource dictionary or an annotation would otherwise + // be swept up as a page. + if dict_type(doc, d) != "Page" { continue } + pagetree_inherit_upwards(doc, d) + append(&doc.pages, d) + append(&doc.page_refs, ref) + } + if len(doc.pages) > 0 { + add_repair(doc, .Missing_Page_Node, 0, {}) + } +} + +// pagetree_inherit_upwards resolves inheritance by following /Parent, for pages +// that were found by scanning and so have no known path from the root. +// +// The visited set is what makes this safe: a /Parent chain that loops back on +// itself is a real thing in damaged files, and is the whole reason the normal +// path walks downwards instead. +@(private) +pagetree_inherit_upwards :: proc(doc: ^Document, leaf: ^Dict) { + visited: map[u32]bool + defer delete(visited) + + node := leaf + for _ in 0 ..< MAX_DEPTH { + parent_ref, has_ref := dict_ref(node, "Parent") + if has_ref { + if visited[parent_ref.num] or_else false { + add_repair(doc, .Cyclic_Page_Tree, 0, parent_ref) + return + } + visited[parent_ref.num] = true + } + parent, ok := to_dict(dict_get(doc, node, "Parent")) + if !ok || parent == leaf { return } + + if !dict_has(leaf, "Resources") && dict_has(parent, "Resources") { + dict_set(leaf, "Resources", dict_get_raw(parent, "Resources")) + } + if !dict_has(leaf, "MediaBox") && dict_has(parent, "MediaBox") { + dict_set(leaf, "MediaBox", dict_get_raw(parent, "MediaBox")) + } + if !dict_has(leaf, "CropBox") && dict_has(parent, "CropBox") { + dict_set(leaf, "CropBox", dict_get_raw(parent, "CropBox")) + } + if !dict_has(leaf, "Rotate") && dict_has(parent, "Rotate") { + dict_set(leaf, "Rotate", dict_get_raw(parent, "Rotate")) + } + node = parent + } +} + +// --------------------------------------------------------------------------- +// Public page access +// --------------------------------------------------------------------------- + +// page_count is the number of pages the document has. +// +// This is the flattened count, not the catalog's /Count, which is a hint that +// damaged files get wrong and which this library never trusts. +page_count :: proc(doc: ^Document) -> int { + if doc == nil { return 0 } + return len(doc.pages) +} + +// page_get returns page `index`, 0-based, with inherited attributes resolved +// and every geometric value guaranteed finite. +page_get :: proc(doc: ^Document, index: int) -> (page: Page, err: Error) { + if doc == nil { return {}, .Invalid_Argument } + if index < 0 || index >= len(doc.pages) { return {}, .Page_Out_Of_Range } + + d := doc.pages[index] + page.doc = doc + page.index = index + page.dict = d + if index < len(doc.page_refs) { + page.ref = doc.page_refs[index] + } + page.resources = dict_dict(doc, d, "Resources") + + // A missing, unparsable or zero-area /MediaBox is common enough -- and + // fatal enough to a renderer that divides by the page height -- that a + // substitute is the only sane answer. Viewers all pick one; this one picks + // A4, which is right more often than Letter outside North America and + // wrong by a survivable margin either way. + mb := dict_rect(doc, d, "MediaBox", RECT_EMPTY) + if rect_is_empty(mb) || !rect_is_finite(mb) { + mb = DEFAULT_PAGE_RECT + } + page.media_box = mb + + // /CropBox is required to lie inside /MediaBox; when it does not, the + // intersection is what the format says to display (ISO 32000-2, 14.11.2). + cb := dict_rect(doc, d, "CropBox", RECT_EMPTY) + if rect_is_empty(cb) || !rect_is_finite(cb) { + cb = mb + } else { + cb = rect_intersect(cb, mb) + if rect_is_empty(cb) { + cb = mb + } + } + page.crop_box = cb + + page.rotate = page_normalize_rotate(dict_int(doc, d, "Rotate", 0)) + + // /UserUnit is inheritable in no version of the format, so it is read from + // the page alone. A non-positive or non-finite value means the producer + // wrote nonsense; 1 is the default and the only safe substitute. + page.user_unit = dict_num(doc, d, "UserUnit", 1) + if !is_finite(page.user_unit) || page.user_unit <= 0 { + page.user_unit = 1 + } + + return page, .None +} + +// page_normalize_rotate folds /Rotate into 0, 90, 180 or 270. +// +// The format requires a multiple of 90 and files write 45, 360 and -90 anyway. +// Negatives are legal and mean anticlockwise; the rest are rounded down to the +// quarter turn below them, which is what viewers do. +@(private) +page_normalize_rotate :: proc(raw: i64) -> int { + // Reduced in i64 first: /Rotate is file-controlled and can arrive as a + // value whose magnitude would overflow a narrower type. + r := raw % 360 + if r < 0 { r += 360 } + return int(r / 90 * 90) +} + +// page_index_of returns the position of a page object in the flattened list, or +// -1. Destinations and outline entries name a page by reference, and this is +// how they become a page number. +page_index_of :: proc(doc: ^Document, ref: Ref) -> int { + if doc == nil || ref.num == 0 { return -1 } + for r, i in doc.page_refs { + if r.num == ref.num { return i } + } + return -1 +} + + +// --------------------------------------------------------------------------- +// Page geometry and content +// +// These three are what a consumer of the library reaches for first: how big is +// the page, how big is the bitmap, and what does it draw. They are deliberately +// not methods on the Document -- a Page is a value, cheap to copy, and holds +// everything they need. +// --------------------------------------------------------------------------- + +// page_bounds is the rectangle a viewer displays: the crop box, already +// intersected with the media box by page_get, and guaranteed finite and +// non-empty. +page_bounds :: proc(page: ^Page) -> Rect { + if page == nil { return DEFAULT_PAGE_RECT } + return page.crop_box +} + +// page_size is the pixel size of the page at `scale`, with /Rotate applied, so +// a page rotated 90 degrees comes back landscape. /UserUnit is included: +// ignoring it renders a large-format drawing at the wrong size. +// +// The result is clamped to at least 1x1 -- a zero-sized bitmap is not something +// a caller can do anything useful with, and every path that produces one here +// comes from a damaged page. +page_size :: proc(page: ^Page, scale: f64 = 1) -> (width, height: int) { + if page == nil { return 1, 1 } + r := page_bounds(page) + unit := page.user_unit + if !is_finite(unit) || unit <= 0 { unit = 1 } + s := scale * unit + if !is_finite(s) || s <= 0 { s = 1 } + + w := rect_width(r) * s + h := rect_height(r) * s + if page.rotate == 90 || page.rotate == 270 { + w, h = h, w + } + if !is_finite(w) || !is_finite(h) { return 1, 1 } + + // Round outward, not to nearest: a page 100.2 pixels wide needs 101 + // columns, or the last fraction of content falls off the right edge. The + // epsilon keeps an exact 200.0 -- which floating-point scaling routinely + // renders as 200.00000000000003 -- from claiming a 201st column. + width = max(1, int(math.ceil(w - 1e-9))) + height = max(1, int(math.ceil(h - 1e-9))) + return +} + +// page_content concatenates the page's content streams into one buffer, +// allocated with `allocator` and owned by the caller. +// +// /Contents is one stream or an array of them, and the format explicitly allows +// a single operator -- even a single token -- to be split across the boundary +// between two of them (ISO 32000-2, 7.8.2). They are therefore joined with a +// newline between, never concatenated bare, or "1 0 0 1 50" + "50 cm" would +// lex as "501" and "cm". A stream that fails to decode contributes nothing and +// leaves the rest intact, because one broken stream should not blank the page. +page_content :: proc(page: ^Page, allocator := context.allocator) -> (data: []byte, err: Error) { + if page == nil || page.doc == nil { return nil, .Invalid_Argument } + doc := page.doc + + out := make([dynamic]byte, 0, 4096, allocator) + contents := dict_get(doc, page.dict, "Contents") + + add :: proc(doc: ^Document, out: ^[dynamic]byte, s: ^Stream) { + if s == nil { return } + bytes, derr := stream_data(doc, s) + if derr != .None || len(bytes) == 0 { return } + if len(out) > 0 { + append(out, byte('\n')) + } + append(out, ..bytes) + } + + #partial switch v in contents { + case ^Stream: + add(doc, &out, v) + case ^Array: + // Cap the number of parts: /Contents is attacker-controlled, and an + // array of a million references would otherwise be walked in full. + n := min(array_len(v), MAX_CONTENT_PARTS) + for i in 0 ..< n { + s, ok := to_stream(array_get(doc, v, i)) + if ok { + add(doc, &out, s) + } + } + } + + return out[:], .None +} + +// MAX_CONTENT_PARTS bounds the /Contents array. Real documents use one stream, +// or a handful when a producer emits one per layer; a thousand is already +// pathological. +MAX_CONTENT_PARTS :: 4096 diff --git a/pagetree_test.odin b/pagetree_test.odin new file mode 100644 index 0000000..4dcd56b --- /dev/null +++ b/pagetree_test.odin @@ -0,0 +1,868 @@ +package pdf + +import "core:testing" + +// --------------------------------------------------------------------------- +// Page tree +// +// These build a whole file rather than a Document by hand, because the page +// tree's job is to survive what real files do to it, and the interesting cases +// -- a /Kids cycle, a page reached only by scanning -- are structural. +// --------------------------------------------------------------------------- + +// pt_open opens a builder's bytes with the page tree walked, which is the +// thing under test here and so must not be skipped. +@(private = "file") +pt_open :: proc(pb: ^Pdf_Builder) -> (^Document, Error) { + return open_bytes(pb.buf[:]) +} + +// pt_finish closes off a file whose objects are already written. +@(private = "file") +pt_finish :: proc(pb: ^Pdf_Builder, root := "1 0 R") { + at := pb_classic_xref(pb) + pb_str(pb, "trailer\n<< /Size ") + pb_num(pb, len(pb.offs)) + pb_str(pb, " /Root ") + pb_str(pb, root) + pb_str(pb, " >>\n") + pb_startxref(pb, at) +} + +@(private = "file") +pt_width :: proc(p: Page) -> f64 { + return rect_width(p.media_box) +} + +// --------------------------------------------------------------------------- +// Flattening +// --------------------------------------------------------------------------- + +@(test) +test_pagetree_single_page :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + testing.expect(t, !doc.is_damaged, "a well-formed file recorded a repair") + + page, perr := page_get(doc, 0) + testing.expect(t, perr == .None, "page_get failed") + testing.expectf(t, pt_width(page) == 200, "width %v, wanted 200", pt_width(page)) + testing.expectf(t, page.ref.num == 3, "page ref %v, wanted 3", page.ref.num) + testing.expect(t, page.index == 0, "page index is not 0") + testing.expect(t, page.rotate == 0, "rotate is not 0") +} + +@(test) +test_pagetree_document_order :: proc(t: ^testing.T) { + // Object numbers deliberately run backwards against page order: a walk that + // happened to be scanning objects instead of following /Kids would pass + // every other assertion in this file but fail this one. + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [5 0 R 4 0 R 3 0 R] /Count 3 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 10] >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 10] >>") + pb_obj(&pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 3, "page_count %v, wanted 3", page_count(doc)) + for want, i in ([3]f64{100, 200, 300}) { + page, _ := page_get(doc, i) + testing.expectf(t, pt_width(page) == want, + "page %v width %v, wanted %v", i, pt_width(page), want) + } +} + +@(test) +test_pagetree_nested :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 4 >>") + pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 >>") + pb_obj(&pb, 4, "<< /Type /Pages /Parent 2 0 R /Kids [7 0 R 8 0 R] /Count 2 >>") + pb_obj(&pb, 5, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 10 10] >>") + pb_obj(&pb, 6, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 20 10] >>") + pb_obj(&pb, 7, "<< /Type /Page /Parent 4 0 R /MediaBox [0 0 30 10] >>") + pb_obj(&pb, 8, "<< /Type /Page /Parent 4 0 R /MediaBox [0 0 40 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 4, "page_count %v, wanted 4", page_count(doc)) + for want, i in ([4]f64{10, 20, 30, 40}) { + page, _ := page_get(doc, i) + testing.expectf(t, pt_width(page) == want, + "page %v width %v, wanted %v", i, pt_width(page), want) + } +} + +// A /Count that disagrees with the number of leaves is a hint, and this +// library never trusts it. Both directions are tested because both occur: a +// stale count after pages were deleted, and an inflated one after a merge. +@(test) +test_pagetree_count_is_not_trusted :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 97 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 20 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) +} + +// A kid written inline instead of as "N 0 R". Legal, and produced by at least +// one generator; the page has no object number of its own. +@(test) +test_pagetree_direct_kid :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [ << /Type /Page /MediaBox [0 0 55 10] >> ] /Count 1 >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 55, "width %v, wanted 55", pt_width(page)) + testing.expect(t, page.ref.num == 0, "a direct kid should have no object number") +} + +// A node with neither /Type nor /Kids, reached through /Kids, is a page by +// position. Omitting /Type on a page is common. +@(test) +test_pagetree_untyped_leaf :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Parent 2 0 R /MediaBox [0 0 66 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 66, "width %v, wanted 66", pt_width(page)) +} + +// A leaf that also carries /Kids, left behind by an editing tool. /Type wins: +// treating it as an interior node would lose the page. +@(test) +test_pagetree_page_with_stray_kids :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /Kids [] /MediaBox [0 0 77 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 77, "width %v, wanted 77", pt_width(page)) +} + +// The same page object under two parents. Rare, but the file means both, and a +// visited set that covered leaves would silently drop the second. +@(test) +test_pagetree_shared_leaf_appears_twice :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 3 0 R] /Count 2 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 88 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) +} + +// --------------------------------------------------------------------------- +// Inherited attributes -- ISO 32000-2, 7.7.3.4 +// --------------------------------------------------------------------------- + +@(test) +test_pagetree_inherits_from_root :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 " + + "/MediaBox [0 0 111 222] /Rotate 90 /Resources << /Foo 9 0 R >> >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 111, "width %v, wanted 111", pt_width(page)) + testing.expectf(t, rect_height(page.media_box) == 222, + "height %v, wanted 222", rect_height(page.media_box)) + testing.expectf(t, page.rotate == 90, "rotate %v, wanted 90", page.rotate) + testing.expect(t, page.resources != nil, "resources were not inherited") +} + +// The nearest ancestor supplying an attribute wins over a more distant one. +@(test) +test_pagetree_nearest_ancestor_wins :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 2 /MediaBox [0 0 10 10] /Rotate 90 >>") + pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R 5 0 R] /Count 2 " + + "/MediaBox [0 0 20 20] >>") + // 4 takes the middle node's box; 5 overrides it itself. + pb_obj(&pb, 4, "<< /Type /Page /Parent 3 0 R >>") + pb_obj(&pb, 5, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 30 30] /Rotate 180 >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + p0, _ := page_get(doc, 0) + testing.expectf(t, pt_width(p0) == 20, "page 0 width %v, wanted 20", pt_width(p0)) + testing.expectf(t, p0.rotate == 90, "page 0 rotate %v, wanted 90", p0.rotate) + + p1, _ := page_get(doc, 1) + testing.expectf(t, pt_width(p1) == 30, "page 1 width %v, wanted 30", pt_width(p1)) + testing.expectf(t, p1.rotate == 180, "page 1 rotate %v, wanted 180", p1.rotate) +} + +// Inheritance must follow the path actually walked, not /Parent. Here /Parent +// points at a node that is not the real parent -- the sort of thing an +// incremental update leaves behind -- and the walked path is authoritative. +@(test) +test_pagetree_wrong_parent_does_not_mislead :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 1 /MediaBox [0 0 41 41] >>") + // /Parent lies: it names 9, a node with a different box, which is not on + // the path from the root. + pb_obj(&pb, 4, "<< /Type /Page /Parent 9 0 R >>") + pb_obj(&pb, 9, "<< /Type /Pages /Kids [] /MediaBox [0 0 99 99] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 41, + "width %v, wanted 41 (inheritance followed /Parent instead of the walk)", pt_width(page)) +} + +// --------------------------------------------------------------------------- +// Geometry +// --------------------------------------------------------------------------- + +@(test) +test_pagetree_missing_mediabox_falls_back :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + page, _ := page_get(doc, 0) + testing.expect(t, page.media_box == DEFAULT_PAGE_RECT, "no /MediaBox anywhere: wanted the default") + testing.expect(t, page.crop_box == DEFAULT_PAGE_RECT, "crop box should follow the media box") +} + +// A degenerate box has to be replaced, not passed on: a renderer that divides +// by the page height would produce an infinity from it. +@(test) +test_pagetree_degenerate_mediabox_falls_back :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 0 0] >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [10 10 10 900] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + for i in 0 ..< 2 { + page, _ := page_get(doc, i) + testing.expectf(t, page.media_box == DEFAULT_PAGE_RECT, + "page %v kept a degenerate media box %v", i, page.media_box) + } +} + +// A /MediaBox written with its corners the wrong way round is legal and must +// come back normalised. +@(test) +test_pagetree_reversed_mediabox :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [612 792 0 0] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + page, _ := page_get(doc, 0) + testing.expect(t, page.media_box == Rect{0, 0, 612, 792}, "reversed media box was not normalised") +} + +@(test) +test_pagetree_cropbox :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>") + // Inside the media box: kept as written. + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [10 10 90 90] >>") + // Overhanging: clipped to the media box. + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [50 50 500 500] >>") + // Disjoint: unusable, so the media box stands in. + pb_obj(&pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [200 200 300 300] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + p0, _ := page_get(doc, 0) + testing.expect(t, p0.crop_box == Rect{10, 10, 90, 90}, "an in-bounds crop box was altered") + + p1, _ := page_get(doc, 1) + testing.expect(t, p1.crop_box == Rect{50, 50, 100, 100}, "an overhanging crop box was not clipped") + + p2, _ := page_get(doc, 2) + testing.expect(t, p2.crop_box == p2.media_box, "a disjoint crop box was not discarded") +} + +@(test) +test_pagetree_rotate_normalisation :: proc(t: ^testing.T) { + Rotate_Case :: struct { written: string, want: int } + cases := [?]Rotate_Case{ + {"0", 0}, + {"90", 90}, + {"180", 180}, + {"270", 270}, + {"360", 0}, + {"450", 90}, + // Negative angles are legal and mean anticlockwise. + {"-90", 270}, + {"-360", 0}, + {"-450", 270}, + // Not a multiple of 90. The format forbids it and files write it + // anyway; rounding down to the quarter turn below is what viewers do. + {"45", 0}, + {"200", 180}, + // An absurd value has to reduce, not overflow: 2^63-1 is 7 mod 360. + {"9223372036854775807", 0}, + } + + for c in cases { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_mark(&pb, 3) + pb_str(&pb, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Rotate ") + pb_str(&pb, c.written) + pb_str(&pb, " >>\nendobj\n") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + if !testing.expectf(t, err == .None, "open failed for /Rotate %v", c.written) { continue } + if doc == nil { continue } + + page, _ := page_get(doc, 0) + testing.expectf(t, page.rotate == c.want, + "/Rotate %v gave %v, wanted %v", c.written, page.rotate, c.want) + } +} + +// --------------------------------------------------------------------------- +// Damage +// --------------------------------------------------------------------------- + +// A /Kids array that points back at an ancestor. Without a visited set this +// recurses until the stack ends. +@(test) +test_pagetree_cycle_is_broken :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [2 0 R 4 0 R] /Count 1 >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 123 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc_has_repair(doc, .Cyclic_Page_Tree), "no cyclic-page-tree repair recorded") + // The page below the cycle is still reachable and must still be listed. + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 123, "width %v, wanted 123", pt_width(page)) +} + +// A node that is its own kid. +@(test) +test_pagetree_self_referential_node :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [2 0 R 3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 21 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) +} + +// Deeper than MAX_DEPTH. The walk stops; it does not overflow the stack. +@(test) +test_pagetree_depth_limit :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + + DEPTH :: MAX_DEPTH + 20 + for i in 0 ..< DEPTH { + num := u32(2 + i) + pb_mark(&pb, num) + pb_num(&pb, int(num)) + pb_str(&pb, " 0 obj\n<< /Type /Pages /Count 1 /Kids [") + pb_num(&pb, int(num) + 1) + pb_str(&pb, " 0 R] >>\nendobj\n") + } + pb_obj(&pb, u32(2 + DEPTH), "<< /Type /Page /MediaBox [0 0 10 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + // The one page in this file sits below the depth limit, so it is not + // reachable by the walk. It is still found by the scan fallback, which is + // exactly the degradation that is wanted: no crash, no hang, and whatever + // pages can still be identified. + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, doc.is_damaged, "a file this deep should have recorded a repair") +} + +// A catalog whose /Pages is missing entirely. The pages are still objects in +// the file and scanning finds them. +@(test) +test_pagetree_scan_fallback :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog >>") + pb_obj(&pb, 2, "<< /Type /Page /MediaBox [0 0 31 10] >>") + pb_obj(&pb, 3, "<< /Type /Page /MediaBox [0 0 32 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) + testing.expect(t, doc_has_repair(doc, .Missing_Page_Node), "no missing-page-node repair recorded") +} + +// Scanned pages have no walked path, so inheritance has to come from /Parent. +@(test) +test_pagetree_scan_inherits_via_parent :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /MediaBox [0 0 44 44] /Rotate 270 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 44, "width %v, wanted 44", pt_width(page)) + testing.expectf(t, page.rotate == 270, "rotate %v, wanted 270", page.rotate) +} + +// A /Parent chain that loops. The upward walk must stop rather than spin. +@(test) +test_pagetree_cyclic_parent_chain :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Parent 3 0 R >>") + pb_obj(&pb, 3, "<< /Type /Pages /Kids [] /Parent 2 0 R >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 51 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) +} + +// A page whose /Parent is itself. +@(test) +test_pagetree_self_parent :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog >>") + pb_obj(&pb, 2, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 52 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) +} + +// /Kids entries that resolve to nothing, or to the wrong kind of object. +@(test) +test_pagetree_junk_kids :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [99 0 R (a string) 42 null 3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 61 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) + page, _ := page_get(doc, 0) + testing.expectf(t, pt_width(page) == 61, "width %v, wanted 61", pt_width(page)) +} + +// A file with no page objects at all. This is the one case where the page tree +// genuinely has nothing to hand back. +@(test) +test_pagetree_no_pages_is_an_error :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expectf(t, err == .No_Pages, "open gave %v, wanted .No_Pages", err) + testing.expect(t, doc == nil, "a failed open must not hand back a document") +} + +// ...unless the caller asked not to walk it, which is what skip_page_tree is +// for: inspecting the objects of a document whose page tree is unusable. +@(test) +test_pagetree_skip_option :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") + pt_finish(&pb) + + doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) + defer close(doc) + testing.expect(t, err == .None, "skip_page_tree should open a document with no pages") + if doc == nil { return } + testing.expect(t, page_count(doc) == 0, "skip_page_tree should leave the page list empty") +} + +// --------------------------------------------------------------------------- +// Public accessors +// --------------------------------------------------------------------------- + +@(test) +test_pagetree_page_get_bounds :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + _, e1 := page_get(doc, -1) + testing.expectf(t, e1 == .Page_Out_Of_Range, "index -1 gave %v", e1) + _, e2 := page_get(doc, 1) + testing.expectf(t, e2 == .Page_Out_Of_Range, "index 1 gave %v", e2) + _, e3 := page_get(doc, 1 << 40) + testing.expectf(t, e3 == .Page_Out_Of_Range, "a huge index gave %v", e3) + + _, e4 := page_get(nil, 0) + testing.expectf(t, e4 == .Invalid_Argument, "a nil document gave %v", e4) + testing.expect(t, page_count(nil) == 0, "page_count(nil) is not 0") +} + +@(test) +test_pagetree_index_of :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") + pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 20 10] >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, page_index_of(doc, Ref{3, 0}) == 0, "object 3 is not page 0") + testing.expect(t, page_index_of(doc, Ref{4, 0}) == 1, "object 4 is not page 1") + testing.expect(t, page_index_of(doc, Ref{9, 0}) == -1, "an unknown ref should be -1") + testing.expect(t, page_index_of(doc, Ref{0, 0}) == -1, "the null ref should be -1") + testing.expect(t, page_index_of(nil, Ref{3, 0}) == -1, "a nil document should be -1") +} + +// Fetching the same page twice must give equal values and must not accumulate +// repairs, which it would if the fallbacks recorded damage on every call. +@(test) +test_pagetree_page_get_is_repeatable :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") + pt_finish(&pb) + + doc, err := pt_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + before := len(doc.repairs) + a, _ := page_get(doc, 0) + b, _ := page_get(doc, 0) + testing.expect(t, a == b, "two fetches of the same page differ") + testing.expectf(t, len(doc.repairs) == before, + "page_get recorded %v repairs", len(doc.repairs) - before) +} + +// --------------------------------------------------------------------------- +// Page geometry and content accessors +// --------------------------------------------------------------------------- + +@(private = "file") +build_page_doc :: proc(page_body: string, contents: string = "") -> (^Document, Pdf_Builder) { + pb := pb_make() + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, page_body) + if contents != "" { + pb_stream_obj(&pb, 4, "", transmute([]byte)contents) + } + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 5 /Root 1 0 R >>", at) + doc, _ := open_bytes(pb.buf[:]) + return doc, pb +} + +@(test) +test_page_size_applies_rotation :: proc(t: ^testing.T) { + doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] >>") + defer pb_destroy(&pb) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, err := page_get(doc, 0) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, page_bounds(&page), Rect{0, 0, 200, 100}) + + w, h := page_size(&page, 1) + testing.expect_value(t, w, 200) + testing.expect_value(t, h, 100) + + // Scale multiplies both axes. + w2, h2 := page_size(&page, 2) + testing.expect_value(t, w2, 400) + testing.expect_value(t, h2, 200) + + // A quarter turn swaps them. + page.rotate = 90 + w3, h3 := page_size(&page, 1) + testing.expect_value(t, w3, 100) + testing.expect_value(t, h3, 200) + + page.rotate = 180 + w4, h4 := page_size(&page, 1) + testing.expect_value(t, w4, 200) + testing.expect_value(t, h4, 100) +} + +// A fractional pixel must round outward, or the last sliver of content falls +// off the right edge of the bitmap. +@(test) +test_page_size_rounds_outward_and_clamps :: proc(t: ^testing.T) { + doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>") + defer pb_destroy(&pb) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, _ := page_get(doc, 0) + w, h := page_size(&page, 1.004) + testing.expect_value(t, w, 101) + testing.expect_value(t, h, 101) + + // A degenerate scale must not produce a zero-sized bitmap. + w2, h2 := page_size(&page, 0) + testing.expect(t, w2 >= 1 && h2 >= 1, "size is clamped to at least 1x1") + w3, h3 := page_size(&page, -5) + testing.expect(t, w3 >= 1 && h3 >= 1, "a negative scale is refused, not honoured") + + testing.expect_value(t, page_bounds(nil), DEFAULT_PAGE_RECT) + wn, hn := page_size(nil, 1) + testing.expect_value(t, wn, 1) + testing.expect_value(t, hn, 1) +} + +@(test) +test_page_user_unit :: proc(t: ^testing.T) { + doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /UserUnit 3 >>") + defer pb_destroy(&pb) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, _ := page_get(doc, 0) + testing.expect_value(t, page.user_unit, 3.0) + w, h := page_size(&page, 1) + testing.expect_value(t, w, 300) + testing.expect_value(t, h, 150) + + // Nonsense must fall back to 1 rather than collapsing the page. + bad, pb2 := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /UserUnit 0 >>") + defer pb_destroy(&pb2) + if bad != nil { + defer close(bad) + p2, _ := page_get(bad, 0) + testing.expect_value(t, p2.user_unit, 1.0) + } +} + +@(test) +test_page_content_single_stream :: proc(t: ^testing.T) { + doc, pb := build_page_doc( + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents 4 0 R >>", + "1 0 0 RG 0 0 m 10 10 l S") + defer pb_destroy(&pb) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, _ := page_get(doc, 0) + data, err := page_content(&page) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(data), "1 0 0 RG 0 0 m 10 10 l S") +} + +// An operator may be split across the boundary between two content streams +// (ISO 32000-2, 7.8.2), so they must be joined with a separator. Bare +// concatenation would lex "1 0 0 1 50" + "50 cm" as the number 5050. +@(test) +test_page_content_array_is_newline_joined :: proc(t: ^testing.T) { + pb := pb_make() + defer pb_destroy(&pb) + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents [4 0 R 5 0 R] >>") + pb_stream_obj(&pb, 4, "", transmute([]byte)string("1 0 0 1 50")) + pb_stream_obj(&pb, 5, "", transmute([]byte)string("50 cm")) + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 6 /Root 1 0 R >>", at) + + doc, _ := open_bytes(pb.buf[:]) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, _ := page_get(doc, 0) + data, err := page_content(&page) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, string(data), "1 0 0 1 50\n50 cm") +} + +@(test) +test_page_content_missing_and_nil :: proc(t: ^testing.T) { + doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") + defer pb_destroy(&pb) + if !testing.expect(t, doc != nil, "should open") { return } + defer close(doc) + + page, _ := page_get(doc, 0) + data, err := page_content(&page) + defer delete(data) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, len(data), 0) + + _, nerr := page_content(nil) + testing.expect_value(t, nerr, Error.Invalid_Argument) +} diff --git a/parser.odin b/parser.odin new file mode 100644 index 0000000..6fa339e --- /dev/null +++ b/parser.odin @@ -0,0 +1,514 @@ +package pdf + +import "core:mem" + +// Token stream to Object (ISO 32000-2, 7.3). +// +// The parser is written so that no input can make it fail outright: an +// unterminated array yields the elements it did collect, a dictionary key that +// is not a name is dropped, a stream whose /Length lies is measured by finding +// its `endstream`. Each of those records a `Repair` on the Document and +// carries on, because a file with one broken object usually still renders. An +// `Error` comes back only when there is genuinely no object to hand over. +// +// A nil Document is supported throughout: inline image dictionaries in a +// content stream are parsed with no file behind them. + +Parser :: struct { + lex: Lexer, + doc: ^Document, + // Everything the parser builds is allocated here and never freed + // individually, so this wants to be an arena -- `doc.allocator` for + // anything that belongs to the Document. + allocator: mem.Allocator, + depth: int, + // The indirect object being parsed, which is what its strings and stream + // are decrypted with. The zero Ref means "do not decrypt": that is the + // trailer, an inline image, and every object that came out of an object + // stream (those were decrypted with their container, ISO 32000-2, 7.5.7). + obj_ref: Ref, + + // Token lookahead. Two are needed and two are enough: telling "12 0 R" + // from "12 0 obj" and from a plain "12" is the only place in the grammar + // that needs to see past the token in hand. + lookahead: [2]Token, + n_lookahead: int, +} + +// parser_make prepares a parser over `data`. +// +// `data` is normally the whole file with `lex.pos` moved to the object, not a +// sub-slice: token offsets are then file offsets, which is what repairs +// record, and a stream payload can be a sub-slice of `doc.data` instead of a +// copy. +parser_make :: proc(doc: ^Document, data: []byte, allocator := context.allocator) -> Parser { + p := Parser{ + lex = lexer_make(data), + doc = doc, + allocator = allocator, + } + if p.allocator.procedure == nil { + p.allocator = context.allocator + } + return p +} + +// --------------------------------------------------------------------------- +// Token queue +// --------------------------------------------------------------------------- + +@(private = "file") +parser_next :: proc(p: ^Parser) -> Token { + if p.n_lookahead > 0 { + tok := p.lookahead[0] + p.lookahead[0] = p.lookahead[1] + p.n_lookahead -= 1 + return tok + } + return lexer_next(&p.lex, p.allocator) +} + +// parser_peek looks `n` tokens ahead (0 or 1) without consuming anything. +@(private = "file") +parser_peek :: proc(p: ^Parser, n: int) -> Token { + idx := clamp(n, 0, len(p.lookahead) - 1) + for p.n_lookahead <= idx { + p.lookahead[p.n_lookahead] = lexer_next(&p.lex, p.allocator) + p.n_lookahead += 1 + } + return p.lookahead[idx] +} + +// token_ends_container names the keywords that can only appear outside of an +// array or dictionary. Meeting one means a closing bracket was lost, and +// swallowing it would take the rest of the file with it. +@(private = "file") +token_ends_container :: proc(t: Token) -> bool { + if t.kind != .Keyword { return false } + switch t.text { + case "endobj", "stream", "endstream", "obj", "xref", "trailer", "startxref": + return true + } + return false +} + +// --------------------------------------------------------------------------- +// Objects +// --------------------------------------------------------------------------- + +// parse_object reads one object. Indirect references are returned as `Ref`; +// nothing is resolved here. +parse_object :: proc(p: ^Parser) -> (Object, Error) { + tok := parser_next(p) + + switch tok.kind { + case .EOF: + return nil, .Truncated + + case .Integer: + // "12 0 R" is a reference; "12 0 obj" is a header that ran into us, + // and "12 0" is two integers. Only the full three-token shape counts. + t1 := parser_peek(p, 0) + if t1.kind == .Integer && t1.int_value >= 0 && t1.int_value <= 65535 { + t2 := parser_peek(p, 1) + if t2.kind == .Keyword && t2.text == "R" { + p.n_lookahead = 0 + return make_ref(tok.int_value, t1.int_value), .None + } + } + return tok.int_value, .None + + case .Real: + return tok.real_value, .None + + case .String: + if tok.truncated { + add_repair(p.doc, .Unterminated_String, i64(tok.start), p.obj_ref) + } + return String(parser_decrypt_string(p, tok.bytes)), .None + + case .Name: + return Name(tok.text), .None + + case .Array_Open: + return parse_array(p) + + case .Dict_Open: + return parse_dict_or_stream(p) + + case .Keyword: + switch tok.text { + case "true": return true, .None + case "false": return false, .None + case "null": return nil, .None + } + return nil, .Unexpected_Token + + case .Array_Close, .Dict_Close, .Brace_Open, .Brace_Close: + return nil, .Unexpected_Token + + case .Error: + return nil, .Syntax_Error + } + + return nil, .Syntax_Error +} + +// make_ref validates an indirect reference. Object number 0 is the free-list +// head and can never be referenced, and a number past the format's limit +// cannot be in any xref table; both read as null, which is what a reference to +// a non-existent object means anyway (ISO 32000-2, 7.3.10). +@(private = "file") +make_ref :: proc(num, gen: i64) -> Object { + if num < 1 || num > MAX_OBJECT_NUMBER || gen < 0 || gen > 65535 { + return nil + } + return Ref{num = u32(num), gen = u16(gen)} +} + +@(private = "file") +parser_decrypt_string :: proc(p: ^Parser, raw: []byte) -> []byte { + if p.doc == nil || p.doc.encrypt == nil || p.obj_ref.num == 0 { + return raw + } + return decrypt_string(p.doc, p.obj_ref, raw) +} + +// --------------------------------------------------------------------------- +// Arrays +// --------------------------------------------------------------------------- + +@(private = "file") +parse_array :: proc(p: ^Parser) -> (Object, Error) { + if p.depth >= MAX_DEPTH { + return nil, .Recursion_Limit + } + p.depth += 1 + defer p.depth -= 1 + + a := new_array(p.allocator) + // One token consumes at least one byte, so the remaining input bounds the + // number of iterations. The counter is belt and braces against a token + // rule that stops advancing. + fuel := len(p.lex.data) - p.lex.pos + 8 + + for fuel > 0 { + fuel -= 1 + tok := parser_peek(p, 0) + switch { + case tok.kind == .Array_Close: + parser_next(p) + return a, .None + case tok.kind == .EOF: + add_repair(p.doc, .Unterminated_Array, i64(tok.start), p.obj_ref) + return a, .None + case tok.kind == .Dict_Close, token_ends_container(tok): + // A ']' was lost. Leave the token for the enclosing dictionary or + // the object reader; taking it would lose that structure too. + add_repair(p.doc, .Unterminated_Array, i64(tok.start), p.obj_ref) + return a, .None + } + + obj, err := parse_object(p) + if err == .Recursion_Limit { + return a, err + } + if err != .None { + // Junk between elements: the token was consumed, so step over it. + // A null *element* is kept -- array positions carry meaning. + continue + } + array_push(a, obj) + } + return a, .None +} + +// --------------------------------------------------------------------------- +// Dictionaries and streams +// --------------------------------------------------------------------------- + +@(private = "file") +parse_dict_or_stream :: proc(p: ^Parser) -> (Object, Error) { + d, err := parse_dict(p) + if err != .None { + // The nesting limit. Whatever was collected before it is not worth + // handing back as if it were the whole object, and an Object holding a + // nil ^Dict is not the null object. + return nil, err + } + // A stream is a dictionary followed by `stream`, and only ever as the + // whole body of an indirect object -- so only at the outermost level. + if p.depth == 0 { + tok := parser_peek(p, 0) + if tok.kind == .Keyword && tok.text == "stream" { + parser_next(p) + return parse_stream(p, d, tok.end), .None + } + } + return d, .None +} + +@(private = "file") +parse_dict :: proc(p: ^Parser) -> (^Dict, Error) { + if p.depth >= MAX_DEPTH { + return nil, .Recursion_Limit + } + p.depth += 1 + defer p.depth -= 1 + + d := new_dict(p.allocator) + fuel := len(p.lex.data) - p.lex.pos + 8 + + for fuel > 0 { + fuel -= 1 + key_tok := parser_peek(p, 0) + switch { + case key_tok.kind == .Dict_Close: + parser_next(p) + return d, .None + case key_tok.kind == .EOF: + add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref) + return d, .None + case token_ends_container(key_tok): + add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref) + return d, .None + } + + if key_tok.kind != .Name { + parser_next(p) + add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref) + // The value belongs to a key that cannot be recorded, so it goes + // with it; left behind it would be read as the next key. A stray + // bracket is junk on its own and takes nothing with it. + #partial switch key_tok.kind { + case .Array_Close, .Brace_Open, .Brace_Close, .Error: + case: + skip := parser_peek(p, 0) + if skip.kind != .Dict_Close && skip.kind != .EOF && !token_ends_container(skip) { + _, _ = parse_object(p) + } + } + continue + } + parser_next(p) + + // "/Key >>" -- a key whose value was lost. Closing the dictionary here + // keeps the '>>' meaningful instead of consuming it as the value. + vt := parser_peek(p, 0) + if vt.kind == .Dict_Close || vt.kind == .EOF || token_ends_container(vt) { + add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref) + if vt.kind == .Dict_Close { + parser_next(p) + } + return d, .None + } + + value, verr := parse_object(p) + if verr == .Recursion_Limit { + // The value is deeper than the library will follow, and its tokens + // are still on the stream, so nothing after it can be trusted to + // be a key. Fail the dictionary the way an array fails. + return d, verr + } + if verr != .None { + continue + } + // A key with a null value and an absent key mean the same thing + // everywhere in the format, so nulls are dropped at parse time. + if value != nil { + dict_set(d, key_tok.text, value) + } + } + return d, .None +} + +// parse_stream reads the payload that follows a `stream` keyword ending at +// `kw_end`. The payload is a sub-slice of the input, never a copy -- except +// when it has to be decrypted. +@(private = "file") +parse_stream :: proc(p: ^Parser, d: ^Dict, kw_end: int) -> Object { + data := p.lex.data + + // "stream" is followed by CRLF or LF, never by CR alone (ISO 32000-2, + // 7.3.8.1). Producers that emit a bare CR, or a space before the EOL, are + // common enough that both are tolerated. + i := kw_end + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i += 1 + } + if i < len(data) && data[i] == '\r' { i += 1 } + if i < len(data) && data[i] == '\n' { i += 1 } + body := i + + length := i64(-1) + if lv, ok := to_int(dict_get(p.doc, d, "Length")); ok { + length = lv + } + + end := -1 + after := -1 + // Trust /Length only if it fits in the file *and* `endstream` is where it + // says it is. A length that is merely plausible is not evidence. + if length >= 0 && length <= i64(len(data) - body) { + probe := Lexer{data = data, pos = body + int(length)} + if lexer_accept_keyword(&probe, "endstream") { + end = body + int(length) + after = probe.pos + } + } + + if end < 0 { + needle := "endstream" + idx := index_bytes(data[body:], transmute([]byte)needle) + if idx < 0 { + // No endstream at all: the file is truncated inside the stream. + // Everything that is left is the best available payload. + add_repair(p.doc, .Missing_Endstream, i64(body), p.obj_ref) + end = len(data) + after = len(data) + } else { + e := body + idx + after = e + len(needle) + // One EOL before `endstream` is the separator, not data. CRLF is + // one marker, so both bytes come off. + if e > body && data[e - 1] == '\n' { e -= 1 } + if e > body && data[e - 1] == '\r' { e -= 1 } + end = e + add_repair(p.doc, .Bad_Stream_Length, i64(body), p.obj_ref) + } + } + + raw := data[body:end] + // `raw` is a view of the file, so decrypt_stream has to return a new + // buffer rather than writing through this one. An object that came out of + // an object stream has the zero Ref and is already in the clear: its + // container was decrypted as a whole (ISO 32000-2, 7.5.7). + if p.doc != nil && p.doc.encrypt != nil && p.obj_ref.num != 0 { + raw = decrypt_stream(p.doc, p.obj_ref, raw, d) + } + + p.n_lookahead = 0 + p.lex.pos = clamp(after, 0, len(data)) + + s := new_stream(d, raw, p.allocator) + s.ref = p.obj_ref + return s +} + +// --------------------------------------------------------------------------- +// Indirect objects +// --------------------------------------------------------------------------- + +// parse_indirect_object_at reads "num gen obj endobj" at `offset`, +// an absolute index into `doc.data`. +// +// `got` is the reference the file actually has there, which is not always the +// one the cross-reference table promised; the caller compares them to detect a +// stale table. A header that is not exactly at `offset` is searched for +// nearby: incremental updaters that miscount by a byte or two are common +// enough that giving up on the first mismatch loses readable objects. +parse_indirect_object_at :: proc(doc: ^Document, offset: i64, want: Ref) -> (obj: Object, got: Ref, err: Error) { + if doc == nil { + return nil, {}, .Invalid_Argument + } + if offset < 0 || offset >= i64(len(doc.data)) { + return nil, {}, .Object_Not_Found + } + + start := int(offset) + num, gen, body, ok := object_header_at(doc.data, start) + if !ok { + num, gen, body, ok = object_header_near(doc.data, start, want) + // Offsets in a file with junk before "%PDF-" are sometimes measured + // from the header instead of from byte zero. + if !ok && doc.header_offset > 0 && offset + doc.header_offset < i64(len(doc.data)) { + num, gen, body, ok = object_header_near(doc.data, int(offset + doc.header_offset), want) + } + if ok { + add_repair(doc, .Xref_Entry_Mismatch, offset, want) + } + } + if !ok { + return nil, {}, .Object_Not_Found + } + + got = Ref{num = num, gen = gen} + + p := parser_make(doc, doc.data, doc.allocator) + p.lex.pos = body + p.obj_ref = got + + // "12 0 obj endobj" is an empty object, which is null rather than an error. + if lexer_at_keyword(&p.lex, "endobj") { + return nil, got, .None + } + + obj, err = parse_object(&p) + if err != .None { + return nil, got, err + } + return obj, got, .None +} + +// object_header_at matches "num gen obj" exactly at `pos`. +@(private = "file") +object_header_at :: proc(data: []byte, pos: int) -> (num: u32, gen: u16, body: int, ok: bool) { + if pos < 0 || pos >= len(data) { + return 0, 0, 0, false + } + lex := Lexer{data = data, pos = pos} + // A header is three tokens, two integers and a keyword, and this runs at + // every byte of a 129-wide window for every object of a damaged file: + // `lexer_next_structural` is what keeps a rejected offset from copying a + // string body the probe is about to throw away. + t1 := lexer_next_structural(&lex) + if t1.kind != .Integer || t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER { + return 0, 0, 0, false + } + t2 := lexer_next_structural(&lex) + if t2.kind == .Keyword && t2.text == "obj" { + // "12 obj": the generation number was dropped. Generation 0 is right + // in every file this has been seen in. + return u32(t1.int_value), 0, lex.pos, true + } + if t2.kind != .Integer || t2.int_value < 0 || t2.int_value > 65535 { + return 0, 0, 0, false + } + t3 := lexer_next_structural(&lex) + if t3.kind != .Keyword || t3.text != "obj" { + return 0, 0, 0, false + } + return u32(t1.int_value), u16(t2.int_value), lex.pos, true +} + +// OBJECT_HEADER_WINDOW is how far either side of a claimed offset a header is +// looked for. Wide enough for the off-by-a-few-bytes tables that broken +// incremental updates produce, narrow enough that it cannot wander into the +// next object. +@(private = "file") +OBJECT_HEADER_WINDOW :: 64 + +@(private = "file") +object_header_near :: proc(data: []byte, center: int, want: Ref) -> (num: u32, gen: u16, body: int, ok: bool) { + lo := max(center - OBJECT_HEADER_WINDOW, 0) + hi := min(center + OBJECT_HEADER_WINDOW, len(data) - 1) + + for i := lo; i <= hi; i += 1 { + // Only start where a token can start, or "123 0 obj" also matches at + // its second digit and yields object 23. + if data[i] < '0' || data[i] > '9' { continue } + if i > 0 && is_pdf_regular(data[i - 1]) { continue } + + n, g, b, hit := object_header_at(data, i) + if !hit { continue } + if want.num == 0 || n == want.num { + return n, g, b, true + } + if !ok { + // Remember the first header found, in case none matches: an object + // with the wrong number is still something the caller can judge. + num, gen, body, ok = n, g, b, true + } + } + return +} diff --git a/parser_test.odin b/parser_test.odin new file mode 100644 index 0000000..df1a680 --- /dev/null +++ b/parser_test.odin @@ -0,0 +1,895 @@ +package pdf + +import "core:mem" +import "core:mem/virtual" +import "core:slice" +import "core:strings" +import "core:testing" + +@(private = "file") +scratch_begin :: proc(t: ^testing.T, a: ^virtual.Arena) -> mem.Allocator { + err := virtual.arena_init_growing(a) + testing.expect(t, err == nil, "arena init failed") + return virtual.arena_allocator(a) +} + +@(private = "file") +bytes_of :: proc(s: string) -> []byte { + return transmute([]byte)s +} + +// test_doc builds a Document by hand. `open_bytes` cannot be used here: it +// needs an xref loader, and the point of these tests is the parser on its own. +// Nothing calls `close`, so the Document's own arena is never initialised -- +// everything lives in the caller's scratch arena instead. +@(private = "file") +test_doc :: proc(src: string, alloc: mem.Allocator) -> ^Document { + doc := new(Document, alloc) + doc.data = bytes_of(src) + doc.allocator = alloc + doc.backing = alloc + doc.xref = make([dynamic]Xref_Entry, alloc) + doc.cache = make(map[u32]Object, 16, alloc) + doc.loading = make(map[u32]bool, 8, alloc) + doc.objstm_cache = make(map[u32]^Objstm, 4, alloc) + doc.pages = make([dynamic]^Dict, alloc) + doc.page_refs = make([dynamic]Ref, alloc) + doc.repairs = make([dynamic]Repair, alloc) + return doc +} + +// test_xref_at registers object `num` as living at the offset of `header` in +// the file, so `get_object` can find it. +@(private = "file") +test_xref_at :: proc(doc: ^Document, num: u32, header: string) { + for len(doc.xref) <= int(num) { + append(&doc.xref, Xref_Entry{}) + } + doc.xref[num] = Xref_Entry{ + offset = i64(strings.index(string(doc.data), header)), + kind = .Normal, + } +} + +// Object holds a slice (String) and pointers, so it is not comparable with +// '=='. Direct objects are compared by value here; containers by identity. +@(private = "file") +object_same :: proc(a, b: Object) -> bool { + sa, a_is_str := a.(String) + sb, b_is_str := b.(String) + if a_is_str || b_is_str { + return a_is_str && b_is_str && slice.equal(transmute([]byte)sa, transmute([]byte)sb) + } + switch va in a { + case bool: vb, ok := b.(bool); return ok && va == vb + case i64: vb, ok := b.(i64); return ok && va == vb + case f64: vb, ok := b.(f64); return ok && va == vb + case Name: vb, ok := b.(Name); return ok && va == vb + case Ref: vb, ok := b.(Ref); return ok && va == vb + case String: return false + case ^Array: vb, ok := b.(^Array); return ok && va == vb + case ^Dict: vb, ok := b.(^Dict); return ok && va == vb + case ^Stream: vb, ok := b.(^Stream); return ok && va == vb + } + return b == nil +} + +@(private = "file") +parse_one :: proc(src: string, alloc: mem.Allocator) -> (Object, Error) { + p := parser_make(nil, bytes_of(src), alloc) + return parse_object(&p) +} + +@(private = "file") +has_repair :: proc(doc: ^Document, kind: Repair_Kind) -> bool { + for r in doc.repairs { + if r.kind == kind { return true } + } + return false +} + +// --------------------------------------------------------------------------- +// Scalars +// --------------------------------------------------------------------------- + +@(test) +test_parser_scalars :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: Object, + } + cases := []Case{ + {"true", true}, + {"false", false}, + {"null", nil}, + {"42", i64(42)}, + {"-7", i64(-7)}, + {"3.5", f64(3.5)}, + {"-.25", f64(-0.25)}, + {"/Name", Name("Name")}, + {"/", Name("")}, + {"/a#20b", Name("a b")}, + // A number a viewer would read as 0 must still be an object. + {"--3", i64(0)}, + } + + for c in cases { + obj, err := parse_one(c.src, alloc) + testing.expectf(t, err == .None, "%q: error %v", c.src, err) + testing.expectf(t, object_same(obj, c.want), "%q: got %v, want %v", c.src, obj, c.want) + } + + // Strings compare by content, not by pointer. + String_Case :: struct { + src: string, + want: string, + } + for c in ([]String_Case{ + {"(hi)", "hi"}, + {"<4869>", "Hi"}, + {"()", ""}, + {"(a\\061b)", "a1b"}, + }) { + obj, err := parse_one(c.src, alloc) + testing.expectf(t, err == .None, "%q: error %v", c.src, err) + b, ok := to_bytes(obj) + testing.expectf(t, ok, "%q: not a string", c.src) + testing.expectf(t, slice.equal(b, bytes_of(c.want)), "%q: got %q", c.src, string(b)) + } +} + +@(test) +test_parser_empty_and_junk :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + _, err := parse_one("", alloc) + testing.expect_value(t, err, Error.Truncated) + + _, err2 := parse_one(" % nothing but a comment\n", alloc) + testing.expect_value(t, err2, Error.Truncated) + + _, err3 := parse_one("endobj", alloc) + testing.expect_value(t, err3, Error.Unexpected_Token) + + _, err4 := parse_one(">", alloc) + testing.expect_value(t, err4, Error.Syntax_Error) + + _, err5 := parse_one("]", alloc) + testing.expect_value(t, err5, Error.Unexpected_Token) +} + +// --------------------------------------------------------------------------- +// References +// --------------------------------------------------------------------------- + +@(test) +test_parser_reference_vs_header :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: Object, + } + cases := []Case{ + {"1 0 R", Ref{1, 0}}, + {"12 65535 R", Ref{12, 65535}}, + {"1 0 R ", Ref{1, 0}}, + // "12 0 obj" is a header, not a reference: the first integer is the + // object here, and the caller sees the rest. + {"12 0 obj", i64(12)}, + {"12 0", i64(12)}, + {"12", i64(12)}, + // "1 0 0 RG" in a content stream is three numbers and an operator. + {"1 0 0 RG", i64(1)}, + {"1 0 RG", i64(1)}, + // Object 0 is the free-list head and can never be referenced. + {"0 0 R", nil}, + {"-1 0 R", nil}, + // Past the format's object-number ceiling. + {"8388608 0 R", nil}, + // A generation that cannot fit is not a reference at all. + {"1 70000 R", i64(1)}, + } + + for c in cases { + obj, err := parse_one(c.src, alloc) + testing.expectf(t, err == .None, "%q: error %v", c.src, err) + testing.expectf(t, object_same(obj, c.want), "%q: got %v, want %v", c.src, obj, c.want) + } +} + +@(test) +test_parser_references_in_containers :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + obj, err := parse_one("[1 0 R 2 0 R 3 4 R]", alloc) + testing.expect_value(t, err, Error.None) + a, ok := to_array(obj) + testing.expect(t, ok, "not an array") + testing.expect_value(t, array_len(a), 3) + testing.expect(t, object_same(array_get_raw(a, 0), Ref{1, 0}), "element 0 is not 1 0 R") + testing.expect(t, object_same(array_get_raw(a, 2), Ref{3, 4}), "element 2 is not 3 4 R") + + dobj, derr := parse_one("<< /Kids [4 0 R] /Parent 9 0 R /Count 1 >>", alloc) + testing.expect_value(t, derr, Error.None) + d, dok := to_dict(dobj) + testing.expect(t, dok, "not a dictionary") + ref, rok := dict_ref(d, "Parent") + testing.expect(t, rok, "/Parent is not a reference") + testing.expect_value(t, ref, Ref{9, 0}) +} + +// --------------------------------------------------------------------------- +// Arrays +// --------------------------------------------------------------------------- + +@(test) +test_parser_nested_arrays :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + obj, err := parse_one("[1 [2 [3 [4 5]]] 6]", alloc) + testing.expect_value(t, err, Error.None) + a, _ := to_array(obj) + testing.expect_value(t, array_len(a), 3) + testing.expect(t, object_same(array_get_raw(a, 0), i64(1)), "element 0 is not 1") + testing.expect(t, object_same(array_get_raw(a, 2), i64(6)), "element 2 is not 6") + + b, _ := to_array(array_get_raw(a, 1)) + testing.expect_value(t, array_len(b), 2) + c, _ := to_array(array_get_raw(b, 1)) + testing.expect_value(t, array_len(c), 2) + d, _ := to_array(array_get_raw(c, 1)) + testing.expect_value(t, array_len(d), 2) + testing.expect(t, object_same(array_get_raw(d, 1), i64(5)), "innermost element is not 5") + + // Nulls hold their position: a /Widths or /Decode array is read by index. + nobj, nerr := parse_one("[1 null 2]", alloc) + testing.expect_value(t, nerr, Error.None) + na, _ := to_array(nobj) + testing.expect_value(t, array_len(na), 3) + testing.expect(t, is_null(array_get_raw(na, 1)), "the null element was dropped") + + // Mixed types, no separators needed between delimited objects. + mobj, merr := parse_one("[/A(b)<43>[]<< >>true]", alloc) + testing.expect_value(t, merr, Error.None) + ma, _ := to_array(mobj) + testing.expect_value(t, array_len(ma), 6) +} + +@(test) +test_parser_depth_limit :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + build :: proc(n: int, alloc: mem.Allocator) -> string { + b := strings.builder_make(0, 2 * n, alloc) + for _ in 0 ..< n { strings.write_byte(&b, '[') } + for _ in 0 ..< n { strings.write_byte(&b, ']') } + return strings.to_string(b) + } + + ok_src := build(MAX_DEPTH, alloc) + _, err := parse_one(ok_src, alloc) + testing.expectf(t, err == .None, "%d levels should parse, got %v", MAX_DEPTH, err) + + deep := build(MAX_DEPTH + 1, alloc) + _, derr := parse_one(deep, alloc) + testing.expect_value(t, derr, Error.Recursion_Limit) + + // Dictionaries are bounded by the same counter, and a mixed nest too. + dict_src := strings.builder_make(0, 8 * MAX_DEPTH, alloc) + for _ in 0 ..< MAX_DEPTH + 1 { strings.write_string(&dict_src, "<< /A ") } + for _ in 0 ..< MAX_DEPTH + 1 { strings.write_string(&dict_src, ">>") } + _, dicterr := parse_one(strings.to_string(dict_src), alloc) + testing.expect_value(t, dicterr, Error.Recursion_Limit) + + // A pathological file is not allowed to recurse the stack away either. + runaway := strings.builder_make(0, 4096, alloc) + for _ in 0 ..< 4000 { strings.write_byte(&runaway, '[') } + _, rerr := parse_one(strings.to_string(runaway), alloc) + testing.expect_value(t, rerr, Error.Recursion_Limit) +} + +@(test) +test_parser_unterminated_array :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + doc := test_doc("[1 2 3", alloc) + p := parser_make(doc, doc.data, alloc) + obj, err := parse_object(&p) + testing.expect_value(t, err, Error.None) + a, _ := to_array(obj) + testing.expect_value(t, array_len(a), 3) + testing.expect(t, has_repair(doc, .Unterminated_Array), "no Unterminated_Array repair") + testing.expect(t, doc.is_damaged, "document should be marked damaged") + + // A '>>' inside an array closes the array without being eaten: the + // enclosing dictionary still needs it. + doc2 := test_doc("<< /A [1 2 >> /B 3", alloc) + p2 := parser_make(doc2, doc2.data, alloc) + obj2, err2 := parse_object(&p2) + testing.expect_value(t, err2, Error.None) + d2, _ := to_dict(obj2) + a2 := dict_array(nil, d2, "A") + testing.expect_value(t, array_len(a2), 2) + + // Likewise a keyword that can only appear outside a container. + doc3 := test_doc("[1 2 endobj", alloc) + p3 := parser_make(doc3, doc3.data, alloc) + obj3, _ := parse_object(&p3) + a3, _ := to_array(obj3) + testing.expect_value(t, array_len(a3), 2) + + // Junk between elements is stepped over, not fatal. + doc4 := test_doc("[1 ) 2 endobj]", alloc) + p4 := parser_make(doc4, doc4.data, alloc) + obj4, err4 := parse_object(&p4) + testing.expect_value(t, err4, Error.None) + a4, _ := to_array(obj4) + testing.expect_value(t, array_len(a4), 2) +} + +// --------------------------------------------------------------------------- +// Dictionaries +// --------------------------------------------------------------------------- + +@(test) +test_parser_dict_basics :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + obj, err := parse_one("<< /Type /Page /Count 3 /Box [0 0 612 792] /S (x) /Null null >>", alloc) + testing.expect_value(t, err, Error.None) + d, ok := to_dict(obj) + testing.expect(t, ok, "not a dictionary") + testing.expect_value(t, dict_name(nil, d, "Type"), "Page") + testing.expect_value(t, dict_int(nil, d, "Count"), i64(3)) + testing.expect_value(t, array_len(dict_array(nil, d, "Box")), 4) + // A key with a null value is indistinguishable from an absent key, so it + // is never stored. + testing.expect(t, !dict_has(d, "Null"), "a null value must be dropped") + testing.expect_value(t, len(d^), 4) + + // The last of a repeated key wins, which is what every viewer does. + dup, _ := parse_one("<< /A 1 /A 2 >>", alloc) + dd, _ := to_dict(dup) + testing.expect_value(t, dict_int(nil, dd, "A"), i64(2)) + + // Nesting. + nest, _ := parse_one("<< /R << /Font << /F1 5 0 R >> >> >>", alloc) + nd, _ := to_dict(nest) + fonts := dict_dict(nil, dict_dict(nil, nd, "R"), "Font") + ref, rok := dict_ref(fonts, "F1") + testing.expect(t, rok, "/F1 is not a reference") + testing.expect_value(t, ref, Ref{5, 0}) + + empty, eerr := parse_one("<<>>", alloc) + testing.expect_value(t, eerr, Error.None) + ed, eok := to_dict(empty) + testing.expect(t, eok, "<<>> is a dictionary") + testing.expect_value(t, len(ed^), 0) +} + +@(test) +test_parser_dict_bad_key :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // A key that is not a name is dropped with its value; the rest of the + // dictionary is still read. + doc := test_doc("<< 5 6 /A 1 (k) (v) /B 2 >>", alloc) + p := parser_make(doc, doc.data, alloc) + obj, err := parse_object(&p) + testing.expect_value(t, err, Error.None) + d, _ := to_dict(obj) + testing.expect_value(t, dict_int(nil, d, "A"), i64(1)) + testing.expect_value(t, dict_int(nil, d, "B"), i64(2)) + testing.expect_value(t, len(d^), 2) + testing.expect(t, has_repair(doc, .Unterminated_Dict), "no repair recorded for the bad keys") +} + +@(test) +test_parser_dict_unterminated :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + doc := test_doc("<< /A 1 /B 2", alloc) + p := parser_make(doc, doc.data, alloc) + obj, err := parse_object(&p) + testing.expect_value(t, err, Error.None) + d, _ := to_dict(obj) + testing.expect_value(t, dict_int(nil, d, "A"), i64(1)) + testing.expect_value(t, dict_int(nil, d, "B"), i64(2)) + testing.expect(t, has_repair(doc, .Unterminated_Dict), "no Unterminated_Dict repair") + + // A key whose value is missing must not swallow the closing '>>'. + doc2 := test_doc("<< /A 1 /B >>", alloc) + p2 := parser_make(doc2, doc2.data, alloc) + obj2, _ := parse_object(&p2) + d2, _ := to_dict(obj2) + testing.expect_value(t, dict_int(nil, d2, "A"), i64(1)) + testing.expect(t, !dict_has(d2, "B"), "/B has no value to store") + tail := lexer_next(&p2.lex, alloc) + testing.expect_value(t, tail.kind, Token_Kind.EOF) + + // Truncated in the middle of a key. + doc3 := test_doc("<< /A 1 /B", alloc) + p3 := parser_make(doc3, doc3.data, alloc) + obj3, err3 := parse_object(&p3) + testing.expect_value(t, err3, Error.None) + d3, _ := to_dict(obj3) + testing.expect_value(t, len(d3^), 1) +} + +@(test) +test_parser_string_repairs :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + doc := test_doc("<< /T (unterminated", alloc) + p := parser_make(doc, doc.data, alloc) + obj, err := parse_object(&p) + testing.expect_value(t, err, Error.None) + d, _ := to_dict(obj) + testing.expect(t, slice.equal(dict_bytes(nil, d, "T"), bytes_of("unterminated")), + "the value of an unterminated string is still usable") + testing.expect(t, has_repair(doc, .Unterminated_String), "no Unterminated_String repair") +} + +// --------------------------------------------------------------------------- +// Streams +// --------------------------------------------------------------------------- + +@(private = "file") +STREAM_SRC :: "%PDF-1.7\n1 0 obj\n<< /Length 5 >>\nstream\r\nHello\nendstream\nendobj\n" + +@(private = "file") +parse_first_object :: proc(doc: ^Document, alloc: mem.Allocator) -> (Object, Ref, Error) { + return parse_indirect_object_at(doc, i64(strings.index(string(doc.data), "1 0 obj")), Ref{1, 0}) +} + +@(test) +test_parser_stream_exact_length :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + doc := test_doc(STREAM_SRC, alloc) + obj, got, err := parse_first_object(doc, alloc) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, got, Ref{1, 0}) + + s, ok := to_stream(obj) + testing.expect(t, ok, "not a stream") + testing.expect(t, slice.equal(s.raw, bytes_of("Hello")), "wrong payload") + testing.expect_value(t, s.ref, Ref{1, 0}) + testing.expect_value(t, dict_int(doc, s.dict, "Length"), i64(5)) + testing.expect(t, len(doc.repairs) == 0, "a well-formed stream needs no repair") + + // The payload is a view into the file, not a copy: a 200 MB document must + // not be doubled in memory by parsing it. + base := uintptr(raw_data(doc.data)) + at := uintptr(raw_data(s.raw)) + testing.expect(t, at >= base && at < base + uintptr(len(doc.data)), + "the stream payload was copied out of doc.data") +} + +@(test) +test_parser_stream_bad_length :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: string, + } + cases := []Case{ + // Too long, too short, absurd, negative, missing, not a number. + {"%PDF-1.7\n1 0 obj\n<< /Length 999 >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length 2 >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length 99999999999999 >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length -5 >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length /Bogus >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + // The EOL before `endstream` is a separator, not payload -- one + // marker's worth, whether it is LF, CR or CRLF. + {"%PDF-1.7\n1 0 obj\n<< /Length 0 >>\nstream\r\nHello\r\nendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length 0 >>\nstream\r\nHello\rendstream\nendobj\n", "Hello"}, + {"%PDF-1.7\n1 0 obj\n<< /Length 0 >>\nstream\r\nHelloendstream\nendobj\n", "Hello"}, + // Binary payload with an embedded "endstream"-looking prefix. + {"%PDF-1.7\n1 0 obj\n<< /Length 7 >>\nstream\r\nend\x00\x01\x02x\nendstream\nendobj\n", + "end\x00\x01\x02x"}, + } + + for c in cases { + doc := test_doc(c.src, alloc) + obj, _, err := parse_first_object(doc, alloc) + testing.expectf(t, err == .None, "%q: error %v", c.src, err) + s, ok := to_stream(obj) + testing.expectf(t, ok, "%q: not a stream", c.src) + if !ok { continue } + testing.expectf(t, slice.equal(s.raw, bytes_of(c.want)), + "%q: payload %q, want %q", c.src, string(s.raw), c.want) + } + + // The first case above is the canonical one: it must be reported. + doc := test_doc(cases[0].src, alloc) + parse_first_object(doc, alloc) + testing.expect(t, has_repair(doc, .Bad_Stream_Length), "no Bad_Stream_Length repair") +} + +@(test) +test_parser_stream_indirect_length :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + src := "%PDF-1.7\n" + + "1 0 obj\n<< /Length 5 0 R >>\nstream\r\nHello\nendstream\nendobj\n" + + "5 0 obj\n5\nendobj\n" + doc := test_doc(src, alloc) + test_xref_at(doc, 5, "5 0 obj") + + obj, _, err := parse_first_object(doc, alloc) + testing.expect_value(t, err, Error.None) + s, ok := to_stream(obj) + testing.expect(t, ok, "not a stream") + testing.expect(t, slice.equal(s.raw, bytes_of("Hello")), "wrong payload") + testing.expect(t, !has_repair(doc, .Bad_Stream_Length), + "an indirect /Length that is right needs no repair") + + // An indirect /Length pointing at nothing falls back to the scan. + src2 := "%PDF-1.7\n1 0 obj\n<< /Length 9 0 R >>\nstream\r\nHello\nendstream\nendobj\n" + doc2 := test_doc(src2, alloc) + obj2, _, err2 := parse_first_object(doc2, alloc) + testing.expect_value(t, err2, Error.None) + s2, ok2 := to_stream(obj2) + testing.expect(t, ok2, "not a stream") + testing.expect(t, slice.equal(s2.raw, bytes_of("Hello")), "wrong payload") + testing.expect(t, has_repair(doc2, .Bad_Stream_Length), "no Bad_Stream_Length repair") + + // An indirect /Length that points back at the stream's own object must not + // recurse: the cycle guard in get_object turns it into null. + src3 := "%PDF-1.7\n1 0 obj\n<< /Length 1 0 R >>\nstream\r\nHello\nendstream\nendobj\n" + doc3 := test_doc(src3, alloc) + test_xref_at(doc3, 1, "1 0 obj") + obj3, _, err3 := parse_first_object(doc3, alloc) + testing.expect_value(t, err3, Error.None) + s3, ok3 := to_stream(obj3) + testing.expect(t, ok3, "not a stream") + testing.expect(t, slice.equal(s3.raw, bytes_of("Hello")), "wrong payload") +} + +@(test) +test_parser_stream_eol_tolerance :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + Case :: struct { + src: string, + want: string, + } + cases := []Case{ + // CRLF (the only spelling the spec allows), LF, a bare CR, and a + // trailing space before the EOL. All four are in the wild. + {"%PDF\n1 0 obj\n<< /Length 5 >>\nstream\r\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF\n1 0 obj\n<< /Length 5 >>\nstream\nHello\nendstream\nendobj\n", "Hello"}, + {"%PDF\n1 0 obj\n<< /Length 5 >>\nstream\rHello\nendstream\nendobj\n", "Hello"}, + {"%PDF\n1 0 obj\n<< /Length 5 >>\nstream \r\nHello\nendstream\nendobj\n", "Hello"}, + // A payload that starts with what looks like an EOL: only one marker + // is consumed, so the leading LF here is data. + {"%PDF\n1 0 obj\n<< /Length 6 >>\nstream\r\n\nHello\nendstream\nendobj\n", "\nHello"}, + // Zero-length streams are legal and common (empty appearance streams). + {"%PDF\n1 0 obj\n<< /Length 0 >>\nstream\r\n\nendstream\nendobj\n", ""}, + } + + for c in cases { + doc := test_doc(c.src, alloc) + obj, _, err := parse_first_object(doc, alloc) + testing.expectf(t, err == .None, "%q: error %v", c.src, err) + s, ok := to_stream(obj) + testing.expectf(t, ok, "%q: not a stream", c.src) + if !ok { continue } + testing.expectf(t, slice.equal(s.raw, bytes_of(c.want)), + "%q: payload %q, want %q", c.src, string(s.raw), c.want) + } +} + +@(test) +test_parser_stream_missing_endstream :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + src := "%PDF-1.7\n1 0 obj\n<< /Length 9999 >>\nstream\r\nHello, truncated" + doc := test_doc(src, alloc) + obj, _, err := parse_first_object(doc, alloc) + testing.expect_value(t, err, Error.None) + s, ok := to_stream(obj) + testing.expect(t, ok, "not a stream") + testing.expect(t, slice.equal(s.raw, bytes_of("Hello, truncated")), + "a truncated stream keeps what is there") + testing.expect(t, has_repair(doc, .Missing_Endstream), "no Missing_Endstream repair") +} + +@(test) +test_parser_stream_only_at_top_level :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // A `stream` keyword after a *nested* dictionary is not a stream: only the + // body of an indirect object can be one. + obj, err := parse_one("[ << /A 1 >> stream ]", alloc) + testing.expect_value(t, err, Error.None) + a, _ := to_array(obj) + testing.expect_value(t, array_len(a), 1) + _, is_dict := to_dict(array_get_raw(a, 0)) + testing.expect(t, is_dict, "the inner object is a plain dictionary") + _, is_stream := to_stream(array_get_raw(a, 0)) + testing.expect(t, !is_stream, "a nested dictionary must not become a stream") +} + +// --------------------------------------------------------------------------- +// Indirect objects +// --------------------------------------------------------------------------- + +@(test) +test_parse_indirect_object_at :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // The trailing padding puts the junk further from object 8 than the + // window search reaches. + src :: "%PDF-1.7\n7 0 obj\n<< /Type /Page >>\nendobj\n8 2 obj\n42\nendobj\n" + + " " + + " " + + "not an object at all\n" + doc := test_doc(src, alloc) + at7 := i64(strings.index(src, "7 0 obj")) + at8 := i64(strings.index(src, "8 2 obj")) + + obj, got, err := parse_indirect_object_at(doc, at7, Ref{7, 0}) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, got, Ref{7, 0}) + testing.expect_value(t, dict_name(doc, to_dict(obj) or_else nil, "Type"), "Page") + + obj8, got8, err8 := parse_indirect_object_at(doc, at8, Ref{8, 2}) + testing.expect_value(t, err8, Error.None) + testing.expect_value(t, got8, Ref{8, 2}) + testing.expect(t, object_same(obj8, i64(42)), "object 8 is not 42") + + // An offset a few bytes off is searched around, and the reference that was + // actually found comes back so a stale table can be detected. + for delta in ([]i64{-3, -1, 1, 4, 12}) { + o, g, e := parse_indirect_object_at(doc, at7 + delta, Ref{7, 0}) + testing.expectf(t, e == .None, "delta %d: error %v", delta, e) + testing.expectf(t, g == Ref{7, 0}, "delta %d: got %v", delta, g) + testing.expectf(t, o != nil, "delta %d: no object", delta) + } + + // The window must not match the digits inside a longer number: "8 2 obj" + // found from an offset near object 7 is still object 8, never object 2. + _, gnear, _ := parse_indirect_object_at(doc, at8 - 2, Ref{8, 0}) + testing.expect_value(t, gnear, Ref{8, 2}) + + // Nowhere near an object header. + _, _, enone := parse_indirect_object_at(doc, i64(strings.index(src, "not an object")), Ref{9, 0}) + testing.expect_value(t, enone, Error.Object_Not_Found) + + // Out of range offsets, including the ones a broken xref produces. + for bad in ([]i64{-1, 0x7fff_ffff, i64(len(src)), i64(len(src)) + 1000}) { + _, _, ebad := parse_indirect_object_at(doc, bad, Ref{7, 0}) + testing.expectf(t, ebad == .Object_Not_Found, "offset %d: error %v", bad, ebad) + } + + // A nil Document is a caller error, not a crash. + _, _, enil := parse_indirect_object_at(nil, 0, Ref{1, 0}) + testing.expect_value(t, enil, Error.Invalid_Argument) +} + +@(test) +test_parse_indirect_object_stale_xref :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // The offset points at object 4 while the table promised object 3. The + // caller gets the truth and decides; here it is close enough that the + // window search must not "helpfully" find something else. + src := "%PDF-1.7\n4 0 obj\n(four)\nendobj\n" + doc := test_doc(src, alloc) + _, got, err := parse_indirect_object_at(doc, i64(strings.index(src, "4 0 obj")), Ref{3, 0}) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, got, Ref{4, 0}) +} + +@(test) +test_parse_indirect_object_empty_body :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // "1 0 obj endobj" is an object with no body, which is null. + src := "%PDF-1.7\n1 0 obj\nendobj\n" + doc := test_doc(src, alloc) + obj, got, err := parse_indirect_object_at(doc, i64(strings.index(src, "1 0 obj")), Ref{1, 0}) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, got, Ref{1, 0}) + testing.expect(t, is_null(obj), "an empty object body is null") + + // A missing generation number is tolerated: "1 obj" is generation 0. + src2 := "%PDF-1.7\n1 obj\n(x)\nendobj\n" + doc2 := test_doc(src2, alloc) + obj2, got2, err2 := parse_indirect_object_at(doc2, i64(strings.index(src2, "1 obj")), Ref{1, 0}) + testing.expect_value(t, err2, Error.None) + testing.expect_value(t, got2, Ref{1, 0}) + testing.expect(t, obj2 != nil, "the body should still be read") +} + +@(test) +test_parse_indirect_object_header_offset :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // Junk before "%PDF-" is common (mail headers, HTTP responses), and some + // of those files measure their offsets from the header instead of byte 0. + junk :: "MIME-Version: 1.0\r\nContent-Type: application/pdf\r\n\r\n" + src :: junk + "%PDF-1.4\n3 0 obj\n(payload)\nendobj\n" + doc := test_doc(src, alloc) + doc.header_offset = i64(len(junk)) + + relative := i64(strings.index(src, "3 0 obj")) - doc.header_offset + obj, got, err := parse_indirect_object_at(doc, relative, Ref{3, 0}) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, got, Ref{3, 0}) + testing.expect(t, slice.equal(to_bytes(obj) or_else nil, bytes_of("payload")), "wrong payload") +} + +// --------------------------------------------------------------------------- +// Fuzz-shaped +// --------------------------------------------------------------------------- + +@(private = "file") +FUZZ_SRC :: "%PDF-1.7\n12 0 obj\n<< /Type /Page /Parent 3 0 R /Kids [4 0 R 5 0 R] " + + "/S (a\\(b\\) c) /H <41 42 43> /N /A#20B /D [1 [2 [3 [4]]]] /L 1e3 /Length 4 >>\n" + + "stream\r\nBODY\nendstream\nendobj\n" + +@(test) +test_parser_survives_truncation :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // Every prefix of a real object must parse or fail, never hang or crash. + whole := FUZZ_SRC + for n in 0 ..= len(whole) { + src := whole[:n] + doc := test_doc(src, alloc) + at := strings.index(src, "12 0 obj") + if at >= 0 { + _, _, _ = parse_indirect_object_at(doc, i64(at), Ref{12, 0}) + } + p := parser_make(doc, doc.data, alloc) + _, _ = parse_object(&p) + testing.expectf(t, p.lex.pos <= len(src), "truncation %d ran past the end", n) + } +} + +@(test) +test_parser_survives_corruption :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // Single-byte substitutions over the same object, cycling through the + // bytes that carry syntactic weight. + poison := "()<>[]{}/%\\ \r\n0123456789.+-eRobj\x00\xff" + buf := make([]byte, len(FUZZ_SRC), alloc) + for i in 0 ..< len(FUZZ_SRC) { + copy(buf, FUZZ_SRC) + buf[i] = poison[i % len(poison)] + doc := test_doc(string(buf), alloc) + at := strings.index(string(buf), "12 0 obj") + if at >= 0 { + _, _, _ = parse_indirect_object_at(doc, i64(at), Ref{12, 0}) + } + p := parser_make(doc, doc.data, alloc) + _, _ = parse_object(&p) + testing.expectf(t, p.lex.pos <= len(buf), "corruption at %d ran past the end", i) + } +} + +@(test) +test_parser_cyclic_and_absurd :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // A reference chain that loops. `resolve` bounds it; nothing here should + // recurse. + src := "%PDF\n1 0 obj\n2 0 R\nendobj\n2 0 obj\n1 0 R\nendobj\n" + doc := test_doc(src, alloc) + test_xref_at(doc, 1, "1 0 obj") + test_xref_at(doc, 2, "2 0 obj") + testing.expect(t, is_null(resolve(doc, Object(Ref{1, 0}))), "a reference loop resolves to null") + + // An object that refers to itself. Resolving /Self is not an error -- it + // is the same object -- but it must terminate, and the parse that had to + // read /Length out of it must not have recursed. + src2 := "%PDF\n1 0 obj\n<< /Self 1 0 R >>\nendobj\n" + doc2 := test_doc(src2, alloc) + test_xref_at(doc2, 1, "1 0 obj") + d, ok := to_dict(get_object(doc2, Ref{1, 0})) + testing.expect(t, ok, "the object itself is still readable") + self, self_ok := to_dict(dict_get(doc2, d, "Self")) + testing.expect(t, self_ok && self == d, "the self-reference should resolve to the object") + + // A dictionary whose value is twice as deep as the limit: the limit is + // reported, and the recursion stops at MAX_DEPTH frames rather than at the + // end of the stack. + deep := strings.builder_make(0, 256, alloc) + strings.write_string(&deep, "<< /A ") + for _ in 0 ..< 2 * MAX_DEPTH { strings.write_string(&deep, "[") } + _, err := parse_one(strings.to_string(deep), alloc) + testing.expect_value(t, err, Error.Recursion_Limit) +} + +// --------------------------------------------------------------------------- +// Decryption wiring +// +// The cipher itself lives in crypt.odin; what belongs to the parser is which +// Ref a string or payload is decrypted with, and the rule that objects out of +// an object stream are not decrypted individually. +// --------------------------------------------------------------------------- + +@(test) +test_parser_obj_ref_defaults_to_zero :: proc(t: ^testing.T) { + arena: virtual.Arena + alloc := scratch_begin(t, &arena) + defer virtual.arena_destroy(&arena) + + // A fresh parser carries the zero Ref, which is the signal not to decrypt. + // Object-stream contents and inline images are parsed exactly this way. + doc := test_doc("<< /Length 4 >>\nstream\r\nBODY\nendstream\n", alloc) + p := parser_make(doc, doc.data, alloc) + testing.expect_value(t, p.obj_ref, Ref{}) + + obj, err := parse_object(&p) + testing.expect_value(t, err, Error.None) + s, ok := to_stream(obj) + testing.expect(t, ok, "not a stream") + testing.expect_value(t, s.ref, Ref{}) + + // Parsed as an indirect object instead, the stream carries the identity + // its key would be derived from. + doc2 := test_doc(STREAM_SRC, alloc) + obj2, _, err2 := parse_first_object(doc2, alloc) + testing.expect_value(t, err2, Error.None) + s2, ok2 := to_stream(obj2) + testing.expect(t, ok2, "not a stream") + testing.expect_value(t, s2.ref, Ref{1, 0}) +} diff --git a/path.odin b/path.odin new file mode 100644 index 0000000..3a0c37a --- /dev/null +++ b/path.odin @@ -0,0 +1,554 @@ +package pdf + +import "core:math" + +// Path_Verb tags one element of a path. The point count each verb consumes is +// fixed: Move and Line take one, Curve takes three (two controls and an end +// point), Close takes none. +Path_Verb :: enum u8 { + Move, + Line, + Curve, + Close, +} + +// Path is a sequence of subpaths in one coordinate space. +// +// Verbs and points are kept in parallel arrays rather than as a tagged union +// per element: a page of text is tens of thousands of elements, and the +// rasteriser walks them once per fill, so the layout that keeps points +// contiguous is worth the small awkwardness of the cursor arithmetic. +Path :: struct { + verbs: [dynamic]Path_Verb, + points: [dynamic]Point, + // Start of the current subpath, for Close. + start: Point, + // Current point. Meaningless before the first Move. + cur: Point, + open: bool, +} + +path_make :: proc(allocator := context.allocator, cap := 32) -> Path { + return Path{ + verbs = make([dynamic]Path_Verb, 0, cap, allocator), + points = make([dynamic]Point, 0, cap, allocator), + } +} + +path_destroy :: proc(p: ^Path) { + if p == nil { return } + delete(p.verbs) + delete(p.points) + p^ = {} +} + +path_clear :: proc(p: ^Path) { + clear(&p.verbs) + clear(&p.points) + p.open = false +} + +path_is_empty :: proc(p: ^Path) -> bool { + return p == nil || len(p.verbs) == 0 +} + +// --------------------------------------------------------------------------- +// Construction +// +// Every entry point drops a non-finite coordinate rather than storing it. A +// single NaN reaching the rasteriser turns a fill into an unbounded scanline +// walk, and content streams contain NaNs -- usually from a division by a zero +// text size -- often enough that filtering here is cheaper than checking in +// the inner loop. +// --------------------------------------------------------------------------- + +path_move_to :: proc(p: ^Path, x, y: f64) { + if !is_finite(x) || !is_finite(y) { return } + append(&p.verbs, Path_Verb.Move) + append(&p.points, Point{x, y}) + p.start = Point{x, y} + p.cur = Point{x, y} + p.open = true +} + +path_line_to :: proc(p: ^Path, x, y: f64) { + if !is_finite(x) || !is_finite(y) { return } + // A line with no current point starts a subpath instead of being dropped: + // content streams do emit "l" before "m", and viewers treat it as a move. + if !p.open { + path_move_to(p, x, y) + return + } + append(&p.verbs, Path_Verb.Line) + append(&p.points, Point{x, y}) + p.cur = Point{x, y} +} + +path_curve_to :: proc(p: ^Path, x1, y1, x2, y2, x3, y3: f64) { + if !is_finite(x1) || !is_finite(y1) || !is_finite(x2) || + !is_finite(y2) || !is_finite(x3) || !is_finite(y3) { + return + } + if !p.open { + path_move_to(p, x1, y1) + } + append(&p.verbs, Path_Verb.Curve) + append(&p.points, Point{x1, y1}, Point{x2, y2}, Point{x3, y3}) + p.cur = Point{x3, y3} +} + +// path_curve_to_v is the "v" operator: the first control point is the current +// point. +path_curve_to_v :: proc(p: ^Path, x2, y2, x3, y3: f64) { + path_curve_to(p, p.cur.x, p.cur.y, x2, y2, x3, y3) +} + +// path_curve_to_y is the "y" operator: the second control point is the end +// point. +path_curve_to_y :: proc(p: ^Path, x1, y1, x3, y3: f64) { + path_curve_to(p, x1, y1, x3, y3, x3, y3) +} + +path_close :: proc(p: ^Path) { + if !p.open || len(p.verbs) == 0 { return } + if p.verbs[len(p.verbs) - 1] == .Close { return } + append(&p.verbs, Path_Verb.Close) + p.cur = p.start +} + +// path_rect appends a closed rectangle, in the winding direction the "re" +// operator specifies (counter-clockwise in user space). +path_rect :: proc(p: ^Path, x, y, w, h: f64) { + if !is_finite(x) || !is_finite(y) || !is_finite(w) || !is_finite(h) { return } + path_move_to(p, x, y) + path_line_to(p, x + w, y) + path_line_to(p, x + w, y + h) + path_line_to(p, x, y + h) + path_close(p) +} + +// path_ellipse appends a closed ellipse inscribed in the rectangle, built from +// four cubics. The magic constant is the standard circle-to-cubic ratio. +path_ellipse :: proc(p: ^Path, r: Rect) { + K :: 0.5522847498307933 + cx := (r.x0 + r.x1) * 0.5 + cy := (r.y0 + r.y1) * 0.5 + rx := (r.x1 - r.x0) * 0.5 + ry := (r.y1 - r.y0) * 0.5 + ox := rx * K + oy := ry * K + path_move_to(p, cx + rx, cy) + path_curve_to(p, cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry) + path_curve_to(p, cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy) + path_curve_to(p, cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry) + path_curve_to(p, cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy) + path_close(p) +} + +// path_append copies every element of `src` onto `dst`. +path_append :: proc(dst: ^Path, src: ^Path) { + if src == nil { return } + append(&dst.verbs, ..src.verbs[:]) + append(&dst.points, ..src.points[:]) + dst.cur = src.cur + dst.start = src.start + dst.open = src.open || dst.open +} + +// path_append_transformed copies `src` onto `dst` with `m` applied. +path_append_transformed :: proc(dst: ^Path, src: ^Path, m: Matrix) { + if src == nil { return } + for pt in src.points { + append(&dst.points, transform_point(m, pt)) + } + append(&dst.verbs, ..src.verbs[:]) + dst.cur = transform_point(m, src.cur) + dst.start = transform_point(m, src.start) + dst.open = src.open || dst.open +} + +path_transform :: proc(p: ^Path, m: Matrix) { + for &pt in p.points { + pt = transform_point(m, pt) + } + p.cur = transform_point(m, p.cur) + p.start = transform_point(m, p.start) +} + +path_clone :: proc(p: ^Path, allocator := context.allocator) -> Path { + out := path_make(allocator, max(len(p.verbs), 8)) + append(&out.verbs, ..p.verbs[:]) + append(&out.points, ..p.points[:]) + out.cur = p.cur + out.start = p.start + out.open = p.open + return out +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +// path_bounds is the control-point hull, which is a conservative +// over-estimate for curves: a cubic never leaves the convex hull of its four +// points. Good enough for culling; `path_tight_bounds` is the exact one. +path_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect { + if p == nil || len(p.points) == 0 { return RECT_EMPTY } + identity := matrix_is_identity(m) + first := identity ? p.points[0] : transform_point(m, p.points[0]) + r := Rect{first.x, first.y, first.x, first.y} + for i in 1 ..< len(p.points) { + pt := identity ? p.points[i] : transform_point(m, p.points[i]) + r.x0 = min(r.x0, pt.x) + r.y0 = min(r.y0, pt.y) + r.x1 = max(r.x1, pt.x) + r.y1 = max(r.y1, pt.y) + } + return r +} + +// path_tight_bounds solves for the cubic's extrema instead of using the hull. +// Worth it for the clip stack, where a loose box costs whole scanlines. +path_tight_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect { + if p == nil || len(p.verbs) == 0 { return RECT_EMPTY } + + r := RECT_EMPTY + have := false + include :: proc(r: ^Rect, have: ^bool, pt: Point) { + if !have^ { + r^ = Rect{pt.x, pt.y, pt.x, pt.y} + have^ = true + return + } + r.x0 = min(r.x0, pt.x) + r.y0 = min(r.y0, pt.y) + r.x1 = max(r.x1, pt.x) + r.y1 = max(r.y1, pt.y) + } + + cur := Point{} + i := 0 + for verb in p.verbs { + switch verb { + case .Move, .Line: + if i >= len(p.points) { break } + pt := transform_point(m, p.points[i]) + include(&r, &have, pt) + cur = pt + i += 1 + case .Curve: + if i + 2 >= len(p.points) { break } + c1 := transform_point(m, p.points[i]) + c2 := transform_point(m, p.points[i + 1]) + e := transform_point(m, p.points[i + 2]) + include(&r, &have, e) + cubic_extrema_include(&r, &have, cur, c1, c2, e) + cur = e + i += 3 + case .Close: + // no points + } + } + return have ? r : RECT_EMPTY +} + +// cubic_extrema_include folds the interior extrema of one cubic into the box. +// The derivative of a cubic Bezier is a quadratic; its roots in (0,1) are the +// only places the curve can turn. +@(private) +cubic_extrema_include :: proc(r: ^Rect, have: ^bool, p0, p1, p2, p3: Point) { + for axis in 0 ..< 2 { + v0 := p0[axis] + v1 := p1[axis] + v2 := p2[axis] + v3 := p3[axis] + + a := -v0 + 3 * v1 - 3 * v2 + v3 + b := 2 * (v0 - 2 * v1 + v2) + c := v1 - v0 + + fold :: proc(r: ^Rect, have: ^bool, axis: int, value: f64) { + if !is_finite(value) { return } + if !have^ { + r^ = Rect{value, value, value, value} + have^ = true + return + } + if axis == 0 { + r.x0 = min(r.x0, value) + r.x1 = max(r.x1, value) + } else { + r.y0 = min(r.y0, value) + r.y1 = max(r.y1, value) + } + } + + eval :: proc(v0, v1, v2, v3, t: f64) -> f64 { + mt := 1 - t + return mt * mt * mt * v0 + 3 * mt * mt * t * v1 + 3 * mt * t * t * v2 + t * t * t * v3 + } + + // Degenerate to a linear derivative: solve b*t + c == 0. + if abs(a) < 1e-12 { + if abs(b) > 1e-12 { + t := -c / b + if t > 0 && t < 1 { + fold(r, have, axis, eval(v0, v1, v2, v3, t)) + } + } + continue + } + // 3*a*t^2 + 2*b'*t + 3*c == 0, with the constant factors cancelled. + disc := b * b - 4 * a * c + if disc < 0 { continue } + sq := math.sqrt(disc) + for t in ([2]f64{(-b + sq) / (2 * a), (-b - sq) / (2 * a)}) { + if t > 0 && t < 1 { + fold(r, have, axis, eval(v0, v1, v2, v3, t)) + } + } + } +} + +// --------------------------------------------------------------------------- +// Flattening +// --------------------------------------------------------------------------- + +// Flat_Subpath describes one polyline inside a Flat_Path. +Flat_Subpath :: struct { + start: int, // index into Flat_Path.points + count: int, + closed: bool, +} + +// Flat_Path is a path reduced to polylines, in device space. The rasteriser +// and the stroker both consume this rather than curves. +// +// Subpaths are described explicitly rather than by a start-index array with a +// sentinel, so that several paths can be flattened into one Flat_Path -- which +// is exactly what a clip stack and a tiling pattern need to do. +Flat_Path :: struct { + points: [dynamic]Point, + subpaths: [dynamic]Flat_Subpath, +} + +flat_path_make :: proc(allocator := context.allocator) -> Flat_Path { + return Flat_Path{ + points = make([dynamic]Point, 0, 64, allocator), + subpaths = make([dynamic]Flat_Subpath, 0, 8, allocator), + } +} + +flat_path_destroy :: proc(fp: ^Flat_Path) { + if fp == nil { return } + delete(fp.points) + delete(fp.subpaths) + fp^ = {} +} + +flat_path_clear :: proc(fp: ^Flat_Path) { + clear(&fp.points) + clear(&fp.subpaths) +} + +flat_path_count :: proc(fp: ^Flat_Path) -> int { + return len(fp.subpaths) +} + +flat_path_slice :: proc(fp: ^Flat_Path, i: int) -> []Point { + if i < 0 || i >= len(fp.subpaths) { return nil } + sp := fp.subpaths[i] + if sp.start < 0 || sp.start + sp.count > len(fp.points) { return nil } + return fp.points[sp.start:sp.start + sp.count] +} + +flat_path_bounds :: proc(fp: ^Flat_Path) -> Rect { + if fp == nil || len(fp.points) == 0 { return RECT_EMPTY } + r := Rect{fp.points[0].x, fp.points[0].y, fp.points[0].x, fp.points[0].y} + for pt in fp.points { + r.x0 = min(r.x0, pt.x) + r.y0 = min(r.y0, pt.y) + r.x1 = max(r.x1, pt.x) + r.y1 = max(r.y1, pt.y) + } + return r +} + +// FLATNESS is the maximum distance, in device pixels, between the true curve +// and the polyline that replaces it. A quarter of a pixel is below what +// anti-aliased output can show and keeps segment counts sane on text. +FLATNESS :: 0.25 + +// path_flatten transforms `p` by `m` and subdivides every curve into line +// segments, appending the result to `out`. Existing contents of `out` are kept. +// +// Subdivision is adaptive and depth-limited: a curve with a cusp or a very +// large control polygon would otherwise subdivide until the stack ran out. +// +// `cull`, when given, is the region -- in the same space `m` maps into -- that +// the result will actually be drawn or clipped through. A curve that cannot +// reach it is replaced by its chord instead of being subdivided, which is what +// keeps a path whose control points are a billion units off the page from +// costing a million points per painting operator. See `flatten_reaches`: the +// substitution is exact, not an approximation. +path_flatten :: proc(out: ^Flat_Path, p: ^Path, m: Matrix, flatness := FLATNESS, + cull: Maybe(Rect) = nil) { + if out == nil || p == nil || len(p.verbs) == 0 { return } + + tol := max(flatness, 0.01) + cull_box, culling := cull.? + cur := Point{} + i := 0 + // Index into out.subpaths of the subpath being built, or -1. + active := -1 + + for verb in p.verbs { + if len(out.points) >= MAX_FLATTEN_POINTS { break } + switch verb { + case .Move: + if i >= len(p.points) { break } + cur = transform_point(m, p.points[i]) + i += 1 + append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) + append(&out.points, cur) + active = len(out.subpaths) - 1 + case .Line: + if i >= len(p.points) { break } + pt := transform_point(m, p.points[i]) + i += 1 + if active < 0 { + // A segment after a close begins a new subpath AT the current + // point, which `h` left at the start of the subpath it closed + // (ISO 32000-2, 8.5.2.1). Seeding it is what keeps the + // connecting segment: without `cur` the new subpath is a + // single stray point, which a fill drops and a stroke turns + // into a dot in the wrong place. The .Curve arm below has + // always done this; the two must agree. + append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) + append(&out.points, cur) + active = len(out.subpaths) - 1 + } + append(&out.points, pt) + out.subpaths[active].count += 1 + cur = pt + case .Curve: + if i + 2 >= len(p.points) { break } + c1 := transform_point(m, p.points[i]) + c2 := transform_point(m, p.points[i + 1]) + e := transform_point(m, p.points[i + 2]) + i += 3 + if active < 0 { + append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1}) + append(&out.points, cur) + active = len(out.subpaths) - 1 + } + before := len(out.points) + flatten_cubic(&out.points, cur, c1, c2, e, tol, 0, cull_box, culling) + append(&out.points, e) + out.subpaths[active].count += len(out.points) - before + cur = e + case .Close: + if active >= 0 { + out.subpaths[active].closed = true + sp := out.subpaths[active] + if sp.count > 0 { + cur = out.points[sp.start] + } + } + // A subpath after a close starts fresh, but at the same point: + // "m 0 0 l 10 0 h l 20 0" continues from the start point. + active = -1 + } + } +} + +// MAX_FLATTEN_DEPTH bounds recursion at 2^16 segments per curve, which no +// legitimate curve approaches and a degenerate one would exceed forever. +MAX_FLATTEN_DEPTH :: 16 + +// MAX_FLATTEN_POINTS bounds the whole of one flattening. MAX_FLATTEN_DEPTH caps +// a SINGLE curve, and nothing capped the number of curves: a content stream may +// append hundreds of thousands of `c` segments to one path before the painting +// operator that clears it, and a cubic whose control points are a billion units +// off the chord subdivides to the depth limit every time. One `f` could +// therefore ask for gigabytes of polyline -- `rd_fill_path` materialises the +// whole flattened path before `raster_fill` ever sees the clip that would have +// discarded it. A million points is two orders of magnitude past the densest +// real path and costs 16 MB; past it the geometry is truncated, which draws +// part of a shape no viewer could have drawn all of. +MAX_FLATTEN_POINTS :: 1 << 20 + +// flatten_reaches reports whether any part of the cubic can lie inside `box`. +// +// Every point of a Bezier lies inside the convex hull of its control points, +// so a hull that misses the box means the whole curve does. It is used to stop +// subdividing there, which leaves the chord between the curve's ends in its +// place -- and that chord is inside the same hull. Deforming one to the other +// therefore never crosses a point of `box`, so the winding number, and with it +// both fill rules, is unchanged everywhere the box covers. The picture inside +// the clip is exactly the picture the full subdivision would have drawn. +// +// A non-finite coordinate makes every comparison false and the curve is kept, +// which is the same thing the un-culled path does with it. +@(private) +flatten_reaches :: proc(p0, p1, p2, p3: Point, box: Rect) -> bool { + if max(p0.x, p1.x, p2.x, p3.x) < box.x0 { return false } + if min(p0.x, p1.x, p2.x, p3.x) > box.x1 { return false } + if max(p0.y, p1.y, p2.y, p3.y) < box.y0 { return false } + if min(p0.y, p1.y, p2.y, p3.y) > box.y1 { return false } + return true +} + +@(private) +flatten_cubic :: proc(out: ^[dynamic]Point, p0, p1, p2, p3: Point, tol: f64, depth: int, + cull: Rect, culling: bool) { + if depth >= MAX_FLATTEN_DEPTH || len(out) >= MAX_FLATTEN_POINTS { + return + } + // Tested at every level, not only the first: the halves of a curve that + // sweeps across the page and back are each smaller than the whole, so a + // curve nothing culls at the top collapses to a handful of segments a few + // levels down. + if culling && !flatten_reaches(p0, p1, p2, p3, cull) { + return + } + // Flatness test: how far the control points sit off the chord. d1 and d2 + // are perpendicular distances scaled by the chord length, so comparing + // (d1+d2)^2 against tol^2 * chord^2 is the same as requiring the two + // perpendicular distances to sum to less than `tol` -- and the curve's own + // deviation from the chord is at most three quarters of that. + dx := p3.x - p0.x + dy := p3.y - p0.y + chord2 := dx * dx + dy * dy + + if chord2 < 1e-12 { + // A curve that comes back to its own start point has no chord to + // measure against, and the cross-product test would call it flat -- + // collapsing an entire loop into a zero-length segment. Measure the + // control points against the start point instead. + c1 := (p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y) + c2 := (p2.x - p0.x) * (p2.x - p0.x) + (p2.y - p0.y) * (p2.y - p0.y) + if max(c1, c2) <= tol * tol { + return + } + } else { + d1 := abs((p1.x - p3.x) * dy - (p1.y - p3.y) * dx) + d2 := abs((p2.x - p3.x) * dy - (p2.y - p3.y) * dx) + dd := d1 + d2 + if dd * dd <= tol * tol * chord2 { + return + } + } + + // de Casteljau split at t = 0.5. + p01 := Point{(p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5} + p12 := Point{(p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5} + p23 := Point{(p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5} + p012 := Point{(p01.x + p12.x) * 0.5, (p01.y + p12.y) * 0.5} + p123 := Point{(p12.x + p23.x) * 0.5, (p12.y + p23.y) * 0.5} + mid := Point{(p012.x + p123.x) * 0.5, (p012.y + p123.y) * 0.5} + + flatten_cubic(out, p0, p01, p012, mid, tol, depth + 1, cull, culling) + append(out, mid) + flatten_cubic(out, mid, p123, p23, p3, tol, depth + 1, cull, culling) +} diff --git a/path_test.odin b/path_test.odin new file mode 100644 index 0000000..5313969 --- /dev/null +++ b/path_test.odin @@ -0,0 +1,351 @@ +package pdf + +import "core:math" +import "core:testing" + +@(test) +test_path_rect_and_bounds :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + + path_rect(&p, 10, 20, 100, 50) + testing.expect_value(t, len(p.verbs), 5) // move, 3 lines, close + testing.expect_value(t, len(p.points), 4) + testing.expect_value(t, path_bounds(&p), Rect{10, 20, 110, 70}) + testing.expect_value(t, path_tight_bounds(&p), Rect{10, 20, 110, 70}) +} + +@(test) +test_path_drops_non_finite :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + + nan := math.nan_f64() + path_move_to(&p, nan, 0) + testing.expect_value(t, len(p.verbs), 0) + path_move_to(&p, 0, math.INF_F64) + testing.expect_value(t, len(p.verbs), 0) + path_curve_to(&p, 0, 0, nan, 1, 2, 3) + testing.expect_value(t, len(p.verbs), 0) + + path_move_to(&p, 1, 2) + testing.expect_value(t, len(p.verbs), 1) +} + +// "l" before any "m" starts a subpath rather than being dropped: content +// streams do this and viewers accept it. +@(test) +test_path_line_without_move :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + + path_line_to(&p, 5, 5) + testing.expect_value(t, len(p.verbs), 1) + testing.expect_value(t, p.verbs[0], Path_Verb.Move) +} + +// A cubic that bulges outside its endpoints must widen the tight box, and the +// tight box must never be larger than the control hull. +@(test) +test_path_tight_bounds_curve :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + + path_move_to(&p, 0, 0) + path_curve_to(&p, 0, 100, 100, 100, 100, 0) + + hull := path_bounds(&p) + tight := path_tight_bounds(&p) + + testing.expect_value(t, hull, Rect{0, 0, 100, 100}) + // Peak of this symmetric curve is at t=0.5: y = 3/4 * 100 = 75. + testing.expect(t, abs(tight.y1 - 75) < 1e-9, "curve peak should be 75") + testing.expect_value(t, tight.x0, 0) + testing.expect_value(t, tight.x1, 100) + testing.expect_value(t, tight.y0, 0) +} + +@(test) +test_path_transform :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + + path_rect(&p, 0, 0, 10, 10) + path_transform(&p, matrix_mul(scale_matrix(2, 3), translate_matrix(5, 7))) + testing.expect_value(t, path_bounds(&p), Rect{5, 7, 25, 37}) +} + +@(test) +test_flatten_line_is_exact :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + fp := flat_path_make() + defer flat_path_destroy(&fp) + + path_move_to(&p, 0, 0) + path_line_to(&p, 10, 0) + path_line_to(&p, 10, 10) + path_close(&p) + + path_flatten(&fp, &p, MATRIX_IDENTITY) + testing.expect_value(t, flat_path_count(&fp), 1) + pts := flat_path_slice(&fp, 0) + testing.expect_value(t, len(pts), 3) + testing.expect_value(t, pts[0], Point{0, 0}) + testing.expect_value(t, pts[2], Point{10, 10}) + testing.expect(t, fp.subpaths[0].closed, "subpath should be closed") +} + +// Flattening twice into one Flat_Path must produce two independent subpaths -- +// the clip stack and tiling patterns both rely on it. +@(test) +test_flatten_accumulates :: proc(t: ^testing.T) { + a := path_make(); defer path_destroy(&a) + b := path_make(); defer path_destroy(&b) + fp := flat_path_make(); defer flat_path_destroy(&fp) + + path_rect(&a, 0, 0, 10, 10) + path_rect(&b, 100, 100, 10, 10) + path_flatten(&fp, &a, MATRIX_IDENTITY) + path_flatten(&fp, &b, MATRIX_IDENTITY) + + testing.expect_value(t, flat_path_count(&fp), 2) + testing.expect_value(t, len(flat_path_slice(&fp, 0)), 4) + testing.expect_value(t, len(flat_path_slice(&fp, 1)), 4) + testing.expect_value(t, flat_path_slice(&fp, 1)[0], Point{100, 100}) +} + +// Every flattened point must lie within the flatness tolerance of the true +// curve, and the segment count must stay sane. +@(test) +test_flatten_curve_tolerance :: proc(t: ^testing.T) { + p := path_make(); defer path_destroy(&p) + fp := flat_path_make(); defer flat_path_destroy(&fp) + + path_move_to(&p, 0, 0) + path_curve_to(&p, 0, 100, 100, 100, 100, 0) + path_flatten(&fp, &p, MATRIX_IDENTITY) + + pts := flat_path_slice(&fp, 0) + testing.expect(t, len(pts) > 4, "curve should subdivide") + testing.expect(t, len(pts) < 400, "curve should not over-subdivide") + testing.expect_value(t, pts[0], Point{0, 0}) + testing.expect_value(t, pts[len(pts) - 1], Point{100, 0}) + + // Every sampled point of the true curve must lie close to the polyline. + // Measure to the nearest *segment*, not the nearest vertex: with coarse + // subdivision a sample halfway along a segment is far from both of its + // endpoints while the polyline itself is still accurate. + eval :: proc(t_: f64) -> Point { + mt := 1 - t_ + x := 3 * mt * t_ * t_ * 100 + t_ * t_ * t_ * 100 + y := 3 * mt * mt * t_ * 100 + 3 * mt * t_ * t_ * 100 + return Point{x, y} + } + dist_to_segment :: proc(p, a, b: Point) -> f64 { + vx := b.x - a.x + vy := b.y - a.y + len2 := vx * vx + vy * vy + u := len2 == 0 ? 0 : ((p.x - a.x) * vx + (p.y - a.y) * vy) / len2 + u = clamp(u, 0, 1) + dx := p.x - (a.x + u * vx) + dy := p.y - (a.y + u * vy) + return math.sqrt(dx * dx + dy * dy) + } + worst: f64 = 0 + for i in 0 ..= 200 { + tv := f64(i) / 200 + want := eval(tv) + best: f64 = 1e30 + for j in 0 ..< len(pts) - 1 { + best = min(best, dist_to_segment(want, pts[j], pts[j + 1])) + } + worst = max(worst, best) + } + testing.expect(t, worst < 1.0, "flattened polyline should stay within a pixel of the curve") +} + +// A degenerate curve (all control points equal) must terminate immediately. +@(test) +test_flatten_degenerate_terminates :: proc(t: ^testing.T) { + p := path_make(); defer path_destroy(&p) + fp := flat_path_make(); defer flat_path_destroy(&fp) + + path_move_to(&p, 5, 5) + path_curve_to(&p, 5, 5, 5, 5, 5, 5) + path_flatten(&fp, &p, MATRIX_IDENTITY) + testing.expect(t, len(fp.points) < 8, "degenerate curve must not subdivide") +} + +@(test) +test_flatten_empty_and_nil :: proc(t: ^testing.T) { + fp := flat_path_make(); defer flat_path_destroy(&fp) + p := path_make(); defer path_destroy(&p) + + path_flatten(&fp, &p, MATRIX_IDENTITY) + testing.expect_value(t, flat_path_count(&fp), 0) + testing.expect_value(t, len(flat_path_slice(&fp, 0)), 0) + testing.expect_value(t, len(flat_path_slice(&fp, -1)), 0) +} + +// A curve whose endpoints coincide encloses a loop. The chord is zero length, +// so the ordinary flatness test would declare it flat and throw the loop away. +@(test) +test_flatten_closed_loop_curve :: proc(t: ^testing.T) { + p := path_make(); defer path_destroy(&p) + fp := flat_path_make(); defer flat_path_destroy(&fp) + + path_move_to(&p, 0, 0) + path_curve_to(&p, 100, 0, 100, 100, 0, 0) + path_flatten(&fp, &p, MATRIX_IDENTITY) + + b := flat_path_bounds(&fp) + testing.expect(t, rect_width(b) > 40, "loop must not collapse in x") + testing.expect(t, rect_height(b) > 15, "loop must not collapse in y") + testing.expect(t, len(fp.points) > 8, "loop must subdivide") +} + +// A segment after `h` begins a new subpath AT the current point, which the +// close left at the start of the subpath it closed (ISO 32000-2, 8.5.2.1) -- +// "even if the new segment begins at the endpoint reached by the h operation". +// Dropping that point loses the connecting segment entirely and leaves a +// one-point subpath, which a fill ignores and a stroke draws as a stray dot. +// The Line and Curve arms of path_flatten must agree about this. +@(test) +test_flatten_segment_after_close_keeps_current_point :: proc(t: ^testing.T) { + // "m 0 0 l 10 0 h l 20 0" + line := path_make(); defer path_destroy(&line) + path_move_to(&line, 0, 0) + path_line_to(&line, 10, 0) + path_close(&line) + path_line_to(&line, 20, 0) + + lf := flat_path_make(); defer flat_path_destroy(&lf) + path_flatten(&lf, &line, MATRIX_IDENTITY) + testing.expect_value(t, flat_path_count(&lf), 2) + second := flat_path_slice(&lf, 1) + testing.expect_value(t, len(second), 2) + testing.expect_value(t, second[0], Point{0, 0}) + testing.expect_value(t, second[1], Point{20, 0}) + + // The same shape written with a curve has always behaved this way, and is + // what the line case is being held to. + curve := path_make(); defer path_destroy(&curve) + path_move_to(&curve, 0, 0) + path_line_to(&curve, 10, 0) + path_close(&curve) + path_curve_to(&curve, 20, 0, 20, 0, 20, 0) + + cf := flat_path_make(); defer flat_path_destroy(&cf) + path_flatten(&cf, &curve, MATRIX_IDENTITY) + testing.expect_value(t, flat_path_count(&cf), 2) + testing.expect_value(t, flat_path_slice(&cf, 1)[0], Point{0, 0}) +} + +// --------------------------------------------------------------------------- +// Flattening what the clip can reach, and only that +// --------------------------------------------------------------------------- + +// PF_BOMB is a cubic whose control points are ten trillion units away. It is +// not exotic: it is what an attacker writes, and what a producer emits when a +// transform has gone wrong. The flatness test never gives up on it, so it +// subdivides to MAX_FLATTEN_DEPTH and yields 65 537 points -- a megabyte of +// polyline for one `c`, none of which can be seen. +@(private = "file") +pf_bomb :: proc(p: ^Path, n: int) { + path_move_to(p, 0, 0) + for _ in 0 ..< n { + path_curve_to(p, -4.98e13, 5.61e13, 7.23e13, 9.65e13, -7.82e13, 9.74e13) + } +} + +@(test) +test_path_flatten_culls_what_the_clip_cannot_reach :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + pf_bomb(&p, 16) + + fp := flat_path_make() + defer flat_path_destroy(&fp) + path_flatten(&fp, &p, MATRIX_IDENTITY) + // Sixteen of them exhaust the whole-flattening cap, which is the only + // thing that stood between this and 16 MB. + testing.expectf(t, len(fp.points) >= MAX_FLATTEN_POINTS, + "the uncalled flattening should reach the cap, got %d points", len(fp.points)) + + flat_path_clear(&fp) + path_flatten(&fp, &p, MATRIX_IDENTITY, FLATNESS, Rect{-1, -1, 613, 793}) + testing.expectf(t, len(fp.points) <= 64, + "a page-sized clip reaches none of it: %d points", len(fp.points)) +} + +// The substitution has to be invisible. A shape that crosses the clip is +// flattened with and without the cull and both are rasterised: the coverage +// inside the clip must agree to the byte, because the chord that replaces an +// unreachable curve lies in the same convex hull the curve did and cannot +// change the winding number of anything the clip covers. +@(private = "file") +Pf_Cov :: struct { + w, h: int, + buf: []u8, +} + +@(private = "file") +pf_span :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Pf_Cov)(user) + if y < 0 || y >= c.h { return } + for i in 0 ..< len(coverage) { + x := x0 + i + if x < 0 || x >= c.w { continue } + c.buf[y * c.w + x] = coverage[i] + } +} + +@(private = "file") +pf_coverage :: proc(t: ^testing.T, p: ^Path, cull: Maybe(Rect), even_odd: bool) -> []u8 { + W :: 120 + H :: 90 + fp := flat_path_make() + defer flat_path_destroy(&fp) + path_flatten(&fp, p, MATRIX_IDENTITY, FLATNESS, cull) + cov := Pf_Cov{w = W, h = H, buf = make([]u8, W * H)} + err := raster_fill(&fp, even_odd, Raster_Clip{0, 0, W, H}, pf_span, &cov) + testing.expect_value(t, err, Error.None) + return cov.buf +} + +@(test) +test_path_flatten_cull_draws_the_same_picture :: proc(t: ^testing.T) { + // Three curves that leave the clip and come back, one that never comes + // near it, and one wholly inside: the culled and the full flattening have + // to agree on every pixel. + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 20, 20) + path_curve_to(&p, 1e6, -1e6, -1e6, 1e6, 100, 70) + path_curve_to(&p, -4.98e13, 5.61e13, 7.23e13, 9.65e13, 60, 10) + path_curve_to(&p, 5e5, 5e5, 6e5, 5e5, 90, 80) + path_curve_to(&p, 30, 85, 25, 40, 20, 20) + path_close(&p) + + for even_odd in ([2]bool{false, true}) { + full := pf_coverage(t, &p, nil, even_odd) + defer delete(full) + culled := pf_coverage(t, &p, Rect{-1, -1, 121, 91}, even_odd) + defer delete(culled) + + ink := 0 + diff := 0 + first := -1 + for i in 0 ..< len(full) { + if full[i] != 0 { ink += 1 } + if full[i] != culled[i] { + diff += 1 + if first < 0 { first = i } + } + } + testing.expect(t, ink > 1000, "the shape has to actually cover the clip") + testing.expectf(t, diff == 0, "even_odd=%v: %d pixels differ, first at (%d,%d)", + even_odd, diff, first % 120, first / 120) + } +} diff --git a/pattern.odin b/pattern.odin new file mode 100644 index 0000000..be138d2 --- /dev/null +++ b/pattern.odin @@ -0,0 +1,506 @@ +package pdf + +import "core:math" + +// Patterns (ISO 32000-2, 8.7.3 and 8.7.4). +// +// PATTERN SPACE IS ANCHORED TO THE PAGE, NOT TO THE FILL +// +// A pattern's /Matrix maps pattern space to the DEFAULT coordinate space of +// the page -- or of the form XObject, Type 3 glyph or annotation appearance +// stream -- in which the pattern is a resource (8.7.3.1). It does NOT compose +// with the CTM that happens to be in force when the fill runs. So the mapping +// every procedure in this file builds is +// +// pattern space --(/Matrix)--> default space --(base CTM)--> device +// +// and `ctm` throughout this file means that base CTM: the one the interpreter +// started the content stream with, saved before the first `cm`, never +// `gs.ctm`. +// +// Two fills with the same pattern under different CTMs must therefore land on +// exactly the same lattice. A patterned rectangle drawn twice -- once plainly, +// once inside `q 2 0 0 2 30 40 cm ... Q` -- shows the same hatch, at the same +// phase, in the same place, and only the shape being filled moves. Composing +// with the live CTM instead is the classic pattern bug: the texture slides +// with whatever transform preceded the fill. It looks plausible in isolation +// and is obvious the moment two shapes with different CTMs share an edge, or +// the moment the page is rendered at a second zoom level. +// +// One consequence is worth stating because the interpreter has to act on it: a +// form XObject re-anchors pattern space. A pattern named from a form's +// /Resources is anchored to the form's default space -- the CTM at the moment +// the form was invoked, including the form's own /Matrix -- so entering a form +// means saving a new base CTM, and leaving it means restoring the old one. +// The same goes for a Type 3 glyph and an annotation appearance stream. +// +// WHAT THE INTERPRETER STILL HAS TO DO +// +// `pattern_tiles` says where to replay a tiling cell, not how. For each matrix +// it returns, the interpreter sets the CTM to that matrix, clips to the +// pattern's /BBox (a rotated pattern makes that a path, not a scissor +// rectangle), and runs `content` against `resources`. For an uncoloured +// pattern -- `pattern_needs_color` -- every colour operator inside the cell is +// ignored and the whole cell paints in the colour supplied with the pattern +// name. Recursion into a pattern that fills with a pattern is the +// interpreter's to bound. + +Pattern_Kind :: enum u8 { + Tiling = 1, + Shading = 2, +} + +Paint_Type :: enum u8 { + Coloured = 1, // the cell supplies its own colour + Uncoloured = 2, // the cell is a stencil painted in the caller's colour +} + +Tiling_Type :: enum u8 { + Constant_Spacing = 1, + No_Distortion = 2, + Faster_Constant_Spacing = 3, +} + +Pattern :: struct { + kind: Pattern_Kind, + // Pattern space to the default space of the page or form the pattern is + // used in -- NOT to the CTM in force when it is painted (8.7.3.1). Getting + // this wrong makes patterns slide around under a transformed fill. + mtx: Matrix, + + // Tiling. + paint_type: Paint_Type, + tiling_type: Tiling_Type, + bbox: Rect, + xstep: f64, + ystep: f64, + resources: ^Dict, + content: ^Stream, + + // Shading. + shading: ^Shading, + ext_gstate: ^Dict, + + // The Document this was read from. A lattice can only be found to be + // pathological once the area to cover is known, which is inside + // `pattern_tiles`, and the repair that records it has to be attached to + // something. + doc: ^Document, + // Set once the tile cap has been reported for this pattern. A page that + // fills with the same hostile pattern five hundred times should cost one + // repair, not five hundred crowding every other one out of doc.repairs. + cap_reported: bool, +} + +// --------------------------------------------------------------------------- +// Limits +// +// Each of these bounds a number that came out of a file, on a path any file +// can reach: `/Pattern cs /P1 scn ... f` is three operators. +// --------------------------------------------------------------------------- + +// Cells one call may ask the interpreter to replay. A full page at 300 dpi is +// 2550 x 3300 device pixels, so even a 16-pixel cell covering all of it is +// about 33 000 -- this is twice that, and everything past it is a file whose +// /XStep is small enough to make one fill cost a million content streams. +@(private) +MAX_PATTERN_TILES :: 1 << 16 + +// How many lattice steps a cell may span before the step is treated as +// degenerate rather than as overlapping cells. Cells that overlap are a real +// design -- a brick bond with a bleed, a hatch drawn with a fat pen -- so a +// generous factor is allowed before the step is snapped back to the cell size. +@(private) +MAX_TILE_OVERLAP :: 64.0 + +// Bound on a lattice index before it is narrowed to an int. The quotients that +// produce the index range are computed from file-derived numbers and can be +// 1e300; converting that to an int is undefined, so the range is clamped in +// f64 first. 2^24 is exact in f64 and is already 16 million cells along one +// axis, far past what the tile cap will let through. +@(private) +MAX_TILE_INDEX :: 1 << 24 + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// pattern_load builds the pattern `obj` describes: a stream for a tiling +// pattern (/PatternType 1), whose dictionary carries the pattern entries and +// whose bytes are the cell's content, or a dictionary for a shading pattern +// (/PatternType 2). +// +// `res` is the /Resources dictionary the pattern was named from. It resolves a +// colour space named by a bare name inside a shading pattern's /Shading, and +// it stands in for a tiling pattern that omits its own /Resources. +// +// The result is owned by the Document and must not be freed. Nothing is +// cached: like a shading, a pattern is built once per `scn` that names it, and +// the content stream it points at is decoded by `stream_data`, which caches. +pattern_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (^Pattern, Error) { + if doc == nil { + return nil, .Invalid_Argument + } + o := resolve(doc, obj) + d, has_dict := to_dict(o) + if !has_dict { + return nil, .Bad_Pattern + } + stm, is_stream := to_stream(o) + + kind := dict_int(doc, d, "PatternType", 0) + if kind != 1 && kind != 2 { + // /PatternType is required, but a file that leaves it out still says + // plainly which kind it meant: only a tiling pattern is a stream, and + // only a shading pattern has a /Shading. + switch { + case dict_has(d, "Shading"): kind = 2 + case is_stream: kind = 1 + case: return nil, .Bad_Pattern + } + add_repair(doc, .Bad_Resource, 0) + } + + p := Pattern{doc = doc} + // `object_to_matrix` is the finiteness boundary for a matrix out of a + // file, so a /Matrix that is short, non-numeric or infinite arrives here + // as the identity -- which is the format's own default and the only + // sensible guess. + p.mtx = dict_matrix(doc, d, "Matrix") + + if kind == 2 { + p.kind = .Shading + sh, sh_err := shading_load(doc, dict_get(doc, d, "Shading"), res) + // A shading pattern is nothing but its shading; with no shading to + // paint there is no degraded version of it to hand back. + if sh_err != .None || sh == nil { + return nil, .Bad_Pattern + } + p.shading = sh + // /ExtGState is applied by the caller: it belongs to the graphics + // state the shading paints under, not to the shading itself, and it + // can carry a soft mask that only the interpreter can build. + p.ext_gstate = dict_dict(doc, d, "ExtGState") + return pattern_intern(doc, p), .None + } + + p.kind = .Tiling + if !is_stream { + // The cell's content is the stream's own bytes. A tiling pattern + // written as a bare dictionary has no content at all, and an empty + // cell tiled across a fill is indistinguishable from not filling. + return nil, .Bad_Pattern + } + p.content = stm + + bbox, bbox_ok := object_to_rect(doc, dict_get(doc, d, "BBox")) + // Both corners are finite by the time `object_to_rect` returns, but their + // difference need not be: a /BBox of [-1e308 -1e308 1e308 1e308] has an + // infinite width, and a step of infinity multiplied by a lattice index of + // zero is a NaN in the middle of a cell matrix. Nothing below the Device + // line may see one, and a cell the size of the number line is not a cell. + bw := rect_width(bbox) + bh := rect_height(bbox) + if !bbox_ok || rect_is_empty(bbox) || !is_finite(bw) || !is_finite(bh) { + // 8.7.3.1: the cell is clipped to /BBox, so a missing or degenerate + // one clips every cell away to nothing. + return nil, .Bad_Pattern + } + p.bbox = bbox + // After this the steps are finite and strictly positive, which is the + // invariant `pattern_bounds` and `pattern_tiles` are written against. + p.xstep = pattern_step(doc, dict_num(doc, d, "XStep"), bw) + p.ystep = pattern_step(doc, dict_num(doc, d, "YStep"), bh) + + // Both are required entries, and an out-of-range value is a broken file + // rather than a new feature; the defaults are what a viewer shows. + p.paint_type = dict_int(doc, d, "PaintType", 1) == 2 ? .Uncoloured : .Coloured + switch dict_int(doc, d, "TilingType", 1) { + case 2: p.tiling_type = .No_Distortion + case 3: p.tiling_type = .Faster_Constant_Spacing + case: p.tiling_type = .Constant_Spacing + } + + p.resources = dict_dict(doc, d, "Resources") + if p.resources == nil { + // /Resources is required (8.7.3.3), but a cell that only draws + // rectangles needs nothing out of it, so its absence is not on its own + // a defect worth recording. Falling back to the resources of the + // stream that named the pattern is what every viewer does, and it is + // the only other dictionary in scope; a name that still does not + // resolve is reported by the interpreter that looks it up. + p.resources = res + } + return pattern_intern(doc, p), .None +} + +// pattern_intern copies a finished pattern into the Document arena. Patterns +// are built on the stack first so that a file that fails half way through +// validation costs no arena at all -- an arena is never reclaimed before +// `close`, and a page can name a broken pattern in every one of its fills. +@(private) +pattern_intern :: proc(doc: ^Document, p: Pattern) -> ^Pattern { + out := new(Pattern, doc.allocator) + out^ = p + return out +} + +// pattern_step normalises /XStep or /YStep against the size of the cell. +// +// Only the magnitude spaces the lattice, so a negative step is read as its +// absolute value rather than rejected. Zero is forbidden by the format and +// occurs anyway; it would stack every cell of an infinite row at one place, so +// it falls back to the cell size, which is what the step means when cells +// abut. +// +// A step far smaller than the cell is the same denial of service written +// differently: at a thousandth of the cell width every device pixel is covered +// by a thousand cells, and one fill costs a thousand times what it looks like. +// Overlapping cells are a legitimate design, so an overlap of up to +// MAX_TILE_OVERLAP-fold is honoured before the step is snapped to the cell. +@(private) +pattern_step :: proc(doc: ^Document, step, cell: f64) -> f64 { + s := abs(step) + if !is_finite(s) || s <= 0 || s * MAX_TILE_OVERLAP < cell { + add_repair(doc, .Bad_Resource, 0) + return cell + } + return s +} + +// --------------------------------------------------------------------------- +// The one composition every caller needs +// --------------------------------------------------------------------------- + +// pattern_matrix maps the pattern's own space to device space. +// +// `ctm` is the DEFAULT-space CTM of the page or form the pattern is a resource +// of, not the CTM of the fill being painted -- see the note at the top of this +// file. This is the only place the two are composed, so an interpreter that +// routes everything through here cannot get 8.7.3.1 wrong in one operator and +// right in another. +@(private) +pattern_matrix :: proc(p: ^Pattern, ctm: Matrix) -> Matrix { + if p == nil { + return ctm + } + return matrix_mul(p.mtx, ctm) +} + +// pattern_needs_color reports whether the caller must supply the colour: an +// uncoloured tiling pattern (/PaintType 2) is a stencil, and every colour +// operator inside its cell is ignored in favour of the components given +// alongside the pattern name in `scn` (8.7.3.1). Those components live in the +// underlying colour space of the /Pattern space, which is what +// `Colorspace.under` holds. +@(private) +pattern_needs_color :: proc(p: ^Pattern) -> bool { + return p != nil && p.kind == .Tiling && p.paint_type == .Uncoloured +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +// pattern_bounds is the device-space region the pattern can paint under the +// default-space `ctm`. +// +// A tiling pattern repeats over the whole plane, so the honest answer is +// RECT_INFINITE, the identity for `rect_intersect`: a caller that intersects +// it with the clip gets the clip. A shading pattern is bounded exactly when +// its shading is -- an un-extended radial one is, an axial one is not. +// +// A transform that collapses the plane to a line or a point paints nothing at +// all, and that is RECT_EMPTY rather than RECT_INFINITE, because the two are +// opposite identities and a caller intersecting with the wrong one would fill +// the whole clip with a pattern that has no area. +pattern_bounds :: proc(p: ^Pattern, ctm: Matrix) -> Rect { + if p == nil || !matrix_is_finite(ctm) { + return RECT_EMPTY + } + m := pattern_matrix(p, ctm) + if !matrix_is_finite(m) { + return RECT_EMPTY + } + if _, ok := matrix_invert(m); !ok { + return RECT_EMPTY + } + + switch p.kind { + case .Shading: + // `shading_bounds` answers RECT_EMPTY for a nil shading, which is the + // right answer and is not worth a second guard here. + return shading_bounds(p.shading, m) + case .Tiling: + // The lattice is infinite in both directions: /XStep and /YStep are + // finite and non-zero after loading, so there is always another cell. + return RECT_INFINITE + } + return RECT_EMPTY +} + +// --------------------------------------------------------------------------- +// Tiling +// --------------------------------------------------------------------------- + +// pattern_tiles returns the matrices at which the interpreter must replay a +// tiling pattern's content stream to cover `area`. +// +// Each matrix maps the cell's own space -- the space /BBox is written in -- to +// device space, and already carries the pattern matrix and `ctm`, so the +// interpreter sets the CTM to it, clips to /BBox, and runs the content. Cell +// (i, j) of the lattice is the cell translated by (i * /XStep, j * /YStep) in +// pattern space, so the returned matrices differ only in that translation and +// share the pattern's rotation and scale exactly; the cells cannot drift apart +// at the far end of a long fill. +// +// area the device-space region to cover, normally the fill's bounding box +// intersected with the clip. It must be finite: RECT_INFINITE, an +// empty rectangle and a NaN one all mean nothing to draw, because +// "cover an unbounded area with a repeating cell" has no answer. +// ctm the DEFAULT-space CTM of the page or form -- see the top of this +// file. It is emphatically not the CTM of the fill. +// +// An empty slice means nothing to draw, which is also what a shading pattern +// always gets: it paints in one pass rather than as cells. The result is +// allocated with `allocator` and belongs to the caller. +// +// A cell whose /BBox merely touches the edge of `area` is included. It is +// clipped away to a hairline at worst, and being inclusive is what keeps the +// common case -- a lattice exactly aligned with the area, where the quotients +// land on integers -- from dropping a whole row to a rounding error. +pattern_tiles :: proc(p: ^Pattern, area: Rect, ctm: Matrix, + allocator := context.allocator) -> []Matrix { + if p == nil || p.kind != .Tiling || p.content == nil { + return nil + } + // RECT_INFINITE has to be caught by identity, not by is_finite: its + // corners are 1e30, which is a perfectly finite number, and + // `transform_rect` passes it through unchanged. + if area == RECT_INFINITE || !rect_is_finite(area) || rect_is_empty(area) { + return nil + } + if !matrix_is_finite(ctm) || p.xstep <= 0 || p.ystep <= 0 { + return nil + } + + m := pattern_matrix(p, ctm) + if !matrix_is_finite(m) { + return nil + } + inv, invertible := matrix_invert(m) + if !invertible { + // A singular pattern matrix collapses every cell to zero area. + return nil + } + + // The lattice is regular in pattern space and only there, so the area is + // taken back into pattern space and the indices are read off directly. For + // a rotated or skewed pattern the pulled-back rectangle is the bounding + // box of a rotated one, so a few cells in the corners fall outside the + // area and are clipped away; that costs a little work and never misses a + // cell, which is the right way round. + pa := transform_rect(inv, area) + if !rect_is_finite(pa) { + return nil + } + + // Cell (i, j) spans [bbox.x0 + i*xs, bbox.x1 + i*xs] across, so it meets + // the area when bbox.x0 + i*xs <= pa.x1 and bbox.x1 + i*xs >= pa.x0. + i0, i1, ok_x := tile_range(pa.x0, pa.x1, p.bbox.x0, p.bbox.x1, p.xstep) + j0, j1, ok_y := tile_range(pa.y0, pa.y1, p.bbox.y0, p.bbox.y1, p.ystep) + if !ok_x || !ok_y { + return nil + } + + nx := i1 - i0 + 1 + ny := j1 - j0 + 1 + if nx <= 0 || ny <= 0 { + return nil + } + // In f64 because the product of two clamped index counts overflows no f64 + // but would need care in an int. + total := f64(nx) * f64(ny) + n := nx * ny + if total > f64(MAX_PATTERN_TILES) { + // Painting fewer cells than were asked for leaves part of the fill + // bare, which is the least bad of the three options: the whole fill + // dropped, a lattice respaced to fit (which would show gaps at the + // wrong pitch and look like a rendering bug rather than a damaged + // file), or an unbounded amount of work. Row-major truncation keeps + // the painted part contiguous. + n = MAX_PATTERN_TILES + if !p.cap_reported { + p.cap_reported = true + add_repair(p.doc, .Bad_Resource, 0) + } + } + + out, alloc_err := make([]Matrix, n, allocator) + if alloc_err != nil { + return nil + } + k := 0 + rows: for j := j0; j <= j1; j += 1 { + ty := f64(j) * p.ystep + for i := i0; i <= i1; i += 1 { + if k >= n { + break rows + } + out[k] = matrix_mul(translate_matrix(f64(i) * p.xstep, ty), m) + k += 1 + } + } + return out +} + +// tile_range is the inclusive range of lattice indices whose cell meets +// [lo, hi], for a cell spanning [c0, c1] and a step of `step`. +// +// The quotients are formed from numbers that came out of a file and can be +// enormous, so they are clamped while they are still f64: `int(1e300)` is +// undefined, and the clamp is what makes the tile cap the only thing standing +// between a hostile file and an allocation. +@(private) +tile_range :: proc(lo, hi, c0, c1, step: f64) -> (from, to: int, ok: bool) { + if step <= 0 || !is_finite(step) { + return 0, 0, false + } + a := math.ceil((lo - c1) / step) + b := math.floor((hi - c0) / step) + if !is_finite(a) || !is_finite(b) { + return 0, 0, false + } + a = clamp(a, -MAX_TILE_INDEX, MAX_TILE_INDEX) + b = clamp(b, -MAX_TILE_INDEX, MAX_TILE_INDEX) + if b < a { + return 0, 0, false + } + return int(a), int(b), true +} + +// --------------------------------------------------------------------------- +// Painting a shading pattern +// --------------------------------------------------------------------------- + +// pattern_paint_shading composites a shading pattern's shading onto `bmp` +// through `clip`, which the caller has already narrowed to the shape being +// filled. +// +// This exists so that the pattern matrix is composed in exactly one place: +// everything else about the call is `shading_paint`'s, including that `bmp` is +// premultiplied and that `mask` is an optional soft mask aligned with it in +// device pixels. +// +// `ctm` is the default-space CTM, not the fill's. The pattern's /ExtGState is +// the caller's to apply -- `alpha`, `mode` and `mask` are where its effect +// arrives here. +@(private) +pattern_paint_shading :: proc(doc: ^Document, p: ^Pattern, bmp: ^Bitmap, ctm: Matrix, + clip: Raster_Clip, alpha: f64, mode: Blend_Mode, + mask: ^Bitmap) -> Error { + if p == nil || p.kind != .Shading || p.shading == nil { + return .Invalid_Argument + } + return shading_paint(doc, p.shading, bmp, pattern_matrix(p, ctm), clip, alpha, mode, mask) +} diff --git a/pattern_test.odin b/pattern_test.odin new file mode 100644 index 0000000..f8ce2f5 --- /dev/null +++ b/pattern_test.odin @@ -0,0 +1,800 @@ +package pdf + +import "core:math" +import "core:mem" +import "core:testing" + +// Pattern tests. +// +// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` -- live in +// function_test.odin. That document is opened by scanning (it has no xref), so +// it already carries repairs; every assertion about a repair here counts the +// growth of `doc.repairs` rather than its length. +// +// Almost everything below is about one question: where does a cell land? So +// the assertions are about device-space positions of returned matrices, not +// about pixels. `pt_origin` is the whole vocabulary -- a tile matrix maps the +// cell's own space to device space, so the image of (0,0) is where that cell's +// bbox origin sits on the page. + +@(private = "file") +PT_EPS :: 1e-9 + +// A 10x10 cell that abuts its neighbours, offset in pattern space so that a +// composition done in the wrong order lands somewhere else. +@(private = "file") +TILE_SRC :: "<< /PatternType 1 /PaintType 1 /TilingType 1 " + + "/BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [1 0 0 1 5 7] >>" + +// The same cell anchored at the origin, for counting. +@(private = "file") +PLAIN_SRC :: "<< /PatternType 1 /PaintType 1 /TilingType 1 " + + "/BBox [0 0 10 10] /XStep 10 /YStep 10 >>" + +@(private = "file") +AXIAL :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " + + "/Extend [true true] >>" + +@(private = "file") +pt_tiling :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil, + loc := #caller_location) -> ^Pattern { + p, err := pattern_load(doc, cf_stream(doc, src, nil), res) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expect(t, p != nil, "pattern did not load", loc = loc) + return p +} + +@(private = "file") +pt_dict_pattern :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil, + loc := #caller_location) -> ^Pattern { + p, err := pattern_load(doc, cf_parse(doc, src), res) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expect(t, p != nil, "pattern did not load", loc = loc) + return p +} + +// pt_origin is where cell-space (0,0) -- the corner of /BBox -- lands. +@(private = "file") +pt_origin :: proc(m: Matrix) -> Point { + return transform_point(m, Point{0, 0}) +} + +@(private = "file") +pt_near :: proc(a, b: Point) -> bool { + return abs(a.x - b.x) < 1e-6 && abs(a.y - b.y) < 1e-6 +} + +@(private = "file") +pt_has_origin :: proc(tiles: []Matrix, want: Point) -> int { + n := 0 + for m in tiles { + if pt_near(pt_origin(m), want) { + n += 1 + } + } + return n +} + +// pt_covers reports whether some returned cell contains the device point `q`. +// The cell is the pattern's /BBox in that tile's own space, which is exactly +// what the interpreter clips the replayed content to. +@(private = "file") +pt_covers :: proc(p: ^Pattern, tiles: []Matrix, q: Point) -> bool { + for m in tiles { + inv, ok := matrix_invert(m) + if !ok { + continue + } + if rect_contains(p.bbox, transform_point(inv, q)) { + return true + } + } + return false +} + +// pt_covers_area checks a grid of sample points across `area`. Under-counting +// the lattice -- an off-by-one in the index range, a step read as the cell +// size when it is not -- leaves a stripe of the fill bare, and that is what +// this catches. +@(private = "file") +expect_covers_area :: proc(t: ^testing.T, p: ^Pattern, tiles: []Matrix, area: Rect, + steps: int, what: string, loc := #caller_location) { + for iy in 0 ..= steps { + for ix in 0 ..= steps { + q := Point{ + area.x0 + (area.x1 - area.x0) * f64(ix) / f64(steps), + area.y0 + (area.y1 - area.y0) * f64(iy) / f64(steps), + } + if !pt_covers(p, tiles, q) { + testing.expectf(t, false, "%s: device point (%f,%f) is covered by no cell", + what, q.x, q.y, loc = loc) + return + } + } + } +} + +// --------------------------------------------------------------------------- +// The headline: /Matrix is anchored to the page, not to the fill +// --------------------------------------------------------------------------- + +// A pattern's /Matrix maps pattern space to the page's DEFAULT space +// (8.7.3.1). The transform in force when the fill runs must not move the +// lattice: the same pattern used inside `q 2 0 0 2 30 40 cm ... Q` puts its +// cells exactly where it puts them outside. +// +// The test pins two things a wrong implementation gets wrong in different +// ways: the device position of the cell that holds pattern-space (0,0), which +// catches composing in the wrong order, and the equality of the whole lattice +// across two different fill CTMs, which catches composing with the live CTM at +// all. The third assertion is the control -- feeding the fill's CTM in as if +// it were the base does move the lattice -- so the second is known to be +// testing something. +@(test) +test_pattern_matrix_is_anchored_to_default_space :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, TILE_SRC) + + // A page flip: user (0,0) is at device (0,200). The area is chosen to + // contain the cell that holds pattern-space (0,0), which the /Matrix puts + // at device (5,193). + base := Matrix{1, 0, 0, -1, 0, 200} + area := Rect{0, 150, 40, 195} + + tiles := pattern_tiles(p, area, base) + defer delete(tiles) + testing.expect(t, len(tiles) > 0, "no tiles for a covered area") + + // /Matrix first, then the page CTM: (0,0) -> (5,7) -> (5,193). + want := Point{5, 193} + testing.expect_value(t, pt_has_origin(tiles, want), 1) + + // The same pattern, filled under `q 2 0 0 2 30 40 cm`. The interpreter + // still passes the base CTM, and the lattice must be bit-identical. + fill_ctm := matrix_mul(Matrix{2, 0, 0, 2, 30, 40}, base) + again := pattern_tiles(p, area, base) + defer delete(again) + testing.expect_value(t, len(again), len(tiles)) + for m, i in again { + testing.expect(t, m == tiles[i], "the lattice moved between two fills") + } + + // Control: had the fill's CTM been composed in, the cells would have gone + // somewhere else entirely. Without this the assertion above is vacuous. + wrong := pattern_tiles(p, area, fill_ctm) + defer delete(wrong) + testing.expect_value(t, pt_has_origin(wrong, want), 0) +} + +// pattern_matrix is the single place the two matrices meet, so the order is +// pinned here directly: /Matrix applies first, the page CTM second. The +// reversed product is a different point for any /Matrix with a translation. +@(test) +test_pattern_matrix_composition_order :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, TILE_SRC) + base := Matrix{1, 0, 0, -1, 0, 200} + + m := pattern_matrix(p, base) + testing.expect(t, pt_near(transform_point(m, Point{0, 0}), Point{5, 193}), + "pattern matrix is not /Matrix then CTM") + testing.expect(t, !pt_near(transform_point(matrix_mul(base, p.mtx), Point{0, 0}), + Point{5, 193}), "the reversed product would have passed too") + + // A nil pattern leaves the CTM alone rather than inventing one. + testing.expect(t, pattern_matrix(nil, base) == base, "nil pattern changed the CTM") +} + +// The cell-to-cell offset in device space is the step carried through the same +// composition, so a rotated pattern's lattice is rotated with it and no cell +// drifts from the grid. +@(test) +test_pattern_tiles_are_one_lattice :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The step is deliberately not the cell size: a lattice whose pitch was + // read off /BBox instead of /XStep would still tile, and would still + // cover, and only the spacing would be wrong. + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 8 /YStep 6 >>") + base := matrix_mul(rotate_matrix(30), Matrix{2, 0, 0, 2, 11, 13}) + + area := Rect{0, 0, 60, 60} + tiles := pattern_tiles(p, area, base) + defer delete(tiles) + testing.expect(t, len(tiles) > 4, "too few tiles to be a lattice") + + m := pattern_matrix(p, base) + step_x := transform_vector(m, Point{p.xstep, 0}) + step_y := transform_vector(m, Point{0, p.ystep}) + + // Every origin sits at base + i*step_x + j*step_y for whole i and j. + o0 := pt_origin(m) + for tm in tiles { + o := pt_origin(tm) + // Solve the 2x2 system rather than trusting a division: the steps are + // not axis-aligned here. + det := step_x.x * step_y.y - step_x.y * step_y.x + testing.expect(t, abs(det) > PT_EPS, "degenerate lattice steps") + dx := o.x - o0.x + dy := o.y - o0.y + i := (dx * step_y.y - dy * step_y.x) / det + j := (step_x.x * dy - step_x.y * dx) / det + testing.expectf(t, abs(i - math.round(i)) < 1e-6 && abs(j - math.round(j)) < 1e-6, + "cell origin (%f,%f) is off the lattice: i=%f j=%f", o.x, o.y, i, j) + } + expect_covers_area(t, p, tiles, area, 12, "rotated lattice") +} + +// --------------------------------------------------------------------------- +// Counting +// --------------------------------------------------------------------------- + +// A 10x10 cell over a 90x90 area, with the area deliberately not aligned to +// the lattice so the count does not depend on how a cell that merely touches +// the edge is treated. +@(test) +test_pattern_tile_count_small_cell_large_area :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, PLAIN_SRC) + area := Rect{5, 5, 95, 95} + tiles := pattern_tiles(p, area, MATRIX_IDENTITY) + defer delete(tiles) + + // i runs 0..9 and j runs 0..9: cell 0 spans [0,10] and cell 9 spans + // [90,100], and both meet the area. + testing.expect_value(t, len(tiles), 100) + testing.expect_value(t, pt_has_origin(tiles, Point{0, 0}), 1) + testing.expect_value(t, pt_has_origin(tiles, Point{90, 90}), 1) + testing.expect_value(t, pt_has_origin(tiles, Point{100, 0}), 0) + expect_covers_area(t, p, tiles, area, 20, "aligned lattice") +} + +// A cell larger than its step overlaps its neighbours, which is a real design +// and not a malformation: the lattice just gets denser than the cell size. +@(test) +test_pattern_overlapping_cells :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + before := len(doc.repairs) + p := pt_tiling(t, doc, + "<< /PatternType 1 /BBox [0 0 10 10] /XStep 5 /YStep 10 >>") + testing.expect_value(t, p.xstep, 5.0) + testing.expect_value(t, len(doc.repairs), before) + + area := Rect{5, 5, 95, 95} + tiles := pattern_tiles(p, area, MATRIX_IDENTITY) + defer delete(tiles) + // Twice as many columns as the abutting case, same number of rows: i runs + // -1..19, because the cell is two steps wide and so reaches back one. + testing.expect_value(t, len(tiles), 21 * 10) + // The pitch is the step, not the cell: cells sit every 5 units, so the one + // at 5 exists and is distinct from the ones at 0 and 10. + testing.expect_value(t, pt_has_origin(tiles, Point{-5, 0}), 1) + testing.expect_value(t, pt_has_origin(tiles, Point{5, 0}), 1) + testing.expect_value(t, pt_has_origin(tiles, Point{95, 90}), 1) + expect_covers_area(t, p, tiles, area, 20, "overlapping cells") +} + +// A /BBox that is not at the origin is the case where the lattice index and +// the cell extent stop being the same number, and it is common: a cell drawn +// around a motif is written wherever the motif already was. +@(test) +test_pattern_offset_bbox :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Cells 20x40 at a 20x40 pitch, so they abut, starting at (10,20). + p := pt_tiling(t, doc, + "<< /PatternType 1 /BBox [10 20 30 60] /XStep 20 /YStep 40 >>") + area := Rect{15, 25, 75, 135} + tiles := pattern_tiles(p, area, MATRIX_IDENTITY) + defer delete(tiles) + + // Across: cell i spans [10+20i, 30+20i], so i runs 0..3 for x in 15..75. + // Up: cell j spans [20+40j, 60+40j], so j runs 0..2 for y in 25..135. + testing.expect_value(t, len(tiles), 4 * 3) + // The matrix maps the cell's own space, in which the bbox corner is at + // (10,20), not at the origin. + testing.expect_value(t, pt_has_origin(tiles, Point{0, 0}), 1) + testing.expect_value(t, pt_has_origin(tiles, Point{60, 80}), 1) + expect_covers_area(t, p, tiles, area, 16, "offset bbox") +} + +// An area that falls in the gap between two cells gets no cells at all: a +// /XStep larger than the cell leaves bare stripes, and painting them would +// mean stretching the pattern to fill space the file left empty. +@(test) +test_pattern_area_between_cells :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 1 1] /XStep 100 /YStep 100 >>") + gap := pattern_tiles(p, Rect{10, 10, 20, 20}, MATRIX_IDENTITY) + defer delete(gap) + testing.expect_value(t, len(gap), 0) + + // The same pattern over an area that does reach a cell finds it. + hit := pattern_tiles(p, Rect{-5, -5, 5, 5}, MATRIX_IDENTITY) + defer delete(hit) + testing.expect_value(t, len(hit), 1) + testing.expect_value(t, pt_has_origin(hit, Point{0, 0}), 1) +} + +// The returned slice belongs to the caller and comes from the allocator it was +// given, not from the context: a page is rendered through a scratch arena that +// is reset between pages, and a lattice that quietly allocated from the +// Document's arena instead would never be reclaimed. +// +// The assertion is the arena's own cursor, not a leak report: a slice taken +// from somewhere else leaves the arena untouched. +@(test) +test_pattern_tiles_use_the_given_allocator :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + backing: [16 * 1024]byte + arena: mem.Arena + mem.arena_init(&arena, backing[:]) + + p := pt_tiling(t, doc, PLAIN_SRC) + tiles := pattern_tiles(p, Rect{5, 5, 95, 95}, MATRIX_IDENTITY, mem.arena_allocator(&arena)) + testing.expect_value(t, len(tiles), 100) + testing.expect(t, arena.offset >= 100 * size_of(Matrix), + "the tiles did not come from the allocator that was passed in") + lo := uintptr(raw_data(backing[:])) + at := uintptr(raw_data(tiles)) + testing.expect(t, at >= lo && at < lo + uintptr(len(backing)), + "the tiles do not live in the arena") +} + +// The pattern's own space is where the lattice is regular, so a rotated +// pattern needs the area pulled back through the inverse before the indices +// mean anything. If that pull-back were skipped the corners of the area would +// come out bare. +@(test) +test_pattern_tiles_under_rotation_cover_the_area :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, + "<< /PatternType 1 /BBox [0 0 8 8] /XStep 8 /YStep 8 /Matrix [0.866 0.5 -0.5 0.866 3 4] >>") + area := Rect{-30, -30, 30, 30} + tiles := pattern_tiles(p, area, MATRIX_IDENTITY) + defer delete(tiles) + + testing.expect(t, len(tiles) > 0, "rotated pattern produced no tiles") + expect_covers_area(t, p, tiles, area, 24, "rotated pattern") +} + +// --------------------------------------------------------------------------- +// Guards +// --------------------------------------------------------------------------- + +// /XStep 0 would stack an infinite row of cells at one place. It falls back to +// the cell width, which is what the step means when cells abut, and the +// substitution is recorded. +@(test) +test_pattern_zero_step_clamped_to_bbox :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + before := len(doc.repairs) + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 20] /XStep 0 /YStep 0 >>") + testing.expect_value(t, p.xstep, 10.0) + testing.expect_value(t, p.ystep, 20.0) + testing.expect(t, len(doc.repairs) >= before + 2, "a clamped step was not recorded") + + area := Rect{5, 5, 95, 95} + tiles := pattern_tiles(p, area, MATRIX_IDENTITY) + defer delete(tiles) + testing.expect_value(t, len(tiles), 10 * 5) + expect_covers_area(t, p, tiles, area, 20, "zero step") +} + +// A step a ten-thousandth of the cell is the same denial of service as zero, +// written so that it looks like a number: every device pixel would be covered +// by ten thousand cells. +@(test) +test_pattern_absurd_step_clamped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + before := len(doc.repairs) + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 0.001 /YStep 10 >>") + testing.expect_value(t, p.xstep, 10.0) + testing.expect_value(t, p.ystep, 10.0) + testing.expect(t, len(doc.repairs) > before, "a clamped step was not recorded") + + // An overlap the guard is meant to allow through untouched. + before2 := len(doc.repairs) + q := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 2.5 /YStep 10 >>") + testing.expect_value(t, q.xstep, 2.5) + testing.expect_value(t, len(doc.repairs), before2) +} + +// Only the magnitude of a step spaces the lattice, so a negative one is read +// rather than repaired. +@(test) +test_pattern_negative_step_uses_magnitude :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + before := len(doc.repairs) + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep -10 /YStep -10 >>") + testing.expect_value(t, p.xstep, 10.0) + testing.expect_value(t, p.ystep, 10.0) + testing.expect_value(t, len(doc.repairs), before) + + tiles := pattern_tiles(p, Rect{5, 5, 95, 95}, MATRIX_IDENTITY) + defer delete(tiles) + testing.expect_value(t, len(tiles), 100) +} + +// A one-unit cell over a million-unit area is 10^12 replays of a content +// stream. The count is capped, the shortfall is recorded once, and what comes +// back is still a usable prefix of the lattice rather than nothing. +@(test) +test_pattern_tile_count_capped :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 1 1] /XStep 1 /YStep 1 >>") + + before := len(doc.repairs) + tiles := pattern_tiles(p, Rect{0, 0, 1e6, 1e6}, MATRIX_IDENTITY) + defer delete(tiles) + testing.expect_value(t, len(tiles), MAX_PATTERN_TILES) + testing.expect(t, len(doc.repairs) > before, "the tile cap was not recorded") + + // Every returned matrix is still a real lattice position, not filler. + for m, i in tiles { + o := pt_origin(m) + testing.expectf(t, is_finite(o.x) && is_finite(o.y), + "tile %d has a non-finite origin (%f,%f)", i, o.x, o.y) + } + + // A page that fills with the same hostile pattern many times must cost one + // repair, not one per fill. + mid := len(doc.repairs) + more := pattern_tiles(p, Rect{0, 0, 1e6, 1e6}, MATRIX_IDENTITY) + defer delete(more) + testing.expect_value(t, len(more), MAX_PATTERN_TILES) + testing.expect_value(t, len(doc.repairs), mid) +} + +// An index range computed from file numbers can be 1e300, and narrowing that +// to an int is undefined. The clamp lives in tile_range, so it is pinned +// there: what comes back is bounded, and no arithmetic on it can trap. +@(test) +test_pattern_tile_range_clamps :: proc(t: ^testing.T) { + from, to, ok := tile_range(-1e300, 1e300, 0, 10, 1e-6) + testing.expect(t, ok, "a huge but finite range was rejected") + testing.expect_value(t, from, -MAX_TILE_INDEX) + testing.expect_value(t, to, MAX_TILE_INDEX) + + _, _, ok_step := tile_range(0, 10, 0, 1, 0) + testing.expect(t, !ok_step, "a zero step produced a range") + + _, _, ok_nan := tile_range(math.QNAN_F64, 10, 0, 1, 1) + testing.expect(t, !ok_nan, "a NaN bound produced a range") + + // An area that ends before the first cell begins has no cells at all. + _, _, ok_empty := tile_range(100, 101, 0, 1, 1000) + testing.expect(t, !ok_empty, "a gap between cells produced a range") +} + +// Areas that cannot be enumerated, and patterns that have no cells. +@(test) +test_pattern_tiles_rejects_unusable_input :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_tiling(t, doc, PLAIN_SRC) + + testing.expect_value(t, len(pattern_tiles(nil, Rect{0, 0, 10, 10}, MATRIX_IDENTITY)), 0) + testing.expect_value(t, len(pattern_tiles(p, RECT_EMPTY, MATRIX_IDENTITY)), 0) + // "Cover an unbounded area with a repeating cell" has no answer, and + // RECT_INFINITE's corners are finite numbers, so it has to be caught by + // identity or it would silently produce a capped field of cells. + testing.expect_value(t, len(pattern_tiles(p, RECT_INFINITE, MATRIX_IDENTITY)), 0) + nan_area := Rect{0, 0, math.QNAN_F64, 10} + testing.expect_value(t, len(pattern_tiles(p, nan_area, MATRIX_IDENTITY)), 0) + inf_ctm := Matrix{math.INF_F64, 0, 0, 1, 0, 0} + testing.expect_value(t, len(pattern_tiles(p, Rect{0, 0, 10, 10}, inf_ctm)), 0) + // A CTM that collapses the plane gives every cell zero area. + singular := Matrix{1, 1, 1, 1, 0, 0} + testing.expect_value(t, len(pattern_tiles(p, Rect{0, 0, 10, 10}, singular)), 0) + + // A shading pattern paints in one pass; it has no cells to replay. + sp := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>") + testing.expect_value(t, len(pattern_tiles(sp, Rect{0, 0, 10, 10}, MATRIX_IDENTITY)), 0) +} + +// --------------------------------------------------------------------------- +// Uncoloured patterns +// --------------------------------------------------------------------------- + +// /PaintType 2 is a stencil: the cell's own colour operators are ignored and +// the whole cell paints in the colour given alongside the pattern name, so the +// interpreter has to be told before it replays anything. +@(test) +test_pattern_uncoloured_needs_the_callers_colour :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + unc := pt_tiling(t, doc, + "<< /PatternType 1 /PaintType 2 /BBox [0 0 4 4] /XStep 4 /YStep 4 >>") + testing.expect_value(t, unc.paint_type, Paint_Type.Uncoloured) + testing.expect(t, pattern_needs_color(unc), "an uncoloured pattern must ask for a colour") + + col := pt_tiling(t, doc, PLAIN_SRC) + testing.expect_value(t, col.paint_type, Paint_Type.Coloured) + testing.expect(t, !pattern_needs_color(col), "a coloured pattern supplies its own colour") + + sh := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>") + testing.expect(t, !pattern_needs_color(sh), "a shading pattern supplies its own colour") + testing.expect(t, !pattern_needs_color(nil), "nil asked for a colour") + + // An out-of-range /PaintType is a broken file, not a third kind of paint. + odd := pt_tiling(t, doc, + "<< /PatternType 1 /PaintType 7 /BBox [0 0 4 4] /XStep 4 /YStep 4 >>") + testing.expect_value(t, odd.paint_type, Paint_Type.Coloured) +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +@(test) +test_pattern_tiling_fields :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + res := new_dict(doc.allocator) + p, err := pattern_load(doc, cf_stream(doc, + "<< /PatternType 1 /PaintType 1 /TilingType 3 /BBox [10 20 30 60] " + + "/XStep 25 /YStep 45 /Matrix [2 0 0 2 1 1] >>", nil), res) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, p.kind, Pattern_Kind.Tiling) + testing.expect_value(t, p.tiling_type, Tiling_Type.Faster_Constant_Spacing) + testing.expect_value(t, p.bbox, Rect{10, 20, 30, 60}) + testing.expect_value(t, p.xstep, 25.0) + testing.expect_value(t, p.ystep, 45.0) + testing.expect_value(t, p.mtx, Matrix{2, 0, 0, 2, 1, 1}) + testing.expect(t, p.content != nil, "the cell's content stream was dropped") + testing.expect(t, p.doc == doc, "the pattern lost its document") + // /Resources is required but harmless to omit for a cell that draws only + // rectangles; the resources of the stream that named the pattern stand in. + testing.expect(t, p.resources == res, "missing /Resources did not fall back") + + // A pattern with resources of its own keeps them. + q, q_err := pattern_load(doc, cf_stream(doc, + "<< /PatternType 1 /BBox [0 0 4 4] /Resources << /Marker 7 >> >>", nil), res) + testing.expect_value(t, q_err, Error.None) + testing.expect(t, q.resources != res, "own /Resources were replaced by the fallback") + testing.expect_value(t, dict_int(doc, q.resources, "Marker"), i64(7)) + // Absent /XStep and /YStep default to the cell size, the same as zero. + testing.expect_value(t, q.xstep, 4.0) + testing.expect_value(t, q.ystep, 4.0) +} + +@(test) +test_pattern_shading_loads_its_shading :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + p := pt_dict_pattern(t, doc, + "<< /PatternType 2 /Matrix [1 0 0 1 20 30] /Shading " + AXIAL + + " /ExtGState << /ca 0.5 >> >>") + testing.expect_value(t, p.kind, Pattern_Kind.Shading) + testing.expect(t, p.shading != nil, "the shading was not loaded") + testing.expect_value(t, p.shading.kind, Shading_Kind.Axial) + testing.expect_value(t, p.shading.coords[2], 10.0) + testing.expect(t, p.ext_gstate != nil, "/ExtGState was dropped") + testing.expect_value(t, dict_num(doc, p.ext_gstate, "ca"), 0.5) + testing.expect(t, p.content == nil, "a shading pattern has no cell content") +} + +// A colour space named by a bare name inside the shading has to be resolved +// through the resource dictionary the pattern was named from. +@(test) +test_pattern_shading_uses_the_resources :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + cs_dict := new_dict(doc.allocator) + dict_set(cs_dict, "Spot", Ref{5, 0}) + res := new_dict(doc.allocator) + dict_set(res, "ColorSpace", cs_dict) + + p := pt_dict_pattern(t, doc, + "<< /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /Spot " + + "/Coords [0 0 10 0] /Function << /FunctionType 2 /Domain [0 1] " + + "/C0 [0] /C1 [1] /N 1 >> >> >>", res) + testing.expect(t, p.shading != nil && p.shading.cs != nil, "the shading has no colour space") + testing.expect_value(t, p.shading.cs.kind, Colorspace_Kind.Separation) +} + +// A missing /PatternType is a broken file that still says which kind it meant. +@(test) +test_pattern_type_inferred :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + before := len(doc.repairs) + tile := pt_tiling(t, doc, "<< /BBox [0 0 4 4] /XStep 4 /YStep 4 >>") + testing.expect_value(t, tile.kind, Pattern_Kind.Tiling) + testing.expect(t, len(doc.repairs) > before, "the missing /PatternType was not recorded") + + sh := pt_dict_pattern(t, doc, "<< /Shading " + AXIAL + " >>") + testing.expect_value(t, sh.kind, Pattern_Kind.Shading) + + // A stream that also carries a /Shading is a shading pattern: the entry + // that only one kind has decides it. + both := pt_tiling(t, doc, "<< /BBox [0 0 4 4] /Shading " + AXIAL + " >>") + testing.expect_value(t, both.kind, Pattern_Kind.Shading) +} + +@(test) +test_pattern_load_rejects_malformed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + expect_bad :: proc(t: ^testing.T, doc: ^Document, obj: Object, what: string, + loc := #caller_location) { + p, err := pattern_load(doc, obj, nil) + testing.expect_value(t, err, Error.Bad_Pattern, loc = loc) + testing.expectf(t, p == nil, "%s produced a pattern", what, loc = loc) + } + + expect_bad(t, doc, cf_parse(doc, "42"), "a number") + expect_bad(t, doc, nil, "a null") + expect_bad(t, doc, cf_parse(doc, "<< /PatternType 9 >>"), "an unknown /PatternType") + expect_bad(t, doc, cf_parse(doc, "<< /PatternType 1 /BBox [0 0 4 4] >>"), + "a tiling pattern that is not a stream") + expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 >>", nil), "no /BBox") + expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 /BBox [0 0 0 8] >>", nil), + "a /BBox with no width") + expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 /BBox [1 2 3] >>", nil), + "a short /BBox") + // Both corners are finite; the width between them is not, and an infinite + // step times a zero lattice index is a NaN cell matrix. + expect_bad(t, doc, cf_stream(doc, + "<< /PatternType 1 /BBox [-1e308 -1e308 1e308 1e308] >>", nil), + "a /BBox as wide as the number line") + expect_bad(t, doc, cf_parse(doc, "<< /PatternType 2 >>"), "no /Shading") + expect_bad(t, doc, cf_parse(doc, "<< /PatternType 2 /Shading << /ShadingType 99 >> >>"), + "an unknown shading type") + + // A nil Document is a caller error, not a malformed file. + p, err := pattern_load(nil, cf_parse(doc, "<< /PatternType 1 >>"), nil) + testing.expect_value(t, err, Error.Invalid_Argument) + testing.expect(t, p == nil, "a nil document produced a pattern") +} + +// A /Matrix that is not six finite numbers has to arrive as the identity -- +// the format's own default -- rather than as a NaN that would poison every +// transform derived from it. The filter itself is `object_to_matrix`'s, in +// geom.odin; what is pinned here is that pattern loading goes through it and +// does not substitute a default of its own. +@(test) +test_pattern_bad_matrix_falls_back :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + short := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] /Matrix [1 0 0] >>") + testing.expect_value(t, short.mtx, MATRIX_IDENTITY) + + absent := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] >>") + testing.expect_value(t, absent.mtx, MATRIX_IDENTITY) + + // 1e400 parses to an infinity, which is exactly the value that must not + // reach a lattice: a single infinite entry makes every cell origin a NaN. + inf := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] /Matrix [1 0 0 1 0 1e400] >>") + testing.expect_value(t, inf.mtx, MATRIX_IDENTITY) + testing.expect(t, matrix_is_finite(inf.mtx), "a non-finite /Matrix was kept") + + tiles := pattern_tiles(inf, Rect{0, 0, 8, 8}, MATRIX_IDENTITY) + defer delete(tiles) + testing.expect(t, len(tiles) > 0, "a repaired /Matrix still produced no cells") + for m in tiles { + o := pt_origin(m) + testing.expect(t, is_finite(o.x) && is_finite(o.y), "a cell origin is not finite") + } +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +@(test) +test_pattern_bounds :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A tiling pattern repeats over the whole plane. RECT_INFINITE is the + // identity for rect_intersect, so a caller gets back its own clip. + tile := pt_tiling(t, doc, PLAIN_SRC) + testing.expect_value(t, pattern_bounds(tile, MATRIX_IDENTITY), RECT_INFINITE) + testing.expect_value(t, rect_intersect(pattern_bounds(tile, MATRIX_IDENTITY), + Rect{3, 4, 5, 6}), Rect{3, 4, 5, 6}) + + // A transform that collapses the plane paints nothing, and nothing is the + // opposite identity: a caller intersecting with RECT_INFINITE here would + // flood the clip with a pattern that has no area. + testing.expect_value(t, pattern_bounds(tile, Matrix{1, 1, 1, 1, 0, 0}), RECT_EMPTY) + testing.expect_value(t, pattern_bounds(tile, Matrix{math.QNAN_F64, 0, 0, 1, 0, 0}), RECT_EMPTY) + testing.expect_value(t, pattern_bounds(nil, MATRIX_IDENTITY), RECT_EMPTY) + + // An un-extended radial shading is bounded by its two circles, and the + // pattern matrix moves that bound with it. + rad := pt_dict_pattern(t, doc, + "<< /PatternType 2 /Matrix [1 0 0 1 20 30] /Shading << /ShadingType 3 " + + "/ColorSpace /DeviceGray /Coords [0 0 0 0 0 5] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>") + testing.expect_value(t, pattern_bounds(rad, MATRIX_IDENTITY), Rect{15, 25, 25, 35}) + // Under a page CTM the two compose, and only in that order. + testing.expect_value(t, pattern_bounds(rad, Matrix{2, 0, 0, 2, 0, 0}), + Rect{30, 50, 50, 70}) + + // An axial shading is unbounded across its axis whatever its extends say. + ax := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>") + testing.expect_value(t, pattern_bounds(ax, MATRIX_IDENTITY), RECT_INFINITE) +} + +// --------------------------------------------------------------------------- +// Painting a shading pattern +// --------------------------------------------------------------------------- + +// The shading has to be painted through the pattern matrix composed with the +// page's default CTM, so a pattern that translates its shading by 20 puts the +// ramp 20 device units further along. Three components and no alpha means the +// destination reads as opaque, so a pixel is what the shading computed. +@(test) +test_pattern_paint_shading_through_the_matrix :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // A ramp from black at x=0 to white at x=10 in pattern space, extended + // both ways, shifted 20 to the right by the pattern matrix. + p := pt_dict_pattern(t, doc, + "<< /PatternType 2 /Matrix [1 0 0 1 20 0] /Shading " + AXIAL + " >>") + + bmp, err := bitmap_make(40, 3, 3) + testing.expect_value(t, err, Error.None) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, [4]u8{9, 200, 30, 255}) + + perr := pattern_paint_shading(doc, p, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil) + testing.expect_value(t, perr, Error.None) + + px :: proc(bmp: ^Bitmap, x, y: int) -> u8 { + off, ok := bitmap_pixel_offset(bmp, x, y) + return ok ? bmp.pixels[off] : 0 + } + // Pixel centres are at x+0.5, so the axis start (pattern x=0, device + // x=20) is half a pixel left of pixel 20's centre: t = 0.05. + testing.expect(t, px(&bmp, 10, 1) == 0, "the extended dark end is not black") + testing.expect(t, px(&bmp, 35, 1) == 255, "the extended bright end is not white") + mid := px(&bmp, 25, 1) + testing.expectf(t, mid > 120 && mid < 160, "midpoint of the ramp is %d, want about 140", mid) + // Without the pattern matrix the ramp would have finished by x = 10. + testing.expect(t, px(&bmp, 12, 1) == 0, "the ramp started 20 units too early") + + // A tiling pattern has no shading to paint, and neither does nil. + tile := pt_tiling(t, doc, PLAIN_SRC) + testing.expect_value(t, pattern_paint_shading(doc, tile, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil), Error.Invalid_Argument) + testing.expect_value(t, pattern_paint_shading(doc, nil, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil), Error.Invalid_Argument) +} diff --git a/pdf.odin b/pdf.odin new file mode 100644 index 0000000..d0575c2 --- /dev/null +++ b/pdf.odin @@ -0,0 +1,90 @@ +/* +Package pdf is a self-contained PDF reader and renderer. + +It parses PDF files (ISO 32000-2 / PDF 2.0, and every earlier revision in +practice), decodes their content, rasterises pages to RGBA8 bitmaps and +extracts their text. It depends only on Odin's core collection -- no C +libraries, no FreeType, no libjpeg -- so it can be dropped into any Odin +project as-is. + +Reading a file and walking its pages works today: + + doc, err := pdf.open_file("thesis.pdf") + if err != .None { + fmt.eprintln(pdf.error_string(err)) + return + } + defer pdf.close(doc) + + for i in 0 ..< pdf.page_count(doc) { + page, perr := pdf.page_get(doc, i) // Page is a value, not a pointer + if perr != .None { continue } + fmt.println(i, page.media_box, page.rotate) + } + +`open_bytes` opens a file already in memory, and `Open_Options` carries the +password for an encrypted document and `skip_page_tree` for a tool that only +wants to inspect objects. Below the page tree, `get_object`, `get_dict`, +`get_stream`, `resolve`, the `dict_*` accessors and `stream_data` read the +object graph directly. + +The document's navigation model is readable today too. `outline_load` returns +the bookmark tree as `Outline_Item`s, `dest_resolve` turns a destination -- +written out in full, named, or carried by a /GoTo action -- into a +`Destination` naming a page index, and `page_label` gives the label a viewer +shows for a page, "iv" rather than "4". Unlike everything above they hand back +memory the caller owns: each takes an `allocator`, and the outline is freed +with `outline_destroy`. + +Rendering and text extraction are the other half: + + page, _ := pdf.page_get(doc, 0) + + bmp, rerr := pdf.render_page(&page, {ctm = pdf.scale_matrix(2, 2)}) + if rerr == .None { + defer pdf.bitmap_destroy(&bmp) // RGBA8, straight alpha + upload_texture(bmp.width, bmp.height, bmp.pixels) + } + + text, _ := pdf.extract_text(&page) + defer delete(text) + +`Render_Options` carries the transform, the background colour, whether the page +keeps an alpha channel and whether annotations are drawn. `render_page_to` +renders into a bitmap the caller already has, which is how a large page is +rendered in tiles. `page_base_matrix` is the transform all of them share -- PDF +user space to device pixels, with the crop box, /Rotate and /UserUnit folded in +-- and inverting it is what turns a pixel a user clicked on back into a point +on the page. Below those, `run_page` runs a page into a `Device` of the +caller's own and `page_display_list` records one for replay. A `Bitmap`'s +pixels are reachable directly -- it is a plain struct -- and `bitmap_row`, +`bitmap_make`, `bitmap_destroy` and `bitmap_to_rgba` are the accessors for +callers that would rather not compute `stride` themselves. + +Memory model: a Document owns a growing arena. Every object, stream and page +parsed out of the file lives in that arena and is freed in one shot by +`close`. Nothing the library hands back out of a Document needs to be freed +individually -- a `Page`, a `^Dict` and the bytes from `stream_data` are all +borrowed and die with the Document. The exceptions are the few procedures that +take an explicit `allocator` parameter and say so in their doc comment: +`render_page`'s bitmap, `extract_text`'s string, `page_display_list`'s list and +the navigation procedures above. Rendering allocates nothing else that outlives +the call -- per-page scratch lives in an arena that is destroyed when the call +returns, so a 900-page loop peaks at one page's working set. + +Thread safety: a Document is not internally synchronised. Parsing mutates +caches, so one Document should be driven by one thread at a time. Two +Documents are fully independent. +*/ +package pdf + +// The PDF version this library targets. Files declaring a newer version are +// still parsed -- PDF readers are required to be forward tolerant, and a +// version number in a header is not a promise about the bytes that follow. +SPEC_VERSION_MAJOR :: 2 +SPEC_VERSION_MINOR :: 0 + +// Library version, for callers that want to report it. +VERSION_MAJOR :: 0 +VERSION_MINOR :: 1 +VERSION_PATCH :: 0 diff --git a/raster.odin b/raster.odin new file mode 100644 index 0000000..d0515af --- /dev/null +++ b/raster.odin @@ -0,0 +1,513 @@ +package pdf + +import "core:math" +import "core:mem" + +// Anti-aliased scanline fill. +// +// WHICH ALGORITHM, AND WHY +// +// This is a signed-area cell accumulator in the style of FreeType's `smooth` +// renderer, not a sub-scanline sampler. Every line segment is chopped at the +// scanline boundaries it crosses and then at the pixel boundaries it crosses +// within a scanline, and each resulting piece deposits two numbers into the +// cell it lies in: +// +// cover[i] += dy the signed height the edge spans +// area [i] += dy * (fx1 + fx2) twice the trapezoid between the edge +// and the LEFT wall of the cell +// +// A left-to-right prefix sum over `cover` then gives the winding-weighted +// coverage of every cell exactly: +// +// coverage(i) = sum(cover[j] for j < i) + cover[i] - area[i]/2 +// +// which is the integral of the winding number over the pixel -- exact in both +// axes for a polygon, with no sampling error at all. Sixteen sub-scanlines +// would quantise vertical coverage to 1/16 and cost sixteen passes over every +// edge; this costs one, and a near-horizontal glyph stem comes out smooth +// instead of banded. The one place it is an approximation rather than an +// identity is a pixel that a path enters with winding +1 and leaves with +// winding -1: the integral cancels where the true nonzero rule would fill. +// That is a sub-pixel artefact at a self-intersection, it is what FreeType, +// stb_truetype and every other accumulator-based rasteriser do, and it is +// invisible next to the cost of the alternative. +// +// The prefix sum is kept in floating point rather than 26.6 fixed point +// because the input is already f64 device coordinates: rounding to a fixed +// grid first would throw away precision that the analytic area then pretends +// to have. + +// Span_Proc receives the coverage of one horizontal run on scanline `y`, +// beginning at device pixel `x0`. Values are 0..255. The slice is scratch +// owned by the rasteriser and is only valid for the duration of the call. +// +// Runs arrive in increasing `y`, and in increasing `x` within a row, and never +// overlap, so a consumer can write straight into a row without sorting. +@(private) +Span_Proc :: #type proc(user: rawptr, y: int, x0: int, coverage: []u8) + +// Raster_Clip is the device-pixel box outside which nothing is drawn, given as +// a half-open range: x0 <= x < x1, y0 <= y < y1. +@(private) +Raster_Clip :: struct { + x0, y0, x1, y1: int, +} + +// MAX_RASTER_EDGES caps the edge table. A page-sized fill needs a handful of +// edges and the most complex real content -- a vector map -- a few hundred +// thousand; a million is past anything legitimate and stops a hostile content +// stream from turning a 200-byte path into gigabytes of edges. Past the cap +// the remaining segments are dropped, which distorts the shape but keeps the +// page rendering, per the degrade-don't-fail rule. +@(private) +MAX_RASTER_EDGES :: 1 << 20 + +// RASTER_COORD_MAX is where coordinates stop being geometry and start being +// noise. Everything is clipped to the clip box anyway, so the only job of this +// clamp is to keep infinities out of the arithmetic that computes the clip +// intersection. +@(private) +RASTER_COORD_MAX :: 1e15 + +// Raster_Edge is one non-horizontal segment, already clipped to the raster +// box in both axes, oriented downwards (y0 < y1) with the original direction +// kept in `dir` -- the winding number needs the direction, the sweep needs the +// order, and carrying both separately is cheaper than re-deriving either. +@(private) +Raster_Edge :: struct { + x0, y0: f64, + x1, y1: f64, + // 1/(y1-y0), for interpolating x at a scanline boundary. + inv_dy: f64, + // +1 when the segment ran down the page, -1 when it ran up it. + dir: f64, + // Next edge starting on the same scanline, or -1. + next: i32, +} + +@(private) +Raster_State :: struct { + // The raster box: the clip intersected with the path's bounds. Cells run + // rx0 ..= rx1 inclusive -- one past the right edge is a guard cell that + // collects the winding of everything clipped off to the right and is never + // emitted. + rx0, rx1, ry0, ry1: int, + fx0, fx1: f64, + + edges: [dynamic]Raster_Edge, + // Head of the list of edges that start on each scanline, indexed from ry0. + buckets: []i32, + active: []i32, + n_active: int, + + cover: []f64, + area: []f64, + covbuf: []u8, + + // Cells touched on the row being built, so the next row only has to clear + // what was written. A page of text is thousands of narrow rows; clearing + // the full clip width for each would dominate the whole fill. + min_x, max_x: int, +} + +// raster_fill scan-converts `fp` and reports its coverage through `span`. +// +// Subpaths are closed implicitly whether or not `Flat_Subpath.closed` is set: +// that is what the fill operators mean (ISO 32000-2, 8.5.3.3.1). Horizontal +// segments are dropped, since they carry no winding. +// +// Degenerate input -- no path, no points, a single point, a subpath that never +// leaves one horizontal line, a path wholly outside `clip` -- produces no +// spans and no error. `allocator` is used for scratch only; everything it +// allocates is released before this returns. +@(private) +raster_fill :: proc(fp: ^Flat_Path, + even_odd: bool, + clip: Raster_Clip, + span: Span_Proc, + user: rawptr, + allocator := context.allocator) -> Error { + if span == nil { return .Invalid_Argument } + if fp == nil || len(fp.points) == 0 || len(fp.subpaths) == 0 { return .None } + if clip.x1 <= clip.x0 || clip.y1 <= clip.y0 { return .None } + + // Narrowing the raster box to the path is not just an optimisation: it is + // what keeps a glyph in the corner of a 300 dpi page from allocating and + // clearing a 2550-cell row, and what bounds a path whose bounds are 1e9 + // wide to the size of the clip. + b := flat_path_bounds(fp) + r := Raster_State{ + rx0 = raster_floor_clamped(b.x0, clip.x0, clip.x1), + rx1 = raster_ceil_clamped (b.x1, clip.x0, clip.x1), + ry0 = raster_floor_clamped(b.y0, clip.y0, clip.y1), + ry1 = raster_ceil_clamped (b.y1, clip.y0, clip.y1), + } + if r.rx1 <= r.rx0 || r.ry1 <= r.ry0 { return .None } + r.fx0 = f64(r.rx0) + r.fx1 = f64(r.rx1) + + width := r.rx1 - r.rx0 + height := r.ry1 - r.ry0 + + // One allocation each, up front. Nothing below this line allocates, which + // is what makes the per-scanline cost independent of the path. + segments := 0 + for sp in fp.subpaths { + if sp.count >= 2 { segments += sp.count } + } + if segments <= 0 { return .None } + + aerr: mem.Allocator_Error + r.edges, aerr = make([dynamic]Raster_Edge, 0, min(segments, 1024), allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.edges) + + r.buckets, aerr = make([]i32, height, allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.buckets, allocator) + for &h in r.buckets { h = -1 } + + r.cover, aerr = make([]f64, width + 1, allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.cover, allocator) + + r.area, aerr = make([]f64, width + 1, allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.area, allocator) + + r.covbuf, aerr = make([]u8, width, allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.covbuf, allocator) + + for i in 0 ..< len(fp.subpaths) { + pts := flat_path_slice(fp, i) + if len(pts) < 2 { continue } + for j in 0 ..< len(pts) - 1 { + raster_add_edge(&r, pts[j], pts[j + 1]) + } + // The closing segment. A subpath whose last point already coincides + // with its first would only add a zero-length edge, which is dropped + // by the horizontal test anyway, but skipping it keeps the edge table + // honest for the common case of an explicitly closed rectangle. + if pts[len(pts) - 1] != pts[0] { + raster_add_edge(&r, pts[len(pts) - 1], pts[0]) + } + } + if len(r.edges) == 0 { return .None } + + r.active, aerr = make([]i32, len(r.edges), allocator) + if aerr != nil { return .Out_Of_Memory } + defer delete(r.active, allocator) + + for y in r.ry0 ..< r.ry1 { + for e := r.buckets[y - r.ry0]; e >= 0; { + next := r.edges[e].next + if r.n_active < len(r.active) { + r.active[r.n_active] = e + r.n_active += 1 + } + e = next + } + if r.n_active == 0 { continue } + + row_top := f64(y) + row_bot := f64(y + 1) + r.min_x = r.rx1 + 1 + r.max_x = r.rx0 - 1 + + for k in 0 ..< r.n_active { + e := &r.edges[r.active[k]] + ya := max(row_top, e.y0) + yb := min(row_bot, e.y1) + if yb <= ya { continue } + xa := ya <= e.y0 ? e.x0 : raster_edge_x(&r, e, ya) + xb := yb >= e.y1 ? e.x1 : raster_edge_x(&r, e, yb) + raster_accumulate(&r, xa, ya, xb, yb, e.dir) + } + + raster_sweep(&r, y, even_odd, span, user) + + if r.min_x <= r.max_x { + for i in r.min_x ..= r.max_x { + r.cover[i - r.rx0] = 0 + r.area [i - r.rx0] = 0 + } + } + + w := 0 + for k in 0 ..< r.n_active { + if r.edges[r.active[k]].y1 > row_bot { + r.active[w] = r.active[k] + w += 1 + } + } + r.n_active = w + } + return .None +} + +// --------------------------------------------------------------------------- +// Building the edge table +// --------------------------------------------------------------------------- + +// raster_add_edge clips one segment to the raster box and appends the one to +// three edges that survive. +// +// Clipping in y is a plain truncation: coverage on a scanline depends only on +// the parts of edges inside that scanline, so anything above or below the box +// cannot affect it. Clipping in x is NOT a truncation -- a segment to the left +// of the box still winds every pixel to its right -- so the parts outside are +// replaced by vertical edges standing on the box's walls, which wind +// identically. That substitution is what makes a path 10^9 units wide cost the +// same as one the width of the clip. +@(private) +raster_add_edge :: proc(r: ^Raster_State, p0, p1: Point) { + if len(r.edges) >= MAX_RASTER_EDGES { return } + + x0 := raster_sane(p0.x) + y0 := raster_sane(p0.y) + x1 := raster_sane(p1.x) + y1 := raster_sane(p1.y) + + if y0 == y1 { return } + dir := 1.0 + if y0 > y1 { + x0, x1 = x1, x0 + y0, y1 = y1, y0 + dir = -1 + } + + top := f64(r.ry0) + bot := f64(r.ry1) + cy0 := max(y0, top) + cy1 := min(y1, bot) + if cy1 <= cy0 { return } + cx0 := cy0 == y0 ? x0 : raster_lerp_x(x0, y0, x1, y1, cy0) + cx1 := cy1 == y1 ? x1 : raster_lerp_x(x0, y0, x1, y1, cy1) + + // Split where the segment crosses the left and right walls, so that each + // piece is wholly inside or wholly outside and clamping its endpoints is + // exact rather than a bend. + ys: [4]f64 + n := 0 + ys[n] = cy0; n += 1 + dx := cx1 - cx0 + if dx != 0 { + for wall in ([2]f64{r.fx0, r.fx1}) { + if (cx0 < wall) == (cx1 < wall) { continue } + t := (wall - cx0) / dx + if !(t > 0 && t < 1) { continue } + yy := cy0 + (cy1 - cy0) * t + if yy > cy0 && yy < cy1 { + ys[n] = yy; n += 1 + } + } + } + ys[n] = cy1; n += 1 + if n == 4 && ys[1] > ys[2] { + ys[1], ys[2] = ys[2], ys[1] + } + + for i in 0 ..< n - 1 { + ya := ys[i] + yb := ys[i + 1] + if yb <= ya { continue } + xa := clamp(ya == cy0 ? cx0 : raster_lerp_x(cx0, cy0, cx1, cy1, ya), r.fx0, r.fx1) + xb := clamp(yb == cy1 ? cx1 : raster_lerp_x(cx0, cy0, cx1, cy1, yb), r.fx0, r.fx1) + raster_push_edge(r, xa, ya, xb, yb, dir) + } +} + +@(private) +raster_push_edge :: proc(r: ^Raster_State, x0, y0, x1, y1, dir: f64) { + if len(r.edges) >= MAX_RASTER_EDGES { return } + dy := y1 - y0 + if !(dy > 0) { return } + + row := int(math.floor(y0)) + row = clamp(row, r.ry0, r.ry1 - 1) + + idx := i32(len(r.edges)) + append(&r.edges, Raster_Edge{ + x0 = x0, + y0 = y0, + x1 = x1, + y1 = y1, + inv_dy = 1 / dy, + dir = dir, + next = r.buckets[row - r.ry0], + }) + r.buckets[row - r.ry0] = idx +} + +// raster_edge_x interpolates the edge's x at a scanline boundary. The guards +// are not decoration: an edge whose y extent is denormally small has an +// infinite 1/dy, and 0 * infinity is a NaN that would otherwise reach the cell +// walk and index an array with it. +@(private) +raster_edge_x :: proc(r: ^Raster_State, e: ^Raster_Edge, y: f64) -> f64 { + t := (y - e.y0) * e.inv_dy + if !is_finite(t) { t = 0 } + t = clamp(t, 0, 1) + x := e.x0 + (e.x1 - e.x0) * t + if !is_finite(x) { x = e.x0 } + return clamp(x, r.fx0, r.fx1) +} + +@(private) +raster_lerp_x :: proc "contextless" (x0, y0, x1, y1, y: f64) -> f64 { + dy := y1 - y0 + t := dy != 0 ? (y - y0) / dy : 0 + if !is_finite(t) { t = 0 } + t = clamp(t, 0, 1) + x := x0 + (x1 - x0) * t + return is_finite(x) ? x : x0 +} + +@(private) +raster_sane :: proc "contextless" (v: f64) -> f64 { + if v != v { return 0 } + return clamp(v, -RASTER_COORD_MAX, RASTER_COORD_MAX) +} + +// raster_floor_clamped and raster_ceil_clamped convert a bound to a pixel +// index without ever letting a huge or non-finite f64 reach the integer +// conversion, which is undefined when the value does not fit. A NaN bound +// widens to the clip rather than narrowing to nothing: dropping content is a +// worse failure than drawing too much. +@(private) +raster_floor_clamped :: proc "contextless" (v: f64, lo, hi: int) -> int { + if v != v || v <= f64(lo) { return lo } + if v >= f64(hi) { return hi } + return int(math.floor(v)) +} + +@(private) +raster_ceil_clamped :: proc "contextless" (v: f64, lo, hi: int) -> int { + if v != v || v >= f64(hi) { return hi } + if v <= f64(lo) { return lo } + return int(math.ceil(v)) +} + +// --------------------------------------------------------------------------- +// Accumulation +// --------------------------------------------------------------------------- + +@(private) +raster_cell :: proc "contextless" (r: ^Raster_State, x: f64) -> int { + if !(x > r.fx0) { return r.rx0 } + if x >= r.fx1 { return r.rx1 } + return int(math.floor(x)) +} + +// raster_accumulate deposits one piece of one edge -- already confined to a +// single scanline -- into the cells it crosses. +@(private) +raster_accumulate :: proc(r: ^Raster_State, xa, ya, xb, yb, dir: f64) { + if !(yb > ya) { return } + + ex1 := raster_cell(r, xa) + ex2 := raster_cell(r, xb) + + if ex1 == ex2 { + raster_cell_add(r, ex1, (yb - ya) * dir, xa, xb) + return + } + + // dy per unit x. Both endpoints sit inside the raster box, so the number + // of cells crossed is bounded by its width. + dydx := (yb - ya) / (xb - xa) + step := xb > xa ? 1 : -1 + ex := ex1 + xc := xa + yc := ya + for ex != ex2 { + xn := f64(step > 0 ? ex + 1 : ex) + yn := ya + (xn - xa) * dydx + if !(yn > yc) { yn = yc } + if yn > yb { yn = yb } + raster_cell_add(r, ex, (yn - yc) * dir, xc, xn) + xc = xn + yc = yn + ex += step + } + raster_cell_add(r, ex, (yb - yc) * dir, xc, xb) +} + +// raster_cell_add records `dy` of signed height crossing cell `ex` between the +// x offsets `x_from` and `x_to`. `area` holds twice the trapezoid on the left +// of the edge, which the sweep subtracts from the running cover. +@(private) +raster_cell_add :: proc "contextless" (r: ^Raster_State, ex: int, dy, x_from, x_to: f64) { + if dy == 0 { return } + i := ex - r.rx0 + if i < 0 || i >= len(r.cover) { return } + fx := f64(ex) + r.cover[i] += dy + r.area [i] += dy * ((x_from - fx) + (x_to - fx)) + r.min_x = min(r.min_x, ex) + r.max_x = max(r.max_x, ex) +} + +// --------------------------------------------------------------------------- +// Sweep +// --------------------------------------------------------------------------- + +// raster_coverage folds a winding-weighted coverage into a 0..255 alpha. +// +// Nonzero clamps the magnitude; even-odd folds it, which is the floating-point +// spelling of the usual integer trick of masking the accumulator to nine bits +// and mirroring everything above the halfway point. Even-odd is not a corner +// case: it is what `f*` and `B*` mean, and Type1 glyph counters depend on it. +@(private) +raster_coverage :: proc "contextless" (cov: f64, even_odd: bool) -> u8 { + a := abs(cov) + if even_odd { + a -= 2 * math.floor(a * 0.5) + if a > 1 { a = 2 - a } + } else if a > 1 { + a = 1 + } + // Written as a positive test so a NaN that survived every other guard + // becomes transparent rather than an out-of-range conversion. + if !(a > 0) { return 0 } + v := int(a * 255 + 0.5) + return u8(clamp(v, 0, 255)) +} + +@(private) +raster_sweep :: proc(r: ^Raster_State, y: int, even_odd: bool, span: Span_Proc, user: rawptr) { + if r.min_x > r.max_x { return } + + lo := r.min_x + // The sweep stops at the last cell that any edge touched, and never needs + // a tail: a closed path's windings sum to zero at the far right, and the + // ones that would have closed beyond the clip were moved onto the guard + // cell rather than dropped, so `running` is back at zero by the time the + // touched range ends. The guard cell itself is one past the last emittable + // pixel, which is why `hi` is capped rather than clamped away. + hi := min(r.max_x, r.rx1 - 1) + if lo > hi { return } + + end := hi + 1 + running := 0.0 + for i in lo ..= hi { + j := i - r.rx0 + c := running + r.cover[j] - r.area[j] * 0.5 + running += r.cover[j] + r.covbuf[j] = raster_coverage(c, even_odd) + } + + i := lo + for i < end { + if r.covbuf[i - r.rx0] == 0 { + i += 1 + continue + } + j := i + for j < end && r.covbuf[j - r.rx0] != 0 { j += 1 } + span(user, y, i, r.covbuf[i - r.rx0 : j - r.rx0]) + i = j + } +} diff --git a/raster_test.odin b/raster_test.odin new file mode 100644 index 0000000..d61dd2f --- /dev/null +++ b/raster_test.odin @@ -0,0 +1,770 @@ +package pdf + +import "core:math" +import "core:mem" +import "core:testing" +import "core:time" + +// --------------------------------------------------------------------------- +// Span recorder +// +// Every test drives the rasteriser through this, so the ordering and bounds +// contract -- increasing y, increasing x within a row, no overlapping runs, +// nothing outside the clip -- is checked by every case rather than by one +// dedicated test that the others could quietly violate. +// --------------------------------------------------------------------------- + +@(private) +Raster_Recorder :: struct { + clip: Raster_Clip, + w, h: int, + grid: []u8, + n_spans: int, + total: int, + ordered: bool, + in_bounds: bool, + disjoint: bool, + nonempty: bool, + nonzero: bool, + last_y: int, + last_end: int, +} + +@(private) +raster_rec_make :: proc(clip: Raster_Clip) -> Raster_Recorder { + w := clip.x1 - clip.x0 + h := clip.y1 - clip.y0 + return Raster_Recorder{ + clip = clip, + w = w, + h = h, + grid = make([]u8, max(w * h, 1)), + ordered = true, + in_bounds = true, + disjoint = true, + nonempty = true, + nonzero = true, + last_y = min(int), + last_end = min(int), + } +} + +@(private) +raster_rec_destroy :: proc(rec: ^Raster_Recorder) { + delete(rec.grid) + rec.grid = nil +} + +@(private) +raster_rec_at :: proc(rec: ^Raster_Recorder, x, y: int) -> u8 { + ix := x - rec.clip.x0 + iy := y - rec.clip.y0 + if ix < 0 || iy < 0 || ix >= rec.w || iy >= rec.h { return 0 } + return rec.grid[iy * rec.w + ix] +} + +@(private) +raster_rec_span :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + rec := (^Raster_Recorder)(user) + rec.n_spans += 1 + + if len(coverage) == 0 { + rec.nonempty = false + return + } + if y < rec.last_y || (y == rec.last_y && x0 < rec.last_end) { + rec.ordered = false + } + rec.last_y = y + rec.last_end = x0 + len(coverage) + + for c, i in coverage { + x := x0 + i + if c == 0 { rec.nonzero = false } + rec.total += int(c) + ix := x - rec.clip.x0 + iy := y - rec.clip.y0 + if ix < 0 || iy < 0 || ix >= rec.w || iy >= rec.h { + rec.in_bounds = false + continue + } + if rec.grid[iy * rec.w + ix] != 0 { rec.disjoint = false } + rec.grid[iy * rec.w + ix] = c + } +} + +@(private) +raster_rec_check :: proc(t: ^testing.T, rec: ^Raster_Recorder, loc := #caller_location) { + testing.expect(t, rec.ordered, "spans must arrive in increasing y then x", loc = loc) + testing.expect(t, rec.in_bounds, "spans must lie inside the clip", loc = loc) + testing.expect(t, rec.disjoint, "spans must not overlap", loc = loc) + testing.expect(t, rec.nonempty, "spans must not be empty", loc = loc) + testing.expect(t, rec.nonzero, "spans must hold no zero coverage", loc = loc) +} + +// Raster_Counter is the recorder for cases whose clip is far too large to +// hold a grid for. +@(private) +Raster_Counter :: struct { + spans: int, + total: int, +} + +@(private) +raster_count_span :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { + c := (^Raster_Counter)(user) + c.spans += 1 + for v in coverage { c.total += int(v) } +} + +@(private) +raster_test_add :: proc(fp: ^Flat_Path, closed: bool, pts: []Point) { + start := len(fp.points) + append(&fp.points, ..pts) + append(&fp.subpaths, Flat_Subpath{start = start, count = len(pts), closed = closed}) +} + +// raster_test_run fills one polygon and returns the recorder, already checked +// for the structural invariants. +@(private) +raster_test_run :: proc(t: ^testing.T, pts: []Point, even_odd: bool, clip: Raster_Clip, + loc := #caller_location) -> Raster_Recorder { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, pts) + + rec := raster_rec_make(clip) + err := raster_fill(&fp, even_odd, clip, raster_rec_span, &rec) + testing.expect_value(t, err, Error.None, loc = loc) + raster_rec_check(t, &rec, loc = loc) + return rec +} + +// --------------------------------------------------------------------------- +// Exactness +// --------------------------------------------------------------------------- + +// A pixel-aligned square must come out with no fringe at all: every interior +// pixel fully on, every pixel outside it untouched. Anti-aliasing that leaks +// half a pixel of grey along a table rule is the single most visible way to +// get this wrong. +@(test) +test_raster_square_is_exact :: proc(t: ^testing.T) { + pts := [][2]f64{{2, 3}, {12, 3}, {12, 13}, {2, 13}} + rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 16, 16}) + defer raster_rec_destroy(&rec) + + bad := 0 + for y in 0 ..< 16 { + for x in 0 ..< 16 { + want: u8 = (x >= 2 && x < 12 && y >= 3 && y < 13) ? 255 : 0 + if raster_rec_at(&rec, x, y) != want { bad += 1 } + } + } + testing.expect_value(t, bad, 0) + // Ten full rows, one run each. + testing.expect_value(t, rec.n_spans, 10) + testing.expect_value(t, rec.total, 100 * 255) +} + +// Half a pixel of coverage is half of 255. The tolerance is two levels, not +// because the maths is approximate -- it is exact -- but so the assertion is +// about the value and not about a rounding convention. +@(test) +test_raster_half_covered_pixel :: proc(t: ^testing.T) { + pts := [][2]f64{{0, 0}, {0.5, 0}, {0.5, 1}, {0, 1}} + rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 4, 4}) + defer raster_rec_destroy(&rec) + + got := int(raster_rec_at(&rec, 0, 0)) + testing.expectf(t, abs(got - 128) <= 2, "half-covered pixel gave %d, want 128", got) + testing.expect_value(t, raster_rec_at(&rec, 1, 0), u8(0)) + testing.expect_value(t, raster_rec_at(&rec, 0, 1), u8(0)) +} + +// A rectangle on fractional boundaries has a closed-form coverage for every +// pixel it touches: the product of the overlaps in x and in y. Matching it to +// one level proves the coverage really is analytic in BOTH axes -- a +// sub-scanline sampler would quantise the y factor and fail this. +@(test) +test_raster_fractional_rect_is_analytic :: proc(t: ^testing.T) { + RX0 :: 1.25 + RY0 :: 0.5 + RX1 :: 4.75 + RY1 :: 3.125 + + pts := [][2]f64{{RX0, RY0}, {RX1, RY0}, {RX1, RY1}, {RX0, RY1}} + rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 8, 8}) + defer raster_rec_destroy(&rec) + + worst := 0 + for y in 0 ..< 8 { + for x in 0 ..< 8 { + ox := max(0.0, min(f64(x + 1), RX1) - max(f64(x), RX0)) + oy := max(0.0, min(f64(y + 1), RY1) - max(f64(y), RY0)) + want := int(ox * oy * 255 + 0.5) + got := int(raster_rec_at(&rec, x, y)) + worst = max(worst, abs(got - want)) + } + } + testing.expectf(t, worst <= 1, "worst analytic coverage error was %d levels", worst) +} + +// Total coverage is the polygon's area. Shoelace gives the answer to compare +// against, and a fill that is a fraction of a pixel out anywhere -- a +// half-open range applied to the wrong side, a dropped last scanline -- misses +// it by far more than the quantisation slack. +@(test) +test_raster_triangle_area :: proc(t: ^testing.T) { + pts := [][2]f64{{1.3, 1.7}, {9.1, 2.4}, {4.2, 8.6}} + area := 0.5 * abs( + pts[0].x * (pts[1].y - pts[2].y) + + pts[1].x * (pts[2].y - pts[0].y) + + pts[2].x * (pts[0].y - pts[1].y)) + + for even_odd in ([2]bool{false, true}) { + rec := raster_test_run(t, pts[:], even_odd, Raster_Clip{0, 0, 12, 12}) + defer raster_rec_destroy(&rec) + got := f64(rec.total) / 255 + testing.expectf(t, abs(got - area) < 0.15, + "triangle covered %f pixels, analytic area is %f (even_odd=%v)", got, area, even_odd) + } +} + +// --------------------------------------------------------------------------- +// Fill rules +// --------------------------------------------------------------------------- + +// Two concentric squares, wound the same way and wound oppositely, under both +// rules. Three of the four cases punch a hole; only same-winding nonzero fills +// the middle. A rasteriser that ignores the fill rule, or that takes the +// absolute value of the winding before folding it, agrees in all four. +@(test) +test_raster_fill_rules_concentric :: proc(t: ^testing.T) { + outer := [][2]f64{{0, 0}, {10, 0}, {10, 10}, {0, 10}} + same := [][2]f64{{3, 3}, {7, 3}, {7, 7}, {3, 7}} + opp := [][2]f64{{3, 3}, {3, 7}, {7, 7}, {7, 3}} + + Case :: struct { inner: [][2]f64, even_odd: bool, middle: u8 } + cases := []Case{ + {same, false, 255}, // winding 2 -> filled + {same, true, 0}, // winding 2 -> folded away + {opp, false, 0}, // winding 0 -> hole + {opp, true, 0}, // winding 0 -> hole + } + + for c, i in cases { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, outer) + raster_test_add(&fp, true, c.inner) + + rec := raster_rec_make(Raster_Clip{0, 0, 12, 12}) + defer raster_rec_destroy(&rec) + testing.expect_value(t, raster_fill(&fp, c.even_odd, rec.clip, raster_rec_span, &rec), Error.None) + raster_rec_check(t, &rec) + + testing.expectf(t, raster_rec_at(&rec, 5, 5) == c.middle, + "case %d: middle pixel was %d, want %d", i, raster_rec_at(&rec, 5, 5), c.middle) + // The ring between the squares is wound once either way, so it is + // solid in every case; that is what makes the middle the only thing + // the fill rule changes. + testing.expectf(t, raster_rec_at(&rec, 1, 1) == 255, + "case %d: ring pixel was %d, want 255", i, raster_rec_at(&rec, 1, 1)) + } +} + +// The same square traced twice in the same direction winds to 2. Nonzero must +// clamp that to solid, not wrap it; even-odd must fold it to nothing. +@(test) +test_raster_double_wound_square :: proc(t: ^testing.T) { + sq := [][2]f64{{1, 1}, {6, 1}, {6, 6}, {1, 6}} + + for even_odd in ([2]bool{false, true}) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, sq) + raster_test_add(&fp, true, sq) + + rec := raster_rec_make(Raster_Clip{0, 0, 8, 8}) + defer raster_rec_destroy(&rec) + testing.expect_value(t, raster_fill(&fp, even_odd, rec.clip, raster_rec_span, &rec), Error.None) + raster_rec_check(t, &rec) + + want := 25 * 255 if !even_odd else 0 + testing.expectf(t, rec.total == want, + "double-wound square totalled %d, want %d (even_odd=%v)", rec.total, want, even_odd) + } +} + +// A self-intersecting figure of eight. The two lobes wind +1 and -1, so both +// rules fill both of them and the total is the sum of the two triangles -- +// 25 + 25. A sign error in the winding, or a clamp applied before the fold, +// loses one lobe entirely. +@(test) +test_raster_figure_of_eight :: proc(t: ^testing.T) { + pts := [][2]f64{{0, 0}, {10, 0}, {0, 10}, {10, 10}} + + for even_odd in ([2]bool{false, true}) { + rec := raster_test_run(t, pts[:], even_odd, Raster_Clip{0, 0, 12, 12}) + defer raster_rec_destroy(&rec) + got := f64(rec.total) / 255 + testing.expectf(t, abs(got - 50) < 0.5, + "figure of eight covered %f pixels, want 50 (even_odd=%v)", got, even_odd) + // The crossing is at (5,5); both lobes must be present. + testing.expectf(t, raster_rec_at(&rec, 5, 1) == 255 && raster_rec_at(&rec, 5, 8) == 255, + "both lobes must be solid (even_odd=%v)", even_odd) + } +} + +// --------------------------------------------------------------------------- +// Degenerate input +// --------------------------------------------------------------------------- + +@(test) +test_raster_degenerate_paths :: proc(t: ^testing.T) { + clip := Raster_Clip{0, 0, 8, 8} + + Case :: struct { name: string, pts: [][2]f64 } + cases := []Case{ + {"empty", {}}, + {"single point", {{2, 2}}}, + {"two equal points", {{2, 2}, {2, 2}}}, + {"horizontal", {{1, 4}, {7, 4}}}, + {"three collinear", {{1, 4}, {4, 4}, {7, 4}}}, + {"vertical", {{4, 1}, {4, 7}}}, + {"zero-area square", {{3, 3}, {3, 3}, {3, 3}, {3, 3}}}, + } + + for c in cases { + fp := flat_path_make() + defer flat_path_destroy(&fp) + if len(c.pts) > 0 { raster_test_add(&fp, false, c.pts) } + + rec := raster_rec_make(clip) + defer raster_rec_destroy(&rec) + err := raster_fill(&fp, false, clip, raster_rec_span, &rec) + testing.expectf(t, err == .None, "%s returned %v", c.name, err) + testing.expectf(t, rec.n_spans == 0, "%s emitted %d spans", c.name, rec.n_spans) + } +} + +// A path outside the clip costs nothing and paints nothing, whichever side it +// is on. +@(test) +test_raster_outside_clip :: proc(t: ^testing.T) { + clip := Raster_Clip{10, 10, 20, 20} + offsets := [][2]f64{{-40, 0}, {40, 0}, {0, -40}, {0, 40}, {40, 40}} + + for off in offsets { + pts := [][2]f64{ + {12 + off.x, 12 + off.y}, {18 + off.x, 12 + off.y}, + {18 + off.x, 18 + off.y}, {12 + off.x, 18 + off.y}, + } + rec := raster_test_run(t, pts[:], false, clip) + defer raster_rec_destroy(&rec) + testing.expectf(t, rec.n_spans == 0, "offset %v emitted %d spans", off, rec.n_spans) + } +} + +// A shape larger than the clip is bounded by it exactly: the clip fills solid +// and nothing is reported outside. This is the case that decides whether the +// left-hand winding of a clipped-away edge survives -- drop it and the whole +// region comes out empty. +@(test) +test_raster_clips_oversized_shape :: proc(t: ^testing.T) { + clip := Raster_Clip{10, 20, 40, 45} + pts := [][2]f64{{-500, -500}, {500, -500}, {500, 500}, {-500, 500}} + rec := raster_test_run(t, pts[:], false, clip) + defer raster_rec_destroy(&rec) + + testing.expect_value(t, rec.n_spans, 25) + testing.expect_value(t, rec.total, 30 * 25 * 255) +} + +@(test) +test_raster_rejects_nil_span :: proc(t: ^testing.T) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, [][2]f64{{0, 0}, {4, 0}, {4, 4}}) + testing.expect_value(t, raster_fill(&fp, false, Raster_Clip{0, 0, 8, 8}, nil, nil), Error.Invalid_Argument) +} + +@(test) +test_raster_empty_clip :: proc(t: ^testing.T) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, [][2]f64{{0, 0}, {4, 0}, {4, 4}}) + + for clip in ([]Raster_Clip{{0, 0, 0, 8}, {0, 0, 8, 0}, {5, 0, 2, 8}, {0, 5, 8, 2}}) { + rec := raster_rec_make(Raster_Clip{0, 0, 8, 8}) + defer raster_rec_destroy(&rec) + testing.expect_value(t, raster_fill(&fp, false, clip, raster_rec_span, &rec), Error.None) + testing.expect_value(t, rec.n_spans, 0) + } +} + +// An unclosed subpath fills as though it were closed: that is what `f` means, +// and a triangle drawn with two lines and no `h` is ordinary content. +@(test) +test_raster_closes_subpaths_implicitly :: proc(t: ^testing.T) { + pts := [][2]f64{{1, 1}, {9, 1}, {9, 9}, {1, 9}} + + fp_open := flat_path_make() + defer flat_path_destroy(&fp_open) + raster_test_add(&fp_open, false, pts) + + fp_closed := flat_path_make() + defer flat_path_destroy(&fp_closed) + raster_test_add(&fp_closed, true, pts) + + a := raster_rec_make(Raster_Clip{0, 0, 12, 12}) + defer raster_rec_destroy(&a) + b := raster_rec_make(Raster_Clip{0, 0, 12, 12}) + defer raster_rec_destroy(&b) + + testing.expect_value(t, raster_fill(&fp_open, false, a.clip, raster_rec_span, &a), Error.None) + testing.expect_value(t, raster_fill(&fp_closed, false, b.clip, raster_rec_span, &b), Error.None) + + testing.expect_value(t, a.total, 64 * 255) + testing.expect_value(t, a.total, b.total) + testing.expect_value(t, a.n_spans, b.n_spans) +} + +// --------------------------------------------------------------------------- +// Robustness +// --------------------------------------------------------------------------- + +// Coordinates like 1e9 are ordinary in content streams -- a "clip to +// everything" rectangle usually is one. The cost must follow the clip, not the +// path, so this is timed as well as checked: a rasteriser that iterates the +// path's scanlines instead of the clip's runs for hours here. +@(test) +test_raster_huge_coordinates_terminate :: proc(t: ^testing.T) { + clip := Raster_Clip{0, 0, 100, 100} + B :: 1e9 + + Case :: struct { name: string, pts: [][2]f64, want_total: int } + cases := []Case{ + {"covers clip", {{-B, -B}, {B, -B}, {B, B}, {-B, B}}, 100 * 100 * 255}, + {"tall spike", {{-B, -B}, {B, -B}, {0, B}}, 100 * 100 * 255}, + {"far away", {{B, B}, {B + 10, B}, {B + 10, B + 10}}, 0}, + {"one huge vertex", {{10, 10}, {B, 20}, {10, 30}}, -1}, + } + + for c in cases { + start := time.now() + rec := raster_test_run(t, c.pts, false, clip) + defer raster_rec_destroy(&rec) + ms := time.duration_milliseconds(time.since(start)) + + testing.expectf(t, ms < 250, "%s took %f ms", c.name, ms) + if c.want_total >= 0 { + testing.expectf(t, rec.total == c.want_total, + "%s totalled %d, want %d", c.name, rec.total, c.want_total) + } + } + +} + +// The mirror image of the case above: a small path inside an enormous clip. +// The cost has to follow the path there, so nothing may be allocated or +// iterated per row of the clip -- a million-row clip would otherwise mean a +// million-entry scanline table before a single pixel was touched. +@(test) +test_raster_small_path_in_huge_clip :: proc(t: ^testing.T) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_add(&fp, true, [][2]f64{{500000, 400000}, {500010, 400000}, {500010, 400010}, {500000, 400010}}) + + count := Raster_Counter{} + start := time.now() + err := raster_fill(&fp, false, Raster_Clip{0, 0, 1000000, 1000000}, raster_count_span, &count) + ms := time.duration_milliseconds(time.since(start)) + + testing.expect_value(t, err, Error.None) + testing.expectf(t, ms < 250, "small path in a huge clip took %f ms", ms) + testing.expect_value(t, count.spans, 10) + testing.expect_value(t, count.total, 100 * 255) +} + +// raster_test_measure fills through a tracking allocator and reports what the +// fill cost in allocations and in bytes. +@(private) +raster_test_measure :: proc(t: ^testing.T, fp: ^Flat_Path, clip: Raster_Clip, + loc := #caller_location) -> (calls: int, bytes: int) { + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + defer mem.tracking_allocator_destroy(&track) + + count := Raster_Counter{} + err := raster_fill(fp, false, clip, raster_count_span, &count, mem.tracking_allocator(&track)) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expectf(t, len(track.allocation_map) == 0, + "raster_fill leaked %d allocations", len(track.allocation_map), loc = loc) + return int(track.total_allocation_count), int(track.total_memory_allocated) +} + +// Two things at once, both of them contract rather than taste. +// +// Nothing is allocated per scanline: a path 400 rows tall must cost the same +// number of allocations as one 4 rows tall. +// +// And the working set follows the path rather than the clip: the same small +// path against a clip a thousand times taller must not allocate a byte more, +// which is what stops a 300 dpi page from building a scanline table for rows +// no edge will ever reach. +@(test) +test_raster_allocation_is_bounded :: proc(t: ^testing.T) { + short := flat_path_make() + defer flat_path_destroy(&short) + raster_test_add(&short, true, [][2]f64{{1, 1}, {5, 1}, {5, 5}, {1, 5}}) + + tall := flat_path_make() + defer flat_path_destroy(&tall) + raster_test_add(&tall, true, [][2]f64{{1, 1}, {5, 1}, {5, 401}, {1, 401}}) + + short_calls, short_bytes := raster_test_measure(t, &short, Raster_Clip{0, 0, 8, 8}) + tall_calls, _ := raster_test_measure(t, &tall, Raster_Clip{0, 0, 8, 512}) + huge_calls, huge_bytes := raster_test_measure(t, &short, Raster_Clip{0, 0, 8, 100000}) + + testing.expectf(t, short_calls <= 8, "a fill took %d allocations", short_calls) + testing.expectf(t, tall_calls == short_calls, + "a path 100x taller took %d allocations against %d", tall_calls, short_calls) + testing.expectf(t, huge_calls == short_calls && huge_bytes == short_bytes, + "a clip 12000x taller cost %d allocations / %d bytes against %d / %d", + huge_calls, huge_bytes, short_calls, short_bytes) +} + +// path.odin filters non-finite coordinates on the way in, but a Flat_Path can +// also come from a transform that overflowed, so the rasteriser guards them +// itself rather than trusting its caller. +// +// The guard clamps rather than discards, and that distinction is the whole +// assertion here: dropping a segment because one end is infinite breaks the +// polygon open and loses everything the remaining edges were enclosing, while +// pulling that end back to the edge of the world keeps it closed and fills +// what the geometry meant. An infinite rectangle fills to the wall. +@(test) +test_raster_non_finite_coordinates :: proc(t: ^testing.T) { + nan := math.nan_f64() + inf := math.INF_F64 + clip := Raster_Clip{0, 0, 8, 8} + + open_right := raster_test_run(t, [][2]f64{{2, 2}, {inf, 2}, {inf, 6}, {2, 6}}, false, clip) + defer raster_rec_destroy(&open_right) + testing.expect_value(t, open_right.total, 6 * 4 * 255) + + open_left := raster_test_run(t, [][2]f64{{-inf, 2}, {6, 2}, {6, 6}, {-inf, 6}}, false, clip) + defer raster_rec_destroy(&open_left) + testing.expect_value(t, open_left.total, 6 * 4 * 255) + + // The rest have no defensible answer; the contract for them is only that + // the rasteriser returns, stays inside the clip, and keeps its spans in + // order -- all of which raster_test_run checks. + paths := [][][2]f64{ + {{nan, 0}, {8, 0}, {8, 8}}, + {{0, nan}, {8, 0}, {8, 8}}, + {{inf, inf}, {-inf, inf}, {0, -inf}}, + {{nan, nan}, {nan, nan}, {nan, nan}}, + {{1, 1}, {7, nan}, {7, 7}, {nan, 7}}, + } + for pts in paths { + rec := raster_test_run(t, pts, false, clip) + defer raster_rec_destroy(&rec) + testing.expect(t, rec.total >= 0) + } +} + +// raster_test_tangle fills `n` points of a dense self-crossing scribble into +// `fp`, all of it inside a 64-pixel box. +@(private) +raster_test_tangle :: proc(fp: ^Flat_Path, n: int, ry: f64) { + start := len(fp.points) + for i in 0 ..< n { + a := f64(i) * 0.31 + append(&fp.points, Point{32 + 30 * math.cos(a), 32 + ry * math.sin(a * 1.7)}) + } + append(&fp.subpaths, Flat_Subpath{start = start, count = n, closed = true}) +} + +// A dense self-crossing scribble: thousands of edges live on every scanline at +// once and the winding runs deep, which is where an active-edge list that +// retires an edge one row early or late shows itself. +@(test) +test_raster_many_segments :: proc(t: ^testing.T) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + raster_test_tangle(&fp, 20000, 30) + + rec := raster_rec_make(Raster_Clip{0, 0, 64, 64}) + defer raster_rec_destroy(&rec) + testing.expect_value(t, raster_fill(&fp, true, rec.clip, raster_rec_span, &rec), Error.None) + raster_rec_check(t, &rec) + testing.expect(t, rec.total > 0) +} + +// Past MAX_RASTER_EDGES the edge table stops growing and the rest of the path +// is dropped. The shape that comes out is wrong -- that is the point of a cap +// -- but it still comes out, in order, inside the clip, without the table +// having grown to whatever a hostile content stream asked for. Uncapped, the +// same path allocates several hundred megabytes. +@(test) +test_raster_edge_cap :: proc(t: ^testing.T) { + fp := flat_path_make() + defer flat_path_destroy(&fp) + // Kept to a couple of scanlines so the cap is what the test measures + // rather than the time it takes to sweep a million tall edges. + raster_test_tangle(&fp, MAX_RASTER_EDGES + (MAX_RASTER_EDGES / 2), 1) + + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + defer mem.tracking_allocator_destroy(&track) + + rec := raster_rec_make(Raster_Clip{0, 0, 64, 64}) + defer raster_rec_destroy(&rec) + err := raster_fill(&fp, true, rec.clip, raster_rec_span, &rec, mem.tracking_allocator(&track)) + + testing.expect_value(t, err, Error.None) + raster_rec_check(t, &rec) + testing.expect_value(t, len(track.allocation_map), 0) + + peak := int(track.peak_memory_allocated) + testing.expectf(t, peak < 128 * 1024 * 1024, + "the edge table peaked at %d bytes", peak) +} + +// --------------------------------------------------------------------------- +// Clip windowing +// --------------------------------------------------------------------------- + +// Coverage is a property of the path, not of the clip: narrowing the clip must +// reproduce the wider fill exactly over the window they share. This is what +// lets a device split a page into bands. +@(test) +test_raster_clip_window_matches :: proc(t: ^testing.T) { + pts := [][2]f64{{1.4, 2.2}, {17.6, 4.1}, {14.2, 18.9}, {3.3, 15.5}} + + full := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 24, 24}) + defer raster_rec_destroy(&full) + + win := Raster_Clip{6, 7, 13, 16} + part := raster_test_run(t, pts[:], false, win) + defer raster_rec_destroy(&part) + + bad := 0 + for y in win.y0 ..< win.y1 { + for x in win.x0 ..< win.x1 { + if raster_rec_at(&full, x, y) != raster_rec_at(&part, x, y) { bad += 1 } + } + } + testing.expect_value(t, bad, 0) +} + +// --------------------------------------------------------------------------- +// Fuzz +// --------------------------------------------------------------------------- + +@(private) +raster_test_rand :: proc(state: ^u64) -> u64 { + // xorshift64*, so the corpus is the same on every machine and a failure + // can be reproduced from the seed alone. + x := state^ + x ~= x >> 12 + x ~= x << 25 + x ~= x >> 27 + state^ = x + return x * 0x2545_f491_4f6c_dd1d +} + +@(private) +raster_test_randf :: proc(state: ^u64, lo, hi: f64) -> f64 { + u := f64(raster_test_rand(state) >> 11) / f64(u64(1) << 53) + return lo + (hi - lo) * u +} + +// raster_test_inside is a deliberately naive reference: cast a ray and count +// signed crossings. Slow, obviously correct, and shares no code with the +// rasteriser, which is the whole point of comparing against it. +@(private) +raster_test_inside :: proc(pts: [][2]f64, px, py: f64, even_odd: bool) -> bool { + w := 0 + for i in 0 ..< len(pts) { + a := pts[i] + b := pts[(i + 1) % len(pts)] + if (a.y <= py) == (b.y <= py) { continue } + tx := a.x + (b.x - a.x) * (py - a.y) / (b.y - a.y) + if tx <= px { continue } + w += b.y > a.y ? 1 : -1 + } + return even_odd ? (w & 1) != 0 : w != 0 +} + +// Random polygons, checked two ways: the structural contract on every span, +// and -- for every pixel the reference says is unambiguously inside or +// unambiguously outside at 12x12 samples -- that the coverage agrees. A +// half-pixel offset, an inverted fill rule or a lost winding all show up as an +// outside pixel that came back painted. +@(test) +test_raster_fuzz_polygons :: proc(t: ^testing.T) { + SAMPLES :: 12 + clip := Raster_Clip{0, 0, 24, 24} + seed: u64 = 0x9e37_79b9_7f4a_7c15 + + for iter in 0 ..< 40 { + n := 3 + int(raster_test_rand(&seed) % 8) + pts := make([][2]f64, n) + defer delete(pts) + for &p in pts { + p = Point{raster_test_randf(&seed, -8, 32), raster_test_randf(&seed, -8, 32)} + } + even_odd := (raster_test_rand(&seed) & 1) != 0 + + rec := raster_test_run(t, pts, even_odd, clip) + defer raster_rec_destroy(&rec) + + testing.expectf(t, rec.total <= 24 * 24 * 255, + "iter %d: total coverage %d exceeds the clip", iter, rec.total) + + for y in 0 ..< 24 { + for x in 0 ..< 24 { + n_in := 0 + for sy in 0 ..< SAMPLES { + for sx in 0 ..< SAMPLES { + px := f64(x) + (f64(sx) + 0.5) / SAMPLES + py := f64(y) + (f64(sy) + 0.5) / SAMPLES + if raster_test_inside(pts, px, py, even_odd) { n_in += 1 } + } + } + got := int(raster_rec_at(&rec, x, y)) + if n_in == SAMPLES * SAMPLES { + testing.expectf(t, got >= 210, + "iter %d: pixel %d,%d is solidly inside but covered %d", iter, x, y, got) + } else if n_in == 0 { + testing.expectf(t, got <= 45, + "iter %d: pixel %d,%d is solidly outside but covered %d", iter, x, y, got) + } + } + } + } +} + +// The same corpus, with the clip offset away from the origin and the polygons +// straddling every wall, to exercise the guard cell and the left-hand winding +// substitution rather than the interior. +@(test) +test_raster_fuzz_straddling_clip :: proc(t: ^testing.T) { + clip := Raster_Clip{7, 11, 23, 29} + seed: u64 = 0xdead_beef_1234_5678 + + for _ in 0 ..< 120 { + n := 3 + int(raster_test_rand(&seed) % 6) + pts := make([][2]f64, n) + defer delete(pts) + for &p in pts { + p = Point{raster_test_randf(&seed, -200, 200), raster_test_randf(&seed, -200, 200)} + } + rec := raster_test_run(t, pts, (raster_test_rand(&seed) & 1) != 0, clip) + defer raster_rec_destroy(&rec) + } +} diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..b09bc83 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Type-check the library and every example with vetting on. +set -euo pipefail +cd "$(dirname "$0")/.." +echo "== pdf ==" +odin check . -no-entry-point -vet-unused -vet-shadowing -vet-using-stmt -warnings-as-errors +for d in examples/*/; do + [ -f "$d/main.odin" ] || continue + echo "== $d ==" + odin check "$d" -vet-unused -vet-shadowing -warnings-as-errors +done +echo "OK" diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..4a7d193 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the test suite. Set PDF_ODIN_CORPUS to a directory of real-world PDFs to +# additionally run the corpus tests; without it those skip. +set -euo pipefail +cd "$(dirname "$0")/.." +odin test . -define:ODIN_TEST_FANCY=false "$@" diff --git a/shading.odin b/shading.odin new file mode 100644 index 0000000..27f59fd --- /dev/null +++ b/shading.odin @@ -0,0 +1,1532 @@ +package pdf + +import "core:math" + +// Shadings (ISO 32000-2, 8.7.4.5). Types 1-3 are evaluated per pixel from a +// function; types 4-7 are meshes that are decomposed into triangles. +// +// HOW A SHADING REACHES THE PIXELS +// +// Everything here paints through `blend_span_rgba8`, one horizontal run at a +// time, in straight RGBA8 -- the same road an image takes, so a shading, an +// image and a fill all round identically and a page cannot show a seam where +// one meets another. Runs are assembled in `Shade_Ctx.row`, one scratch row as +// wide as the clip, and a pixel the shading does not cover is left with alpha +// zero and skipped rather than painted transparent, because "not painted" and +// "painted with alpha 0" differ under every blend mode but Normal. +// +// Types 1-3 walk the clip box row by row, mapping each pixel centre back +// through the inverse CTM into the shading's own space. Types 4-7 read +// triangles out of the mesh stream and scan-convert each one with a top-left +// fill rule at pixel centres. That rule is not decoration: adjacent triangles +// share an edge exactly, and a pixel centre landing on a shared edge must be +// claimed by exactly one of them or the mesh grows either a bright seam (the +// pixel painted twice, visible whenever alpha < 1) or a dark one (painted by +// neither). Triangles are drawn without anti-aliasing for the same reason -- +// two anti-aliased edges meeting would composite twice and leave a lighter +// line down every shared edge. Triangles that genuinely overlap -- which only +// a self-intersecting mesh produces -- are composited in the order the stream +// lists them, so the later one shows, which is what the format asks for; with +// a constant alpha below 1 the overlap is painted twice, and correcting that +// would mean rendering the whole shading into a buffer of its own for a case +// no real file contains. +// +// WHAT IS NOT DONE HERE +// +// /Background is parsed but never painted. It applies only when a shading is +// used as a pattern's fill, and even there only outside the shading's own +// extent; no viewer this library was checked against paints it, and painting +// it would make `sh` disagree with `scn` for the same shading dictionary. +// /AntiAlias is likewise parsed and ignored -- it is a hint, and the mesh +// rasteriser's fill rule is chosen for seam-freedom rather than smoothness. + +Shading_Kind :: enum u8 { + Function_Based = 1, + Axial = 2, + Radial = 3, + Free_Triangle = 4, + Lattice_Triangle = 5, + Coons = 6, + Tensor = 7, +} + +Shading :: struct { + kind: Shading_Kind, + cs: ^Colorspace, + // One function producing cs.n outputs, or cs.n functions each producing + // one. Both spellings are legal and both occur. + funcs: []^Function, + + // /Background, painted outside the shading's own extent when the shading + // is used as a pattern (it is ignored by the `sh` operator). + background: []f64, + has_background: bool, + // /BBox in the shading's own space. + bbox: Rect, + has_bbox: bool, + antialias: bool, + + // Type 1. + domain: [4]f64, + mtx: Matrix, + + // Types 2 and 3: /Coords is 4 numbers for axial, 6 for radial. + coords: [6]f64, + // /Domain, defaulting to [0 1]. + t0, t1: f64, + extend: [2]bool, + + // Types 4-7: the mesh stream and its sample layout. + stream: ^Stream, + bits_per_coord: int, + bits_per_component: int, + bits_per_flag: int, + decode: []f64, + // Type 5 only. + vertices_per_row: int, + + // Colour values per mesh vertex: 1 when /Function maps a parametric value, + // otherwise one per component of `cs`. It fixes the size of every vertex + // in the stream, so it is resolved once at load time. + n_comp: int, +} + +// --------------------------------------------------------------------------- +// Limits +// +// Every one of these bounds a number that came out of a file. +// --------------------------------------------------------------------------- + +// SHADE_LUT_SIZE is how finely the colour function of an axial or radial +// shading is sampled. The function is 1-D there, so sampling it once per +// entry and looking the colour up per pixel turns a type 4 PostScript +// calculator running several million times into 256 runs. 256 entries put the +// error below the 8-bit quantisation the output has anyway. +@(private) +SHADE_LUT_SIZE :: 256 + +// Vertices one mesh stream may yield. A megabyte of 16-bit vertices is already +// far past any real gradient mesh. +@(private) +MAX_MESH_VERTICES :: 1 << 20 + +// /VerticesPerRow. Two is the minimum that forms a triangle; the cap is what +// stops a four-byte integer from sizing an allocation. +@(private) +MAX_MESH_ROW :: 1 << 14 + +@(private) +MAX_MESH_TRIANGLES :: 1 << 20 + +@(private) +MAX_MESH_PATCHES :: 1 << 16 + +// Steps a patch is subdivided into along each parameter. 32 gives 2048 +// triangles for the largest patch, which is smooth well past 300 dpi. +@(private) +MAX_PATCH_SUBDIV :: 32 + +// Device pixels a patch aims to put between two subdivision steps. +@(private) +PATCH_STEP_PIXELS :: 3.0 + +// Pixels one shading may examine. This is not a quality knob -- a full page at +// 600 dpi is under 35 million -- it is the stop on a mesh built to make the +// rasteriser walk the clip a thousand times over. +@(private) +SHADE_PIXEL_FUEL :: 1 << 26 + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// shading_load builds the shading `obj` describes: a dictionary for types 1-3, +// a stream for types 4-7 (whose dictionary carries the same entries). +// +// `res` is the /Resources dictionary the shading was named from, needed only +// to resolve a colour space given as a bare name. +// +// The result is owned by the Document and must not be freed. Nothing is +// cached: unlike a colour space or a function, a shading is built once per +// `sh` operator or once per pattern, and the mesh stream it points at is +// decoded lazily by `stream_data`, which does its own caching. +shading_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (out: ^Shading, err: Error) { + if doc == nil { + return nil, .Invalid_Argument + } + o := resolve(doc, obj) + d, has_dict := to_dict(o) + if !has_dict { + return nil, .Bad_Shading + } + + st := dict_int(doc, d, "ShadingType", 0) + if st < 1 || st > 7 { + return nil, .Bad_Shading + } + + sh := new(Shading, doc.allocator) + sh.kind = Shading_Kind(u8(st)) + + cs, cs_err := colorspace_load(doc, dict_get(doc, d, "ColorSpace"), res) + // A shading whose colour space will not load cannot be guessed at: for a + // mesh the component count is what fixes the size of every vertex in the + // stream, so a wrong guess does not degrade, it garbles. + if cs_err != .None || cs == nil || cs.n < 1 || cs.n > MAX_COLOR_COMPONENTS { + return nil, .Bad_Shading + } + // 8.7.4.5.1: the colour space of a shading shall not be a Pattern space. + if cs.kind == .Pattern { + return nil, .Bad_Shading + } + sh.cs = cs + + sh.funcs = shade_load_functions(doc, dict_get(doc, d, "Function")) + // Types 1-3 have no colour of their own; without a function there is + // nothing to paint with. + if len(sh.funcs) == 0 && (sh.kind == .Function_Based || sh.kind == .Axial || sh.kind == .Radial) { + return nil, .Bad_Shading + } + + if ba := dict_array(doc, d, "Background"); array_len(ba) > 0 { + n := min(array_len(ba), cs.n) + bg := make([]f64, n, doc.allocator) + for i in 0 ..< n { + bg[i] = clamp_finite(array_num(doc, ba, i), 0) + } + sh.background = bg + sh.has_background = true + } + + if bb, bb_ok := object_to_rect(doc, dict_get(doc, d, "BBox")); bb_ok { + sh.bbox = bb + sh.has_bbox = true + } + sh.antialias = dict_bool(doc, d, "AntiAlias") + + switch sh.kind { + case .Function_Based: + shade_load_type1(doc, sh, d) + case .Axial, .Radial: + shade_load_axial_radial(doc, sh, d) or_return + case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor: + stm, is_stream := to_stream(o) + if !is_stream { + return nil, .Bad_Shading + } + sh.stream = stm + shade_load_mesh(doc, sh, d) or_return + } + return sh, .None +} + +// shade_load_functions reads /Function, which is either one function with as +// many outputs as the colour space has components or an array of that many +// one-output functions. Both spellings are legal and both occur, so the array +// is what is stored and the count is what tells them apart. +// +// The array form is INDEXED BY COMPONENT, so its length and the position of +// every entry in it are load-bearing: a sub-function that will not load holds +// its slot as nil rather than being dropped. An array in which nothing loads +// comes back empty, which for types 1-3 is what makes the shading fail. +@(private) +shade_load_functions :: proc(doc: ^Document, obj: Object) -> []^Function { + if obj == nil { + return nil + } + if a, is_array := to_array(obj); is_array { + count := min(array_len(a), MAX_COLOR_COMPONENTS) + if count == 0 { + return nil + } + out, aerr := make([]^Function, count, doc.allocator) + if aerr != nil { + return nil + } + any := false + for i in 0 ..< count { + f, err := function_load(doc, array_get(doc, a, i)) + if err != .None || f == nil { + // One broken sub-function must not lose the others, and it + // must not move them either: the slot is left nil so that + // every surviving function stays on the component the file + // wrote it for, and only the component this one would have + // supplied reads as zero. Dropping the entry instead would + // shift every later function down one index and repaint the + // whole shading in the wrong colours. + add_repair(doc, .Bad_Resource, 0) + continue + } + out[i] = f + any = true + } + if !any { + return nil + } + return out + } + f, err := function_load(doc, obj) + if err != .None || f == nil { + return nil + } + one := make([]^Function, 1, doc.allocator) + one[0] = f + return one +} + +@(private) +shade_load_type1 :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) { + sh.domain = {0, 1, 0, 1} + if da := dict_array(doc, d, "Domain"); array_len(da) >= 4 { + dom: [4]f64 + ok := true + for i in 0 ..< 4 { + dom[i] = array_num(doc, da, i) + if !is_finite(dom[i]) { + ok = false + } + } + // A reversed or non-finite /Domain is not worth failing the shading + // over; normalising it paints the same region the file meant. + if ok { + sh.domain = { + min(dom[0], dom[1]), max(dom[0], dom[1]), + min(dom[2], dom[3]), max(dom[2], dom[3]), + } + } + } + sh.mtx = dict_matrix(doc, d, "Matrix") +} + +@(private) +shade_load_axial_radial :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) -> Error { + need := sh.kind == .Axial ? 4 : 6 + ca := dict_array(doc, d, "Coords") + if array_len(ca) < need { + return .Bad_Shading + } + for i in 0 ..< need { + sh.coords[i] = array_num(doc, ca, i) + if !is_finite(sh.coords[i]) { + return .Bad_Shading + } + } + + sh.t0, sh.t1 = 0, 1 + if da := dict_array(doc, d, "Domain"); array_len(da) >= 2 { + a := array_num(doc, da, 0) + b := array_num(doc, da, 1) + if is_finite(a) && is_finite(b) { + sh.t0, sh.t1 = a, b + } + } + + if ea := dict_array(doc, d, "Extend"); array_len(ea) >= 2 { + sh.extend[0], _ = to_bool(array_get(doc, ea, 0)) + sh.extend[1], _ = to_bool(array_get(doc, ea, 1)) + } + return .None +} + +@(private) +shade_load_mesh :: proc(doc: ^Document, sh: ^Shading, d: ^Dict) -> Error { + bpc := int(dict_int(doc, d, "BitsPerCoordinate", 0)) + switch bpc { + case 1, 2, 4, 8, 12, 16, 24, 32: + sh.bits_per_coord = bpc + case: + return .Bad_Shading + } + + bpcomp := int(dict_int(doc, d, "BitsPerComponent", 0)) + switch bpcomp { + case 1, 2, 4, 8, 12, 16: + sh.bits_per_component = bpcomp + case: + return .Bad_Shading + } + + if sh.kind != .Lattice_Triangle { + bpf := int(dict_int(doc, d, "BitsPerFlag", 0)) + switch bpf { + case 2, 4, 8: + sh.bits_per_flag = bpf + case: + return .Bad_Shading + } + } + + // With a /Function the stream carries one parametric value per vertex, not + // one value per colour component (8.7.4.5.5). + sh.n_comp = len(sh.funcs) > 0 ? 1 : sh.cs.n + need := 4 + 2 * sh.n_comp + da := dict_array(doc, d, "Decode") + if array_len(da) < need { + return .Bad_Shading + } + dec := make([]f64, need, doc.allocator) + for i in 0 ..< need { + dec[i] = array_num(doc, da, i) + if !is_finite(dec[i]) { + return .Bad_Shading + } + } + sh.decode = dec + + if sh.kind == .Lattice_Triangle { + vpr := int(dict_int(doc, d, "VerticesPerRow", 0)) + if vpr < 2 || vpr > MAX_MESH_ROW { + return .Bad_Shading + } + sh.vertices_per_row = vpr + } + return .None +} + +// --------------------------------------------------------------------------- +// Colour +// --------------------------------------------------------------------------- + +// shade_components_to_rgb converts raw colour components -- the ones a mesh +// vertex carries when there is no /Function -- through the shading's space. +@(private) +shade_components_to_rgb :: proc(sh: ^Shading, comps: []f64) -> [3]f64 { + c := Color{n = sh.cs.n} + for i in 0 ..< min(sh.cs.n, len(comps)) { + c.v[i] = comps[i] + } + return colorspace_to_rgb(sh.cs, c) +} + +// shade_eval runs the shading's function(s) at `in_` and converts the result +// to RGB. `in_` is one parametric value for types 2-7 and two coordinates for +// type 1. +@(private) +shade_eval :: proc(sh: ^Shading, in_: []f64) -> [3]f64 { + comps: [MAX_COLOR_COMPONENTS]f64 + n := sh.cs.n + switch len(sh.funcs) { + case 0: + // Only reachable for a mesh whose colours come from the stream; the + // caller uses shade_components_to_rgb there. + return [3]f64{0, 0, 0} + case 1: + // One function supplying every component: the array spelling with a + // single entry means the same thing as the bare one, and both occur. + function_eval(sh.funcs[0], in_, comps[:n]) + case: + one: [1]f64 + for i in 0 ..< min(len(sh.funcs), n) { + // A nil slot is a sub-function that would not load. Its component + // stays zero; the ones around it keep their own indices. + if sh.funcs[i] == nil { + continue + } + function_eval(sh.funcs[i], in_, one[:]) + comps[i] = one[0] + } + } + return shade_components_to_rgb(sh, comps[:n]) +} + +// Shade_Lut is the colour of an axial or radial shading sampled across its +// /Domain. Entry i is the colour at t0 + (i/(N-1)) * (t1 - t0). +@(private) +Shade_Lut :: struct { + rgb: [SHADE_LUT_SIZE][3]f64, +} + +@(private) +shade_build_lut :: proc(sh: ^Shading, lut: ^Shade_Lut) { + in_: [1]f64 + for i in 0 ..< SHADE_LUT_SIZE { + s := f64(i) / f64(SHADE_LUT_SIZE - 1) + in_[0] = sh.t0 + s * (sh.t1 - sh.t0) + lut.rgb[i] = shade_eval(sh, in_[:]) + } +} + +// shade_lut_at looks a parametric position 0..1 up, rounding to the nearest +// sample so the error is half a step rather than a whole one. +@(private) +shade_lut_at :: proc(lut: ^Shade_Lut, s: f64) -> [3]f64 { + if !(s > 0) { + return lut.rgb[0] + } + if s >= 1 { + return lut.rgb[SHADE_LUT_SIZE - 1] + } + i := int(s * f64(SHADE_LUT_SIZE - 1) + 0.5) + return lut.rgb[clamp(i, 0, SHADE_LUT_SIZE - 1)] +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +// shading_bounds is the device-space region the shading can paint under `ctm`. +// +// RECT_INFINITE means "unbounded", which is the honest answer for an axial +// shading (the strip between its two ends is infinite across the axis, and an +// /Extend makes it infinite along it too) and for an extended radial one. It +// is the identity for `rect_intersect`, so a caller that intersects it with a +// clip gets the clip, which is exactly right. +shading_bounds :: proc(sh: ^Shading, ctm: Matrix) -> Rect { + if sh == nil || !matrix_is_finite(ctm) { + return RECT_EMPTY + } + + r := RECT_INFINITE + switch sh.kind { + case .Function_Based: + dom := Rect{sh.domain[0], sh.domain[2], sh.domain[1], sh.domain[3]} + r = transform_rect(matrix_mul(sh.mtx, ctm), dom) + case .Axial: + // Unbounded across the axis whatever /Extend says. + r = RECT_INFINITE + case .Radial: + if !sh.extend[0] && !sh.extend[1] { + // Every circle between the two ends is a convex combination of a + // point of the first disc and a point of the second, so the two + // discs bound the whole family. + // + // The corners are taken with min and max rather than by unioning + // two rectangles: a circle of radius zero -- which is how every + // "glow" gradient starts -- is an empty rectangle, and + // `rect_union` drops an empty operand, which would shrink the + // bound to the other circle and clip the narrow end of the cone + // away. + rr0 := max(sh.coords[2], 0) + rr1 := max(sh.coords[5], 0) + r = transform_rect(ctm, Rect{ + min(sh.coords[0] - rr0, sh.coords[3] - rr1), + min(sh.coords[1] - rr0, sh.coords[4] - rr1), + max(sh.coords[0] + rr0, sh.coords[3] + rr1), + max(sh.coords[1] + rr0, sh.coords[4] + rr1), + }) + } + case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor: + // /Decode fixes the range every coordinate in the stream decodes into, + // so the mesh is bounded without reading a byte of it. + if len(sh.decode) >= 4 { + r = transform_rect(ctm, rect_normalize( + Rect{sh.decode[0], sh.decode[2], sh.decode[1], sh.decode[3]})) + } + } + + if sh.has_bbox { + r = rect_intersect(r, transform_rect(ctm, sh.bbox)) + } + if !rect_is_finite(r) { + return RECT_INFINITE + } + return r +} + +// --------------------------------------------------------------------------- +// Painting +// --------------------------------------------------------------------------- + +@(private) +Shade_Ctx :: struct { + doc: ^Document, + sh: ^Shading, + bmp: ^Bitmap, + clip: Raster_Clip, + alpha: f64, + mode: Blend_Mode, + mask: ^Bitmap, + + ctm: Matrix, + // Device space back to the shading's own space. + inv: Matrix, + + // One scratch row of straight RGBA8, indexed from clip.x0. Alpha 0 marks a + // pixel the shading does not cover. + row: []u8, + // The mask's coverage for `mask_row_y`, one byte per pixel from clip.x0, + // or nil when there is no mask. + mask_row: []u8, + mask_row_y: int, + + fuel: int, + tris: int, +} + +// shading_paint composites `sh` onto `bmp` through `clip`. +// +// ctm maps the shading's own space to device space +// clip device pixels, half-open, intersected with the bitmap here +// alpha the constant alpha of the paint operation +// mask an optional soft mask aligned with `bmp` in device pixels, one +// component read (what `mask_from_bitmap` produces). Pixels outside +// it read as 255, so a mask smaller than the clip shows content +// rather than erasing it. +// +// `bmp` is PREMULTIPLIED, like everything below the Device line. A singular or +// non-finite CTM paints nothing, and so does a colour space of /None. +// `context.allocator` supplies scratch only; everything it takes is released +// before this returns. +shading_paint :: proc(doc: ^Document, sh: ^Shading, bmp: ^Bitmap, ctm: Matrix, + clip: Raster_Clip, alpha: f64, mode: Blend_Mode, mask: ^Bitmap) -> Error { + if sh == nil || bmp == nil || sh.cs == nil { + return .Invalid_Argument + } + // A /None separation marks nothing at all on the page (8.6.6.4). + if sh.cs.is_none { + return .None + } + if !matrix_is_finite(ctm) { + return .None + } + inv, invertible := matrix_invert(ctm) + if !invertible { + return .None + } + + ga := blend_clamp01(alpha) + if ga <= 0 { + return .None + } + + cl := Raster_Clip{ + x0 = max(clip.x0, 0), + y0 = max(clip.y0, 0), + x1 = min(clip.x1, bmp.width), + y1 = min(clip.y1, bmp.height), + } + if cl.x1 <= cl.x0 || cl.y1 <= cl.y0 { + return .None + } + + // Narrowing to what the shading can possibly reach keeps a small gradient + // on a big page from walking the whole page, and is what applies /BBox to + // a mesh. + b := shading_bounds(sh, ctm) + cl = Raster_Clip{ + x0 = raster_floor_clamped(b.x0, cl.x0, cl.x1), + y0 = raster_floor_clamped(b.y0, cl.y0, cl.y1), + x1 = raster_ceil_clamped(b.x1, cl.x0, cl.x1), + y1 = raster_ceil_clamped(b.y1, cl.y0, cl.y1), + } + if cl.x1 <= cl.x0 || cl.y1 <= cl.y0 { + return .None + } + + width := cl.x1 - cl.x0 + row, row_err := make([]u8, width * 4, context.allocator) + if row_err != nil { + return .Out_Of_Memory + } + defer delete(row, context.allocator) + + ctx := Shade_Ctx{ + doc = doc, + sh = sh, + bmp = bmp, + clip = cl, + alpha = ga, + mode = mode, + mask = mask, + ctm = ctm, + inv = inv, + row = row, + mask_row_y = -1, + fuel = SHADE_PIXEL_FUEL, + } + + if mask != nil { + mrow, mask_err := make([]u8, width, context.allocator) + if mask_err != nil { + return .Out_Of_Memory + } + ctx.mask_row = mrow + } + // Freed here rather than in the branch above: a `defer` runs at the end of + // the block it sits in, so one inside the `if` would free the row before a + // single span had been composited through it. + defer if ctx.mask_row != nil { delete(ctx.mask_row, context.allocator) } + + switch sh.kind { + case .Function_Based: + shade_paint_function(&ctx) + case .Axial: + shade_paint_axial(&ctx) + case .Radial: + shade_paint_radial(&ctx) + case .Free_Triangle, .Lattice_Triangle, .Coons, .Tensor: + return shade_paint_mesh(&ctx) + } + return .None +} + +// shade_put writes one covered pixel into the scratch row. +@(private) +shade_put :: proc(ctx: ^Shade_Ctx, x: int, rgb: [3]f64) { + i := (x - ctx.clip.x0) * 4 + if i < 0 || i + 4 > len(ctx.row) { + return + } + ctx.row[i] = blend_quantize(rgb[0]) + ctx.row[i + 1] = blend_quantize(rgb[1]) + ctx.row[i + 2] = blend_quantize(rgb[2]) + ctx.row[i + 3] = 255 +} + +@(private) +shade_skip :: proc(ctx: ^Shade_Ctx, x: int) { + i := (x - ctx.clip.x0) * 4 + if i < 0 || i + 4 > len(ctx.row) { + return + } + ctx.row[i + 3] = 0 +} + +// shade_mask_row lifts the mask's coverage for scanline `y` into a byte per +// pixel, which is the form `blend_span_rgba8` indexes. +@(private) +shade_mask_row :: proc(ctx: ^Shade_Ctx, y: int) { + if ctx.mask_row == nil || ctx.mask_row_y == y { + return + } + ctx.mask_row_y = y + for i in 0 ..< len(ctx.mask_row) { + ctx.mask_row[i] = 255 + } + if ctx.mask == nil { + return + } + src := bitmap_row(ctx.mask, y) + if src == nil { + return + } + n := ctx.mask.n + for i in 0 ..< len(ctx.mask_row) { + x := ctx.clip.x0 + i + if x >= ctx.mask.width { + break + } + off := x * n + if off < len(src) { + ctx.mask_row[i] = src[off] + } + } +} + +// shade_flush composites the covered runs of `row` between x_from and x_to. +@(private) +shade_flush :: proc(ctx: ^Shade_Ctx, y, x_from, x_to: int) { + lo := max(x_from, ctx.clip.x0) + hi := min(x_to, ctx.clip.x1) + if lo >= hi { + return + } + shade_mask_row(ctx, y) + + i := lo + for i < hi { + if ctx.row[(i - ctx.clip.x0) * 4 + 3] == 0 { + i += 1 + continue + } + j := i + for j < hi && ctx.row[(j - ctx.clip.x0) * 4 + 3] != 0 { + j += 1 + } + a := (i - ctx.clip.x0) * 4 + b := (j - ctx.clip.x0) * 4 + mrow: []u8 + if ctx.mask_row != nil { + mrow = ctx.mask_row[i - ctx.clip.x0 : j - ctx.clip.x0] + } + blend_span_rgba8(ctx.bmp, y, i, ctx.row[a:b], ctx.alpha, ctx.mode, nil, mrow) + i = j + } +} + +// shade_in_bbox tests a device pixel centre against /BBox, which is given in +// the shading's own space. Testing there rather than against the transformed +// box keeps a rotated CTM from painting the corners the box excludes. +@(private) +shade_in_bbox :: proc(ctx: ^Shade_Ctx, p: Point) -> bool { + if !ctx.sh.has_bbox { + return true + } + return rect_contains(ctx.sh.bbox, p) +} + +// --------------------------------------------------------------------------- +// Type 1: function-based +// --------------------------------------------------------------------------- + +@(private) +shade_paint_function :: proc(ctx: ^Shade_Ctx) { + sh := ctx.sh + // /Matrix maps the domain into the shading's space, which the CTM then + // maps to the device; one inverse takes a pixel all the way back. + full, ok := matrix_invert(matrix_mul(sh.mtx, ctx.ctm)) + if !ok { + return + } + + in_: [2]f64 + for y in ctx.clip.y0 ..< ctx.clip.y1 { + for x in ctx.clip.x0 ..< ctx.clip.x1 { + dev := Point{f64(x) + 0.5, f64(y) + 0.5} + p := transform_point(full, dev) + if !is_finite(p.x) || !is_finite(p.y) || + p.x < sh.domain[0] || p.x > sh.domain[1] || + p.y < sh.domain[2] || p.y > sh.domain[3] { + shade_skip(ctx, x) + continue + } + if sh.has_bbox && !shade_in_bbox(ctx, transform_point(ctx.inv, dev)) { + shade_skip(ctx, x) + continue + } + in_[0] = p.x + in_[1] = p.y + shade_put(ctx, x, shade_eval(sh, in_[:])) + } + shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1) + ctx.fuel -= ctx.clip.x1 - ctx.clip.x0 + if ctx.fuel <= 0 { + return + } + } +} + +// --------------------------------------------------------------------------- +// Type 2: axial +// --------------------------------------------------------------------------- + +@(private) +shade_paint_axial :: proc(ctx: ^Shade_Ctx) { + sh := ctx.sh + p0 := Point{sh.coords[0], sh.coords[1]} + d := Point{sh.coords[2] - p0.x, sh.coords[3] - p0.y} + len2 := d.x * d.x + d.y * d.y + // A zero-length axis leaves the parametric variable undefined; there is no + // colour to choose, so nothing is painted. + if !(len2 > 0) { + return + } + inv_len2 := 1 / len2 + + lut: Shade_Lut + shade_build_lut(sh, &lut) + + for y in ctx.clip.y0 ..< ctx.clip.y1 { + for x in ctx.clip.x0 ..< ctx.clip.x1 { + p := transform_point(ctx.inv, Point{f64(x) + 0.5, f64(y) + 0.5}) + s := ((p.x - p0.x) * d.x + (p.y - p0.y) * d.y) * inv_len2 + if !is_finite(s) || !shade_in_bbox(ctx, p) { + shade_skip(ctx, x) + continue + } + if s < 0 { + if !sh.extend[0] { + shade_skip(ctx, x) + continue + } + s = 0 + } else if s > 1 { + if !sh.extend[1] { + shade_skip(ctx, x) + continue + } + s = 1 + } + shade_put(ctx, x, shade_lut_at(&lut, s)) + } + shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1) + ctx.fuel -= ctx.clip.x1 - ctx.clip.x0 + if ctx.fuel <= 0 { + return + } + } +} + +// --------------------------------------------------------------------------- +// Type 3: radial +// --------------------------------------------------------------------------- + +// shade_radial_accept tests one candidate s against the two rules that decide +// whether a circle in the family exists there at all: its radius must not be +// negative (8.7.4.5.4 -- the extension stops where the circles would invert), +// and a value outside 0..1 needs the matching /Extend. The returned value is +// clamped, which is what makes an extended shading paint its end colour. +@(private) +shade_radial_accept :: proc(s, r0, dr: f64, extend: [2]bool) -> (f64, bool) { + if !is_finite(s) { + return 0, false + } + if r0 + s * dr < 0 { + return 0, false + } + if s < 0 { + return 0, extend[0] + } + if s > 1 { + return 1, extend[1] + } + return s, true +} + +// shade_radial_solve finds the largest s whose circle passes through the point, +// as the format requires: later circles paint over earlier ones, so the largest +// s is the one that shows. +// +// a = dx^2 + dy^2 - dr^2 +// b = px*dx + py*dy + r0*dr +// c = px^2 + py^2 - r0^2 +// +// solving a*s^2 - 2*b*s + c = 0. `a` is zero exactly when the circles' radii +// grow as fast as their centres move -- the cone degenerates to a paraboloid +// and the equation is linear. A negative discriminant means no circle in the +// family reaches the point, which must paint nothing rather than an end colour. +@(private) +shade_radial_solve :: proc(a, b, c, r0, dr: f64, extend: [2]bool) -> (f64, bool) { + if a == 0 { + if b == 0 { + return 0, false + } + return shade_radial_accept(c / (2 * b), r0, dr, extend) + } + disc := b * b - a * c + if !(disc >= 0) { + return 0, false + } + sq := math.sqrt(disc) + s1 := (b + sq) / a + s2 := (b - sq) / a + hi := max(s1, s2) + lo := min(s1, s2) + if s, ok := shade_radial_accept(hi, r0, dr, extend); ok { + return s, true + } + return shade_radial_accept(lo, r0, dr, extend) +} + +@(private) +shade_paint_radial :: proc(ctx: ^Shade_Ctx) { + sh := ctx.sh + x0, y0, r0 := sh.coords[0], sh.coords[1], sh.coords[2] + dx := sh.coords[3] - x0 + dy := sh.coords[4] - y0 + dr := sh.coords[5] - r0 + a := dx * dx + dy * dy - dr * dr + + lut: Shade_Lut + shade_build_lut(sh, &lut) + + for y in ctx.clip.y0 ..< ctx.clip.y1 { + for x in ctx.clip.x0 ..< ctx.clip.x1 { + p := transform_point(ctx.inv, Point{f64(x) + 0.5, f64(y) + 0.5}) + if !is_finite(p.x) || !is_finite(p.y) || !shade_in_bbox(ctx, p) { + shade_skip(ctx, x) + continue + } + px := p.x - x0 + py := p.y - y0 + b := px * dx + py * dy + r0 * dr + c := px * px + py * py - r0 * r0 + s, ok := shade_radial_solve(a, b, c, r0, dr, sh.extend) + if !ok { + shade_skip(ctx, x) + continue + } + shade_put(ctx, x, shade_lut_at(&lut, s)) + } + shade_flush(ctx, y, ctx.clip.x0, ctx.clip.x1) + ctx.fuel -= ctx.clip.x1 - ctx.clip.x0 + if ctx.fuel <= 0 { + return + } + } +} + +// --------------------------------------------------------------------------- +// Triangles +// --------------------------------------------------------------------------- + +@(private) +Shade_Vertex :: struct { + // Device space: the mesh is transformed as it is read, so the whole + // rasteriser works in one space. + p: Point, + rgb: [3]f64, +} + +// shade_top_left reports whether an edge owns the pixel centres that land +// exactly on it. With triangles wound to a positive area, the interior lies on +// the e >= 0 side, which makes an edge running upwards a left edge and a +// horizontal edge running right a top edge. Two triangles sharing an edge +// traverse it in opposite directions, so exactly one of them owns it. +@(private) +shade_top_left :: proc "contextless" (d: Point) -> bool { + return d.y < 0 || (d.y == 0 && d.x > 0) +} + +@(private) +shade_edge_accept :: proc "contextless" (w: f64, top_left: bool) -> bool { + // Written so a NaN is outside rather than inside. + return w > 0 || (w == 0 && top_left) +} + +// shade_triangle scan-converts one Gouraud-shaded triangle at pixel centres. +@(private) +shade_triangle :: proc(ctx: ^Shade_Ctx, va, vb, vc: Shade_Vertex) { + a := va + b := vb + c := vc + if !is_finite(a.p.x) || !is_finite(a.p.y) || + !is_finite(b.p.x) || !is_finite(b.p.y) || + !is_finite(c.p.x) || !is_finite(c.p.y) { + return + } + + area := (b.p.x - a.p.x) * (c.p.y - a.p.y) - (b.p.y - a.p.y) * (c.p.x - a.p.x) + if !is_finite(area) || area == 0 { + return + } + if area < 0 { + // Winding every triangle the same way is what makes the top-left rule + // consistent between neighbours: an adjacent triangle on the far side + // of a shared edge necessarily traverses that edge the other way. + b, c = c, b + area = -area + } + ctx.tris += 1 + + bx0 := raster_floor_clamped(min(a.p.x, b.p.x, c.p.x), ctx.clip.x0, ctx.clip.x1) + bx1 := raster_ceil_clamped(max(a.p.x, b.p.x, c.p.x), ctx.clip.x0, ctx.clip.x1) + by0 := raster_floor_clamped(min(a.p.y, b.p.y, c.p.y), ctx.clip.y0, ctx.clip.y1) + by1 := raster_ceil_clamped(max(a.p.y, b.p.y, c.p.y), ctx.clip.y0, ctx.clip.y1) + if bx1 <= bx0 || by1 <= by0 { + return + } + + d0 := Point{b.p.x - a.p.x, b.p.y - a.p.y} + d1 := Point{c.p.x - b.p.x, c.p.y - b.p.y} + d2 := Point{a.p.x - c.p.x, a.p.y - c.p.y} + tl0 := shade_top_left(d0) + tl1 := shade_top_left(d1) + tl2 := shade_top_left(d2) + inv_area := 1 / area + + for y in by0 ..< by1 { + py := f64(y) + 0.5 + for x in bx0 ..< bx1 { + px := f64(x) + 0.5 + // Recomputed rather than stepped along the row on purpose: the + // same edge evaluated from two neighbouring triangles must give + // bit-identical results, and an incremental sum would depend on + // where each triangle's bounding box started. + w0 := d0.x * (py - a.p.y) - d0.y * (px - a.p.x) + w1 := d1.x * (py - b.p.y) - d1.y * (px - b.p.x) + w2 := d2.x * (py - c.p.y) - d2.y * (px - c.p.x) + if !shade_edge_accept(w0, tl0) || !shade_edge_accept(w1, tl1) || + !shade_edge_accept(w2, tl2) { + shade_skip(ctx, x) + continue + } + if ctx.sh.has_bbox && !shade_in_bbox(ctx, transform_point(ctx.inv, Point{px, py})) { + shade_skip(ctx, x) + continue + } + // w1 is the edge opposite a, w2 opposite b, w0 opposite c. + la := w1 * inv_area + lb := w2 * inv_area + lc := w0 * inv_area + rgb := [3]f64{ + la * a.rgb[0] + lb * b.rgb[0] + lc * c.rgb[0], + la * a.rgb[1] + lb * b.rgb[1] + lc * c.rgb[1], + la * a.rgb[2] + lb * b.rgb[2] + lc * c.rgb[2], + } + shade_put(ctx, x, rgb) + } + shade_flush(ctx, y, bx0, bx1) + ctx.fuel -= bx1 - bx0 + if ctx.fuel <= 0 { + return + } + } +} + +// --------------------------------------------------------------------------- +// Mesh streams +// --------------------------------------------------------------------------- + +@(private) +Mesh_Reader :: struct { + sh: ^Shading, + ctm: Matrix, + data: []byte, + // Bit position, MSB first, which is how every packed field in a PDF is + // written. + pos: int, + max_coord: f64, + max_comp: f64, + count: int, +} + +@(private) +mesh_reader_make :: proc(sh: ^Shading, ctm: Matrix, data: []byte) -> Mesh_Reader { + return Mesh_Reader{ + sh = sh, + ctm = ctm, + data = data, + max_coord = math.pow(2, f64(sh.bits_per_coord)) - 1, + max_comp = math.pow(2, f64(sh.bits_per_component)) - 1, + } +} + +// mesh_bits reads the next `n` bits. Returns ok = false at end of data, and +// leaves the reader at the end so every later read fails too -- a truncated +// mesh stops, it does not wrap around and paint garbage. +@(private) +mesh_bits :: proc(r: ^Mesh_Reader, n: int) -> (v: u32, ok: bool) { + total := len(r.data) * 8 + if n <= 0 || n > 32 || r.pos + n > total { + r.pos = total + return 0, false + } + acc: u64 + left := n + for left > 0 { + byte_index := r.pos >> 3 + bit_off := r.pos & 7 + avail := 8 - bit_off + take := min(avail, left) + chunk := (u64(r.data[byte_index]) >> uint(avail - take)) & u64((1 << uint(take)) - 1) + acc = acc << uint(take) | chunk + r.pos += take + left -= take + } + return u32(acc), true +} + +// mesh_align skips to the next byte boundary. Free-form triangles and patches +// pad each record to a whole number of bytes; lattice rows do not. +@(private) +mesh_align :: proc(r: ^Mesh_Reader) { + r.pos = ((r.pos + 7) / 8) * 8 +} + +@(private) +mesh_decode :: proc(v: u32, maxv, lo, hi: f64) -> f64 { + if !(maxv > 0) { + return lo + } + return lo + f64(v) * (hi - lo) / maxv +} + +@(private) +mesh_point :: proc(r: ^Mesh_Reader) -> (Point, bool) { + xv, ok0 := mesh_bits(r, r.sh.bits_per_coord) + yv, ok1 := mesh_bits(r, r.sh.bits_per_coord) + if !ok0 || !ok1 { + return Point{}, false + } + dec := r.sh.decode + p := Point{ + mesh_decode(xv, r.max_coord, dec[0], dec[1]), + mesh_decode(yv, r.max_coord, dec[2], dec[3]), + } + return transform_point(r.ctm, p), true +} + +@(private) +mesh_color :: proc(r: ^Mesh_Reader) -> ([3]f64, bool) { + comps: [MAX_COLOR_COMPONENTS]f64 + sh := r.sh + dec := sh.decode + for i in 0 ..< sh.n_comp { + cv, ok := mesh_bits(r, sh.bits_per_component) + if !ok { + return [3]f64{}, false + } + comps[i] = mesh_decode(cv, r.max_comp, dec[4 + 2 * i], dec[5 + 2 * i]) + } + if len(sh.funcs) > 0 { + return shade_eval(sh, comps[:1]), true + } + return shade_components_to_rgb(sh, comps[:sh.n_comp]), true +} + +@(private) +mesh_vertex :: proc(r: ^Mesh_Reader, out: ^Shade_Vertex) -> bool { + if r.count >= MAX_MESH_VERTICES { + return false + } + p, p_ok := mesh_point(r) + if !p_ok { + return false + } + rgb, c_ok := mesh_color(r) + if !c_ok { + return false + } + out.p = p + out.rgb = rgb + r.count += 1 + return true +} + +@(private) +shade_paint_mesh :: proc(ctx: ^Shade_Ctx) -> Error { + sh := ctx.sh + if sh.stream == nil || len(sh.decode) < 4 + 2 * sh.n_comp { + return .None + } + data, err := stream_data(ctx.doc, sh.stream) + if err != .None || len(data) == 0 { + return .None + } + + r := mesh_reader_make(sh, ctx.ctm, data) + switch sh.kind { + case .Free_Triangle: + shade_mesh_free(ctx, &r) + case .Lattice_Triangle: + return shade_mesh_lattice(ctx, &r) + case .Coons, .Tensor: + shade_mesh_patches(ctx, &r) + case .Function_Based, .Axial, .Radial: + // Not reachable: shading_paint dispatches those elsewhere. + } + return .None +} + +// --------------------------------------------------------------------------- +// Type 4: free-form triangles +// --------------------------------------------------------------------------- + +@(private) +mesh_flagged_vertex :: proc(r: ^Mesh_Reader, out: ^Shade_Vertex) -> bool { + // The two vertices that follow a flag-0 vertex carry a flag of their own, + // which the format says shall be 0 and which nothing reads. + if _, ok := mesh_bits(r, r.sh.bits_per_flag); !ok { + return false + } + if !mesh_vertex(r, out) { + return false + } + mesh_align(r) + return true +} + +@(private) +shade_mesh_free :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) { + va, vb, vc: Shade_Vertex + have := false + + for ctx.tris < MAX_MESH_TRIANGLES && ctx.fuel > 0 { + flag, flag_ok := mesh_bits(r, r.sh.bits_per_flag) + if !flag_ok { + return + } + v: Shade_Vertex + if !mesh_vertex(r, &v) { + return + } + mesh_align(r) + + switch { + case flag == 0 || !have: + // A continuation flag with no previous triangle is malformed; + // starting a fresh triangle is the recovery that loses least. + v1, v2: Shade_Vertex + if !mesh_flagged_vertex(r, &v1) || !mesh_flagged_vertex(r, &v2) { + return + } + va, vb, vc = v, v1, v2 + case flag == 1: + va, vb, vc = vb, vc, v + case flag == 2: + va, vb, vc = va, vc, v + case: + add_repair(ctx.doc, .Bad_Resource, 0) + return + } + shade_triangle(ctx, va, vb, vc) + have = true + } +} + +// --------------------------------------------------------------------------- +// Type 5: lattice-form triangles +// --------------------------------------------------------------------------- + +@(private) +shade_mesh_lattice :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) -> Error { + per_row := ctx.sh.vertices_per_row + if per_row < 2 || per_row > MAX_MESH_ROW { + return .None + } + + rows, rows_err := make([]Shade_Vertex, per_row * 2, context.allocator) + if rows_err != nil { + return .Out_Of_Memory + } + defer delete(rows, context.allocator) + prev := rows[:per_row] + cur := rows[per_row:] + + have_prev := false + for ctx.tris < MAX_MESH_TRIANGLES && ctx.fuel > 0 { + complete := true + for i in 0 ..< per_row { + if !mesh_vertex(r, &cur[i]) { + complete = false + break + } + } + // A row that ran out mid-way is dropped whole: half a row has no + // triangles in it. + if !complete { + return .None + } + if have_prev { + for i in 0 ..< per_row - 1 { + shade_triangle(ctx, prev[i], prev[i + 1], cur[i]) + shade_triangle(ctx, prev[i + 1], cur[i + 1], cur[i]) + } + } + prev, cur = cur, prev + have_prev = true + } + return .None +} + +// --------------------------------------------------------------------------- +// Types 6 and 7: Coons and tensor patches +// --------------------------------------------------------------------------- + +// The twelve boundary control points are written anticlockwise from one +// corner, which lays them onto the 4x4 tensor grid like this (ISO 32000-2, +// figures 46 and 47): +// +// p1 p2 p3 p4 [0][0] [0][1] [0][2] [0][3] +// p12 p5 [1][0] p13 p14 [1][3] +// p11 p6 [2][0] p16 p15 [2][3] +// p10 p9 p8 p7 [3][0] [3][1] [3][2] [3][3] +// +// with the corner colours c1..c4 at p1, p4, p7 and p10 in that order. +@(private) +PATCH_GRID := [12][2]int{ + {0, 0}, {0, 1}, {0, 2}, {0, 3}, + {1, 3}, {2, 3}, {3, 3}, + {3, 2}, {3, 1}, {3, 0}, + {2, 0}, {1, 0}, +} + +// PATCH_SHARED tells which of the previous patch's points and colours a flag +// of 1, 2 or 3 reuses as the new patch's first edge (Table 85). +@(private) +PATCH_SHARED := [4][4]int{ + {0, 0, 0, 0}, // flag 0: nothing is shared + {3, 4, 5, 6}, + {6, 7, 8, 9}, + {9, 10, 11, 0}, +} + +@(private) +PATCH_SHARED_COLOR := [4][2]int{ + {0, 0}, + {1, 2}, + {2, 3}, + {3, 0}, +} + +@(private) +shade_mesh_patches :: proc(ctx: ^Shade_Ctx, r: ^Mesh_Reader) { + n_points := ctx.sh.kind == .Tensor ? 16 : 12 + + pts, prev_pts: [16]Point + cols, prev_cols: [4][3]f64 + have := false + + for _ in 0 ..< MAX_MESH_PATCHES { + if ctx.tris >= MAX_MESH_TRIANGLES || ctx.fuel <= 0 { + return + } + + raw_flag, flag_ok := mesh_bits(r, r.sh.bits_per_flag) + if !flag_ok { + return + } + flag := int(raw_flag) + if flag > 3 { + add_repair(ctx.doc, .Bad_Resource, 0) + return + } + + first_point := 0 + first_color := 0 + if flag != 0 && have { + share := PATCH_SHARED[flag] + for k in 0 ..< 4 { + pts[k] = prev_pts[share[k]] + } + cshare := PATCH_SHARED_COLOR[flag] + cols[0] = prev_cols[cshare[0]] + cols[1] = prev_cols[cshare[1]] + first_point = 4 + first_color = 2 + } + + for k in first_point ..< n_points { + p, ok := mesh_point(r) + if !ok { + return + } + pts[k] = p + } + for k in first_color ..< 4 { + c, ok := mesh_color(r) + if !ok { + return + } + cols[k] = c + } + mesh_align(r) + + shade_patch(ctx, pts, cols, ctx.sh.kind == .Tensor) + prev_pts = pts + prev_cols = cols + have = true + } +} + +@(private) +bernstein3 :: proc "contextless" (t: f64) -> [4]f64 { + mt := 1 - t + return [4]f64{mt * mt * mt, 3 * mt * mt * t, 3 * mt * t * t, t * t * t} +} + +@(private) +patch_eval :: proc(pole: ^[4][4]Point, u, v: f64) -> (p: Point) { + bu := bernstein3(u) + bv := bernstein3(v) + for i in 0 ..< 4 { + for j in 0 ..< 4 { + w := bu[i] * bv[j] + p.x += w * pole[i][j].x + p.y += w * pole[i][j].y + } + } + return +} + +// patch_color is the bilinear interpolation of the four corner colours the +// format prescribes (8.7.4.5.7): c1 at (0,0), c2 at (0,1), c3 at (1,1) and +// c4 at (1,0), matching the corners the control points put there. +@(private) +patch_color :: proc(c: [4][3]f64, u, v: f64) -> (out: [3]f64) { + for i in 0 ..< 3 { + e0 := (1 - v) * c[0][i] + v * c[1][i] + e1 := (1 - v) * c[3][i] + v * c[2][i] + out[i] = (1 - u) * e0 + u * e1 + } + return +} + +// shade_patch subdivides one patch into triangles. +// +// The subdivision is uniform, with its step chosen from the patch's device +// size so that a step is about PATCH_STEP_PIXELS across: a patch a few pixels +// wide costs two triangles, and one covering a page is split as far as the cap +// allows. Uniform subdivision rather than adaptive flatness testing is a +// deliberate trade -- a Bezier patch's colour varies over its whole area, not +// just where it curves, so the flat parts need the same subdivision as the +// curved ones and adaptivity would buy nothing. +@(private) +shade_patch :: proc(ctx: ^Shade_Ctx, v: [16]Point, c: [4][3]f64, tensor: bool) { + pole: [4][4]Point + for k in 0 ..< 12 { + g := PATCH_GRID[k] + pole[g[0]][g[1]] = v[k] + } + if tensor { + pole[1][1] = v[12] + pole[1][2] = v[13] + pole[2][2] = v[14] + pole[2][1] = v[15] + } else { + // A Coons patch gives no interior control points; the format defines + // them from the boundary (8.7.4.5.7). + pole[1][1] = shade_coons_inner(pole[0][0], pole[0][1], pole[1][0], pole[0][3], pole[3][0], pole[3][1], pole[1][3], pole[3][3]) + pole[1][2] = shade_coons_inner(pole[0][3], pole[0][2], pole[1][3], pole[0][0], pole[3][3], pole[3][2], pole[1][0], pole[3][0]) + pole[2][1] = shade_coons_inner(pole[3][0], pole[3][1], pole[2][0], pole[3][3], pole[0][0], pole[0][1], pole[2][3], pole[0][3]) + pole[2][2] = shade_coons_inner(pole[3][3], pole[3][2], pole[2][3], pole[3][0], pole[0][3], pole[0][2], pole[2][0], pole[0][0]) + } + + lo := pole[0][0] + hi := pole[0][0] + for i in 0 ..< 4 { + for j in 0 ..< 4 { + p := pole[i][j] + if !is_finite(p.x) || !is_finite(p.y) { + return + } + lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y) + hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y) + } + } + extent := max(hi.x - lo.x, hi.y - lo.y) + n := 1 + if is_finite(extent) && extent > 0 { + steps := math.ceil(extent / PATCH_STEP_PIXELS) + if steps >= f64(MAX_PATCH_SUBDIV) { + n = MAX_PATCH_SUBDIV + } else if steps > 1 { + n = int(steps) + } + } + inv_n := 1 / f64(n) + + row_a, row_b: [MAX_PATCH_SUBDIV + 1]Point + for j in 0 ..= n { + row_a[j] = patch_eval(&pole, 0, f64(j) * inv_n) + } + for i in 1 ..= n { + u1 := f64(i) * inv_n + u0 := f64(i - 1) * inv_n + for j in 0 ..= n { + row_b[j] = patch_eval(&pole, u1, f64(j) * inv_n) + } + for j in 0 ..< n { + v0 := f64(j) * inv_n + v1 := f64(j + 1) * inv_n + p00 := Shade_Vertex{row_a[j], patch_color(c, u0, v0)} + p01 := Shade_Vertex{row_a[j + 1], patch_color(c, u0, v1)} + p11 := Shade_Vertex{row_b[j + 1], patch_color(c, u1, v1)} + p10 := Shade_Vertex{row_b[j], patch_color(c, u1, v0)} + shade_triangle(ctx, p00, p01, p11) + shade_triangle(ctx, p00, p11, p10) + } + row_a = row_b + if ctx.fuel <= 0 || ctx.tris >= MAX_MESH_TRIANGLES { + return + } + } +} + +// shade_coons_inner is the interior control point the format derives from the +// boundary of a Coons patch: +// +// p11 = (-4*p00 + 6*(p01 + p10) - 2*(p03 + p30) + 3*(p31 + p13) - p33) / 9 +// +// with the arguments named for the corner it belongs to; the other three come +// from the same expression with the grid rotated. +@(private) +shade_coons_inner :: proc "contextless" (corner, along_a, along_b, far_a, far_b, near_a, near_b, opposite: Point) -> Point { + return Point{ + (-4 * corner.x + 6 * (along_a.x + along_b.x) - 2 * (far_a.x + far_b.x) + + 3 * (near_a.x + near_b.x) - opposite.x) / 9, + (-4 * corner.y + 6 * (along_a.y + along_b.y) - 2 * (far_a.y + far_b.y) + + 3 * (near_a.y + near_b.y) - opposite.y) / 9, + } +} diff --git a/shading_test.odin b/shading_test.odin new file mode 100644 index 0000000..fa044ed --- /dev/null +++ b/shading_test.odin @@ -0,0 +1,1731 @@ +package pdf + +import "core:testing" + +// Shading tests. +// +// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_bytes` -- live in +// function_test.odin, which is also where the Document these build on comes +// from. +// +// Every target bitmap is three components, no alpha. That is deliberate: with +// no alpha channel the destination reads as opaque and `blend_over` collapses +// to "the source colour", so an assertion about a pixel is an assertion about +// what the shading computed rather than about compositing. It also gives +// "painted nothing" a spelling that "painted black" cannot fake -- the bitmap +// starts filled with SH_BG, a colour no shading in this file produces. + +@(private = "file") +SH_BG :: [3]u8{9, 200, 30} + +@(private = "file") +sh_target :: proc(t: ^testing.T, w, h: int, loc := #caller_location) -> Bitmap { + bmp, err := bitmap_make(w, h, 3) + testing.expect_value(t, err, Error.None, loc = loc) + bitmap_fill(&bmp, [4]u8{SH_BG[0], SH_BG[1], SH_BG[2], 255}) + return bmp +} + +@(private = "file") +sh_pixel :: proc(bmp: ^Bitmap, x, y: int) -> [3]u8 { + off, ok := bitmap_pixel_offset(bmp, x, y) + if !ok { + return [3]u8{} + } + return [3]u8{bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2]} +} + +@(private = "file") +expect_pixel :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [3]u8, tol: int, + what: string, loc := #caller_location) { + got := sh_pixel(bmp, x, y) + for i in 0 ..< 3 { + if abs(int(got[i]) - int(want[i])) > tol { + testing.expectf(t, false, "%s: pixel (%d,%d) is %v, want %v (tolerance %d)", + what, x, y, got, want, tol, loc = loc) + return + } + } +} + +@(private = "file") +expect_gray :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, g: u8, tol: int, + what: string, loc := #caller_location) { + expect_pixel(t, bmp, x, y, [3]u8{g, g, g}, tol, what, loc) +} + +@(private = "file") +expect_untouched :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, what: string, + loc := #caller_location) { + got := sh_pixel(bmp, x, y) + if got != SH_BG { + testing.expectf(t, false, "%s: pixel (%d,%d) was painted %v, expected it left alone", + what, x, y, got, loc = loc) + } +} + +@(private = "file") +expect_all_painted :: proc(t: ^testing.T, bmp: ^Bitmap, what: string, loc := #caller_location) { + for y in 0 ..< bmp.height { + for x in 0 ..< bmp.width { + if sh_pixel(bmp, x, y) == SH_BG { + testing.expectf(t, false, "%s: pixel (%d,%d) was never painted", what, x, y, loc = loc) + return + } + } + } +} + +// sh_gray is the byte a linear 0..1 grey function paints at parametric +// position `s`, through the sampled colour table: the table is indexed by the +// nearest of its 256 entries, and entry i holds grey i/255. +@(private = "file") +sh_gray :: proc(s: f64) -> u8 { + return u8(clamp(int(s * 255 + 0.5), 0, 255)) +} + +@(private = "file") +sh_load :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) -> ^Shading { + sh, err := shading_load(doc, cf_parse(doc, src), nil) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expect(t, sh != nil, "shading did not load", loc = loc) + return sh +} + +@(private = "file") +sh_load_stream :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, + loc := #caller_location) -> ^Shading { + sh, err := shading_load(doc, cf_stream(doc, dict_src, payload), nil) + testing.expect_value(t, err, Error.None, loc = loc) + testing.expect(t, sh != nil, "shading did not load", loc = loc) + return sh +} + +@(private = "file") +sh_paint :: proc(t: ^testing.T, doc: ^Document, sh: ^Shading, bmp: ^Bitmap, + ctm := MATRIX_IDENTITY, loc := #caller_location) { + err := shading_paint(doc, sh, bmp, ctm, + Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil) + testing.expect_value(t, err, Error.None, loc = loc) +} + +// A linear grey ramp: f(t) = t. +@(private = "file") +RAMP :: "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>" + +// --------------------------------------------------------------------------- +// Type 2: axial +// --------------------------------------------------------------------------- + +// The axis runs from device x = 2.5 to x = 10.5, so t is exactly 0, 0.5 and 1 +// at the centres of pixels 2, 6 and 10 rather than somewhere between them. +@(private = "file") +AXIAL_SRC :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] " + RAMP + +@(test) +test_shading_axial_endpoints :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false false] >>") + bmp := sh_target(t, 13, 3) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 2, 1, 0, 0, "t = 0") + expect_gray(t, &bmp, 6, 1, 128, 0, "t = 0.5") + expect_gray(t, &bmp, 10, 1, 255, 0, "t = 1") + + // Every row sees the same colour: the axis is horizontal. + expect_gray(t, &bmp, 6, 0, 128, 0, "t = 0.5, first row") + expect_gray(t, &bmp, 6, 2, 128, 0, "t = 0.5, last row") + + // Quarter points, to catch an axis that lands right but runs at the wrong + // rate. + expect_gray(t, &bmp, 4, 1, sh_gray(0.25), 1, "t = 0.25") + expect_gray(t, &bmp, 8, 1, sh_gray(0.75), 1, "t = 0.75") +} + +@(test) +test_shading_axial_extend_false_leaves_ends_alone :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false false] >>") + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_untouched(t, &bmp, 0, 0, "before the axis") + expect_untouched(t, &bmp, 1, 0, "before the axis") + expect_untouched(t, &bmp, 11, 0, "past the axis") + expect_untouched(t, &bmp, 12, 0, "past the axis") + expect_gray(t, &bmp, 2, 0, 0, 0, "the first pixel on the axis") + expect_gray(t, &bmp, 10, 0, 255, 0, "the last pixel on the axis") +} + +@(test) +test_shading_axial_extend_true_paints_ends :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, AXIAL_SRC + " /Extend [true true] >>") + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 0, 0, 0, 0, "extended start") + expect_gray(t, &bmp, 1, 0, 0, 0, "extended start") + expect_gray(t, &bmp, 11, 0, 255, 0, "extended end") + expect_gray(t, &bmp, 12, 0, 255, 0, "extended end") +} + +@(test) +test_shading_axial_extend_one_end_only :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false true] >>") + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_untouched(t, &bmp, 0, 0, "start is not extended") + expect_gray(t, &bmp, 12, 0, 255, 0, "end is extended") +} + +// /Domain remaps the parametric variable before the function sees it, so the +// far end of the axis asks the ramp for 0.5 and gets mid grey. +@(test) +test_shading_axial_domain :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] /Domain [0 0.5] " + + RAMP + " >>") + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 2, 0, 0, 0, "domain start") + expect_gray(t, &bmp, 10, 0, 128, 0, "domain end is t = 0.5") + expect_gray(t, &bmp, 6, 0, sh_gray(0.25), 1, "domain midpoint is t = 0.25") +} + +// The same picture through a CTM: the shading's coordinates are half as big +// and the matrix doubles them, so the pixels must come out where they did +// before. +@(test) +test_shading_axial_under_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [1.25 0 5.25 0] " + RAMP + + " /Extend [false false] >>") + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp, scale_matrix(2, 2)) + + expect_gray(t, &bmp, 2, 0, 0, 0, "t = 0") + expect_gray(t, &bmp, 6, 0, 128, 0, "t = 0.5") + expect_gray(t, &bmp, 10, 0, 255, 0, "t = 1") + expect_untouched(t, &bmp, 12, 0, "past the axis") +} + +@(test) +test_shading_axial_vertical :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0.5 0 8.5] " + RAMP + " >>") + bmp := sh_target(t, 3, 9) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 1, 0, 0, 0, "top of the axis") + expect_gray(t, &bmp, 1, 4, 128, 0, "middle of the axis") + expect_gray(t, &bmp, 1, 8, 255, 0, "bottom of the axis") +} + +@(test) +test_shading_axial_degenerate_axis_paints_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [4 4 4 4] " + RAMP + + " /Extend [true true] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_untouched(t, &bmp, x, y, "zero-length axis") + } + } +} + +@(test) +test_shading_axial_rgb :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Three one-output functions, the other legal spelling of /Function. + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>") + bmp := sh_target(t, 9, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_pixel(t, &bmp, 0, 0, [3]u8{0, 0, 255}, 0, "t = 0 is blue") + expect_pixel(t, &bmp, 8, 0, [3]u8{255, 0, 0}, 0, "t = 1 is red") + expect_pixel(t, &bmp, 4, 0, [3]u8{128, 0, 127}, 1, "t = 0.5 is purple") +} + +// A /Function array is indexed by colour component, so a sub-function that +// will not load has to hold its slot. Dropping it moves every later function +// down one component and repaints the whole shading -- red where the file +// asked for green -- which is far worse than the one missing component the +// header promises. +@(test) +test_shading_broken_subfunction_keeps_its_slot :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // Red will not load; green ramps to 1 and blue to 0.5. + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" + + "<< /FunctionType 99 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >>] >>") + if sh == nil { return } + testing.expect_value(t, len(sh.funcs), 3) + testing.expect(t, sh.funcs[0] == nil, "the broken entry is a nil placeholder") + + one := [1]f64{1} + rgb := shade_eval(sh, one[:]) + testing.expect_value(t, rgb[0], 0.0) + testing.expect_value(t, rgb[1], 1.0) + testing.expect_value(t, rgb[2], 0.5) + + bmp := sh_target(t, 9, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + expect_pixel(t, &bmp, 8, 0, [3]u8{0, 255, 128}, 1, "t = 1: no red, full green, half blue") + expect_pixel(t, &bmp, 0, 0, [3]u8{0, 0, 0}, 1, "t = 0: every ramp starts at zero") +} + +// The same defect at its worst: with only one survivor the array would shrink +// to length 1, which is the spelling that means "one function supplying every +// component" -- so the blue ramp would be evaluated into all three and paint +// the shading red. +@(test) +test_shading_one_surviving_subfunction_is_not_promoted :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" + + "<< /FunctionType 99 >> << /FunctionType 99 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >>] >>") + if sh == nil { return } + testing.expect_value(t, len(sh.funcs), 3) + + one := [1]f64{1} + rgb := shade_eval(sh, one[:]) + testing.expect_value(t, rgb[0], 0.0) + testing.expect_value(t, rgb[1], 0.0) + testing.expect_value(t, rgb[2], 0.5) + + bmp := sh_target(t, 9, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + expect_pixel(t, &bmp, 8, 0, [3]u8{0, 0, 128}, 1, "the survivor stays on blue") +} + +// Nothing in the array loads: an axial shading with no usable function has no +// colour at all, and there is nothing to paint with. +@(test) +test_shading_all_subfunctions_broken_fails :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh, err := shading_load(doc, cf_parse(doc, + "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" + + "<< /FunctionType 99 >> << /FunctionType 99 >> << /FunctionType 99 >>] >>"), nil) + testing.expect_value(t, err, Error.Bad_Shading) + testing.expect(t, sh == nil, "a shading with no usable function must not load") +} + +// --------------------------------------------------------------------------- +// Type 3: radial +// --------------------------------------------------------------------------- + +// Concentric circles with a zero inner radius: the ordinary "glow" gradient, +// and the case where s is simply the distance from the centre over the radius. +@(test) +test_shading_radial_zero_inner_radius :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 0 4 4 4] " + RAMP + + " /Extend [false false] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // (4.5, 4.5) is sqrt(0.5) from the centre, so s = 0.1768. + expect_gray(t, &bmp, 4, 4, sh_gray(0.176777), 1, "near the centre") + // (2.5, 4.5) is sqrt(2.5) away, s = 0.3953. + expect_gray(t, &bmp, 2, 4, sh_gray(0.395285), 1, "half way out") + // (0.5, 0.5) is 4.95 away: past the outer circle, and not extended. + expect_untouched(t, &bmp, 0, 0, "outside the last circle") + expect_untouched(t, &bmp, 7, 7, "outside the last circle") +} + +// Concentric circles with a non-zero inner radius are nested, and the hole in +// the middle belongs to s < 0: unpainted unless the first end is extended. +@(test) +test_shading_radial_nested_concentric :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + src :: "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 2 4 4 4] " + RAMP + sh := sh_load(t, doc, src + " /Extend [false false] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_untouched(t, &bmp, 4, 4, "inside the inner circle") + // (7.5, 4.5) is 3.5355 from the centre: s = (3.5355 - 2) / 2. + expect_gray(t, &bmp, 7, 4, sh_gray(0.767767), 1, "between the circles") + expect_untouched(t, &bmp, 0, 0, "outside the outer circle") + + sh2 := sh_load(t, doc, src + " /Extend [true true] >>") + bmp2 := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp2) + sh_paint(t, doc, sh2, &bmp2) + + expect_gray(t, &bmp2, 4, 4, 0, 0, "the hole is filled by the first extension") + expect_gray(t, &bmp2, 0, 0, 255, 0, "the outside is filled by the second") + expect_all_painted(t, &bmp2, "extended at both ends") +} + +// Nested but not concentric: the small circle sits off to one side, which is +// the case that separates a real quadratic solve from "distance over radius". +@(test) +test_shading_radial_nested_offset :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [3 4 1 4 4 4] " + RAMP + + " /Extend [false false] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // At (4.5, 4.5): a = -8, b = 4.5, c = 1.5, so s = (4.5 - sqrt(32.25)) / -8. + expect_gray(t, &bmp, 4, 4, sh_gray(0.147361), 1, "inside the family") + // At (0.5, 0.5) the larger root is 1.418 (past the end, not extended) and + // the smaller one is -1.543, where the circle radius has gone negative. + expect_untouched(t, &bmp, 0, 0, "outside the outer circle") +} + +// A cone: circles growing from a point. Points outside it give a negative +// discriminant and must stay unpainted rather than take an end colour. +@(test) +test_shading_radial_cone_and_root_choice :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + src :: "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 4 0 8 4 4] " + RAMP + sh := sh_load(t, doc, src + " /Extend [false false] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // (1.5, 0.5) is well outside the cone: b*b - a*c is -552. + expect_untouched(t, &bmp, 1, 0, "outside the cone") + expect_untouched(t, &bmp, 1, 7, "outside the cone") + // (2.5, 4.5) is inside the narrow end, near the apex: s = 0.6121. The + // apex is a circle of radius zero, and the painted region reaches back to + // it, so nothing may clip the cone off short of it. + expect_gray(t, &bmp, 2, 4, sh_gray(0.612146), 1, "the narrow end of the cone") + // (4.5, 4.5) is inside. The larger root is 1.118, which /Extend rejects, + // so the smaller root 0.382 is the one that paints. + expect_gray(t, &bmp, 4, 4, sh_gray(0.382005), 1, "smaller root, end not extended") + + // With the far end extended the larger root is accepted and clamps to 1. + sh2 := sh_load(t, doc, src + " /Extend [false true] >>") + bmp2 := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp2) + sh_paint(t, doc, sh2, &bmp2) + expect_gray(t, &bmp2, 4, 4, 255, 0, "larger root wins when the end is extended") +} + +@(test) +test_shading_radial_under_ctm :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [2 2 0 2 2 2] " + RAMP + " >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp, scale_matrix(2, 2)) + + // Device (4.5, 4.5) maps back to (2.25, 2.25), sqrt(0.125) from the + // centre, over a radius of 2. + expect_gray(t, &bmp, 4, 4, sh_gray(0.176777), 1, "near the centre") + expect_untouched(t, &bmp, 0, 0, "outside the last circle") +} + +// --------------------------------------------------------------------------- +// Type 1: function-based +// --------------------------------------------------------------------------- + +// /Matrix maps the domain onto the page. Here it scales the unit square to +// 8x8 device pixels, so a function of the domain's x is a horizontal ramp. +// The function is `{ pop }`: the inputs arrive as x then y, so dropping the +// top of the stack leaves x. +@(private = "file") +sh_type1 :: proc(t: ^testing.T, doc: ^Document, dict_src: string) -> ^Shading { + d, ok := to_dict(cf_parse(doc, dict_src)) + testing.expect(t, ok, "type 1 fixture is not a dictionary") + fn := cf_stream(doc, "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1] >>", cf_bytes("{ pop }")) + dict_set(d, "Function", fn) + sh, err := shading_load(doc, d, nil) + testing.expect_value(t, err, Error.None) + return sh +} + +@(test) +test_shading_function_based_matrix :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_type1(t, doc, + "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "the matrix covers the whole bitmap") + for x in 0 ..< 8 { + expect_gray(t, &bmp, x, 3, sh_gray((f64(x) + 0.5) / 8), 1, "ramp across the domain") + } + // The function ignores y, so a column is flat. + expect_gray(t, &bmp, 4, 0, sh_gray(4.5 / 8), 1, "first row") + expect_gray(t, &bmp, 4, 7, sh_gray(4.5 / 8), 1, "last row") +} + +// Without a /Matrix the domain is one unit square at the origin, so only the +// first pixel is inside it. This is the same shading as above with the matrix +// removed: if /Matrix were being ignored, the test above would paint nothing. +@(test) +test_shading_function_based_domain_clips :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_type1(t, doc, "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 0, 0, sh_gray(0.5), 1, "the one pixel inside the domain") + expect_untouched(t, &bmp, 1, 0, "outside the domain in x") + expect_untouched(t, &bmp, 0, 1, "outside the domain in y") + expect_untouched(t, &bmp, 7, 7, "outside the domain") +} + +// Half a domain paints half the area. +@(test) +test_shading_function_based_half_domain :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_type1(t, doc, + "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 0.5 0 1] /Matrix [8 0 0 8 0 0] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 3, 3, sh_gray(3.5 / 8), 1, "inside the domain") + expect_untouched(t, &bmp, 4, 3, "past the domain's x") + expect_untouched(t, &bmp, 7, 0, "past the domain's x") +} + +// --------------------------------------------------------------------------- +// Types 4 and 5: triangle meshes +// +// Every mesh below uses 16-bit coordinates with /Decode [0 65535 ...], which +// makes a coordinate equal to the integer written in the stream, and 8-bit +// colour components and flags, which makes every field a whole byte. The +// payloads are then readable as written rather than as a bit-packing puzzle. +// --------------------------------------------------------------------------- + +@(private = "file") +MESH_DICT :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] >>" + +// Both triangles of an 8x8 square, coloured so that grey is x/8 in each: the +// two together must reproduce one seamless horizontal ramp. +@(test) +test_shading_type4_shared_edge :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0 + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), grey 1 + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), grey 0 + 0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 1, (8,8), grey 1 + } + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // Nothing is missed along the shared diagonal, where eight pixel centres + // land exactly on the edge both triangles own half of. + expect_all_painted(t, &bmp, "two triangles tiling the square") + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp") + } + } +} + +// The same square, built the other way: a lattice of two rows of two. +@(test) +test_shading_type5_lattice :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0 + 0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1 + 0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0 + 0x00, 0x08, 0x00, 0x08, 0xFF, // (8,8) grey 1 + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "a two-by-two lattice") + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp") + } + } +} + +// A three-column lattice whose middle column is pulled to one side: the rows +// are no longer a rectangle, which is what a lattice is for. +@(test) +test_shading_type5_three_columns :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0 + 0x00, 0x04, 0x00, 0x00, 0x80, // (4,0) grey 0.5 + 0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1 + 0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0 + 0x00, 0x04, 0x00, 0x08, 0x80, // (4,8) grey 0.5 + 0x00, 0x08, 0x00, 0x08, 0xFF, // (8,8) grey 1 + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 3 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "a two-by-three lattice") + // 0x80 is 128/255, so the middle column is a shade off exact mid grey and + // the ramp has a slight kink in it -- which is what the vertices say. + mid := 128.0 / 255.0 + expect_gray(t, &bmp, 0, 4, sh_gray(0.125 * mid), 1, "left half") + expect_gray(t, &bmp, 4, 4, sh_gray(mid + 0.125 * (1 - mid)), 1, "right half") +} + +// With a /Function the stream carries one parametric value per vertex instead +// of one value per colour component, which changes the size of every vertex. +@(test) +test_shading_type4_with_function :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), t = 0 + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), t = 1 + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), t = 0 + 0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 1, (8,8), t = 1 + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 1] /C1 [1 0 0] /N 1 >> >>", + payload[:]) + testing.expect_value(t, sh.n_comp, 1) + + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "a mesh coloured through a function") + for x in 0 ..< 8 { + s := (f64(x) + 0.5) / 8 + expect_pixel(t, &bmp, x, 4, [3]u8{sh_gray(s), 0, sh_gray(1 - s)}, 2, "blue to red") + } +} + +// A stream that stops in the middle of a vertex draws what it had and stops. +@(test) +test_shading_type4_truncated :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0 + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), grey 1 + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), grey 0 + 0x01, 0x00, 0x08, 0x00, // flag 1, and then the file ends + } + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // The first triangle is complete and is drawn; the second never arrives. + expect_gray(t, &bmp, 0, 0, sh_gray(0.5 / 8), 1, "the complete triangle") + expect_gray(t, &bmp, 6, 0, sh_gray(6.5 / 8), 1, "the complete triangle") + expect_untouched(t, &bmp, 7, 7, "the truncated triangle") + expect_untouched(t, &bmp, 5, 5, "the truncated triangle") +} + +// A mesh stream that ends between two whole vertices is not an error either. +@(test) +test_shading_type5_partial_row :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0 + 0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1 + 0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0 -- and no partner + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // Half a row has no triangles in it, so nothing is drawn at all. + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_untouched(t, &bmp, x, y, "an incomplete lattice row") + } + } +} + +@(test) +test_shading_mesh_empty_stream :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + empty: [0]u8 + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, empty[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + expect_untouched(t, &bmp, 4, 4, "an empty mesh") +} + +// --------------------------------------------------------------------------- +// Types 6 and 7: Coons and tensor patches +// --------------------------------------------------------------------------- + +@(private = "file") +PATCH_DICT :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] >>" + +// A flat Coons patch: every control point sits on the bilinear surface of a +// 24x24 square, spaced in thirds, and the corner colours vary in x alone so +// that the patch's bilinear colour and a triangle mesh's linear one agree +// everywhere. It must then paint exactly what the equivalent lattice paints. +@(test) +test_shading_type6_flat_patch_matches_mesh :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + patch := [?]u8{ + 0x00, // flag 0: a whole patch follows + 0x00, 0x00, 0x00, 0x00, // p1 (0,0) + 0x00, 0x08, 0x00, 0x00, // p2 (8,0) + 0x00, 0x10, 0x00, 0x00, // p3 (16,0) + 0x00, 0x18, 0x00, 0x00, // p4 (24,0) + 0x00, 0x18, 0x00, 0x08, // p5 (24,8) + 0x00, 0x18, 0x00, 0x10, // p6 (24,16) + 0x00, 0x18, 0x00, 0x18, // p7 (24,24) + 0x00, 0x10, 0x00, 0x18, // p8 (16,24) + 0x00, 0x08, 0x00, 0x18, // p9 (8,24) + 0x00, 0x00, 0x00, 0x18, // p10 (0,24) + 0x00, 0x00, 0x00, 0x10, // p11 (0,16) + 0x00, 0x00, 0x00, 0x08, // p12 (0,8) + 0x00, 0xFF, 0xFF, 0x00, // c1 at p1, c2 at p4, c3 at p7, c4 at p10 + } + sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, patch[:]) + bmp := sh_target(t, 24, 24) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + mesh := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0 + 0x00, 0x18, 0x00, 0x00, 0xFF, // (24,0) grey 1 + 0x00, 0x00, 0x00, 0x18, 0x00, // (0,24) grey 0 + 0x00, 0x18, 0x00, 0x18, 0xFF, // (24,24) grey 1 + } + sh2 := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", mesh[:]) + ref := sh_target(t, 24, 24) + defer bitmap_destroy(&ref) + sh_paint(t, doc, sh2, &ref) + + expect_all_painted(t, &bmp, "the patch covers its square") + expect_all_painted(t, &ref, "the mesh covers its square") + for y in 0 ..< 24 { + for x in 0 ..< 24 { + expect_pixel(t, &bmp, x, y, sh_pixel(&ref, x, y), 1, "patch against mesh") + } + } + // And both are the ramp they were built to be, so a pair of matching but + // equally wrong pictures cannot pass. + expect_gray(t, &bmp, 0, 12, sh_gray(0.5 / 24), 1, "left edge") + expect_gray(t, &bmp, 23, 12, sh_gray(23.5 / 24), 1, "right edge") +} + +// The same square as a tensor patch: the four interior control points are +// written out instead of derived, and put where the Coons rule would have put +// them, so the picture must not change. +@(test) +test_shading_type7_matches_coons :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + coons := [?]u8{ + 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18, + 0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, + 0x00, 0xFF, 0xFF, 0x00, + } + tensor := [?]u8{ + 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18, + 0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, + // p13 [1][1], p14 [1][2], p15 [2][2], p16 [2][1] + 0x00, 0x08, 0x00, 0x08, 0x00, 0x10, 0x00, 0x08, + 0x00, 0x10, 0x00, 0x10, 0x00, 0x08, 0x00, 0x10, + 0x00, 0xFF, 0xFF, 0x00, + } + + a := sh_target(t, 24, 24) + defer bitmap_destroy(&a) + sh_paint(t, doc, sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, coons[:]), &a) + + b := sh_target(t, 24, 24) + defer bitmap_destroy(&b) + sh_paint(t, doc, sh_load_stream(t, doc, "<< /ShadingType 7 " + PATCH_DICT, tensor[:]), &b) + + expect_all_painted(t, &a, "the Coons patch") + for y in 0 ..< 24 { + for x in 0 ..< 24 { + expect_pixel(t, &b, x, y, sh_pixel(&a, x, y), 1, "tensor against Coons") + } + } +} + +// Two patches, the second continuing from the first's D1 edge with a flag of +// 1. The pair tiles a 24x24 square with a ramp that runs up and back down, and +// the seam down the middle must be complete. +@(test) +test_shading_type6_shared_edge :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + // Patch 1: x from 0 to 12, black at the left edge, white at x = 12. + 0x00, + 0x00, 0x00, 0x00, 0x00, // p1 (0,0) + 0x00, 0x04, 0x00, 0x00, // p2 (4,0) + 0x00, 0x08, 0x00, 0x00, // p3 (8,0) + 0x00, 0x0C, 0x00, 0x00, // p4 (12,0) + 0x00, 0x0C, 0x00, 0x08, // p5 (12,8) + 0x00, 0x0C, 0x00, 0x10, // p6 (12,16) + 0x00, 0x0C, 0x00, 0x18, // p7 (12,24) + 0x00, 0x08, 0x00, 0x18, // p8 (8,24) + 0x00, 0x04, 0x00, 0x18, // p9 (4,24) + 0x00, 0x00, 0x00, 0x18, // p10 (0,24) + 0x00, 0x00, 0x00, 0x10, // p11 (0,16) + 0x00, 0x00, 0x00, 0x08, // p12 (0,8) + 0x00, 0xFF, 0xFF, 0x00, // c1..c4 + // Patch 2: flag 1 takes p4 p5 p6 p7 and c2 c3 as its first edge, so + // only eight points and two colours are written. + 0x01, + 0x00, 0x10, 0x00, 0x18, // (16,24) + 0x00, 0x14, 0x00, 0x18, // (20,24) + 0x00, 0x18, 0x00, 0x18, // (24,24) + 0x00, 0x18, 0x00, 0x10, // (24,16) + 0x00, 0x18, 0x00, 0x08, // (24,8) + 0x00, 0x18, 0x00, 0x00, // (24,0) + 0x00, 0x14, 0x00, 0x00, // (20,0) + 0x00, 0x10, 0x00, 0x00, // (16,0) + 0x00, 0x00, // c3, c4: black again at x = 24 + } + sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, payload[:]) + bmp := sh_target(t, 24, 24) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "two patches sharing an edge") + expect_gray(t, &bmp, 0, 12, sh_gray(0.5 / 12), 1, "start of the first patch") + expect_gray(t, &bmp, 11, 12, sh_gray(11.5 / 12), 1, "end of the first patch") + expect_gray(t, &bmp, 12, 12, sh_gray(11.5 / 12), 1, "start of the second patch") + expect_gray(t, &bmp, 23, 12, sh_gray(0.5 / 12), 1, "end of the second patch") + expect_gray(t, &bmp, 5, 3, sh_gray(5.5 / 12), 1, "first patch, another row") + expect_gray(t, &bmp, 18, 20, sh_gray(5.5 / 12), 1, "second patch, another row") +} + +// A patch with curved edges still has to stay inside the box its control +// points span, and cover the corners it is anchored at. +@(test) +test_shading_type6_curved_patch :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + // The top edge bulges downwards: p2 and p3 are pulled to y = 12 instead of + // sitting on the straight edge at y = 0. + payload := [?]u8{ + 0x00, + 0x00, 0x00, 0x00, 0x00, // p1 (0,0) + 0x00, 0x08, 0x00, 0x0C, // p2 (8,12) + 0x00, 0x10, 0x00, 0x0C, // p3 (16,12) + 0x00, 0x18, 0x00, 0x00, // p4 (24,0) + 0x00, 0x18, 0x00, 0x08, // p5 (24,8) + 0x00, 0x18, 0x00, 0x10, // p6 (24,16) + 0x00, 0x18, 0x00, 0x18, // p7 (24,24) + 0x00, 0x10, 0x00, 0x18, // p8 (16,24) + 0x00, 0x08, 0x00, 0x18, // p9 (8,24) + 0x00, 0x00, 0x00, 0x18, // p10 (0,24) + 0x00, 0x00, 0x00, 0x10, // p11 (0,16) + 0x00, 0x00, 0x00, 0x08, // p12 (0,8) + 0x00, 0xFF, 0xFF, 0x00, + } + sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, payload[:]) + bmp := sh_target(t, 24, 24) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // The top edge is x = 24v, y = 36v(1-v): it dives away from the corner at + // once, so the whole top of the square is outside the patch. + expect_untouched(t, &bmp, 12, 1, "under the bulge") + expect_untouched(t, &bmp, 12, 8, "under the bulge") + expect_untouched(t, &bmp, 0, 0, "beside the bulge, where the edge has already dropped") + + // The bottom edge is straight and its colour runs left to right along it. + for x in 0 ..< 24 { + testing.expectf(t, sh_pixel(&bmp, x, 23) != SH_BG, + "the bottom row of a curved patch: pixel (%d,23) was never painted", x) + } + left := int(sh_pixel(&bmp, 1, 23)[0]) + mid := int(sh_pixel(&bmp, 12, 23)[0]) + right := int(sh_pixel(&bmp, 22, 23)[0]) + testing.expectf(t, left < mid && mid < right, + "the ramp should run left to right along the bottom edge: %d %d %d", left, mid, right) + testing.expectf(t, abs(mid - 128) <= 24, "the middle of the bottom edge is mid grey: %d", mid) +} + +// --------------------------------------------------------------------------- +// Clipping, alpha, blend mode and soft mask +// --------------------------------------------------------------------------- + +// A shading that is one flat colour everywhere, for testing what happens to it +// on the way into the bitmap rather than what colour it computes. +@(private = "file") +FLAT_HALF :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0.5] /C1 [0.5] /N 1 >> >>" + +@(private = "file") +FLAT_WHITE :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " + + "/Function << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [1] /N 1 >> >>" + +@(test) +test_shading_paint_respects_clip :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + + err := shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, Raster_Clip{2, 3, 5, 6}, 1, .Normal, nil) + testing.expect_value(t, err, Error.None) + + for y in 0 ..< 8 { + for x in 0 ..< 8 { + inside := x >= 2 && x < 5 && y >= 3 && y < 6 + if inside { + expect_gray(t, &bmp, x, y, 255, 0, "inside the clip") + } else { + expect_untouched(t, &bmp, x, y, "outside the clip") + } + } + } +} + +@(test) +test_shading_paint_clip_outside_bitmap :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + + // A clip larger than the bitmap is clamped, not trusted. + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{-100, -100, 1000, 1000}, 1, .Normal, nil), Error.None) + expect_gray(t, &bmp, 0, 0, 255, 0, "painted") + expect_gray(t, &bmp, 7, 7, 255, 0, "painted") + + // And one entirely off it paints nothing. + bmp2 := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp2) + testing.expect_value(t, shading_paint(doc, sh, &bmp2, MATRIX_IDENTITY, + Raster_Clip{20, 20, 30, 30}, 1, .Normal, nil), Error.None) + expect_untouched(t, &bmp2, 4, 4, "a clip off the bitmap") +} + +@(test) +test_shading_paint_alpha :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 4, 4) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, [4]u8{0, 0, 0, 255}) + + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 4, 4}, 0.5, .Normal, nil), Error.None) + expect_gray(t, &bmp, 1, 1, 128, 1, "half of white over black") + + // Zero alpha is not "paint nothing visible", it is "do not touch". + bmp2 := sh_target(t, 4, 4) + defer bitmap_destroy(&bmp2) + testing.expect_value(t, shading_paint(doc, sh, &bmp2, MATRIX_IDENTITY, + Raster_Clip{0, 0, 4, 4}, 0, .Normal, nil), Error.None) + expect_untouched(t, &bmp2, 2, 2, "alpha 0") +} + +@(test) +test_shading_paint_blend_mode :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_HALF) + bmp := sh_target(t, 4, 4) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, [4]u8{255, 0, 0, 255}) + + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 4, 4}, 1, .Multiply, nil), Error.None) + // Multiply against red keeps the red channel and kills the other two; + // Normal would have painted a flat grey. + expect_pixel(t, &bmp, 2, 2, [3]u8{128, 0, 0}, 1, "half grey multiplied over red") +} + +@(test) +test_shading_paint_soft_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 8, 2) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, [4]u8{0, 0, 0, 255}) + + mask, mask_err := bitmap_make(8, 2, 1) + testing.expect_value(t, mask_err, Error.None) + defer bitmap_destroy(&mask) + for y in 0 ..< 2 { + row := bitmap_row(&mask, y) + for x in 0 ..< 8 { + row[x] = x < 3 ? 0 : (x < 6 ? 128 : 255) + } + } + + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 8, 2}, 1, .Normal, &mask), Error.None) + + expect_gray(t, &bmp, 0, 0, 0, 0, "masked out entirely") + expect_gray(t, &bmp, 4, 0, 128, 1, "half masked") + expect_gray(t, &bmp, 7, 0, 255, 0, "not masked") +} + +// A mask narrower than the clip must show the content it does not cover +// rather than erase it. +@(test) +test_shading_paint_short_mask :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 8, 4) + defer bitmap_destroy(&bmp) + bitmap_fill(&bmp, [4]u8{0, 0, 0, 255}) + + mask, mask_err := bitmap_make(3, 1, 1) + testing.expect_value(t, mask_err, Error.None) + defer bitmap_destroy(&mask) + bitmap_clear(&mask, 0) + + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 8, 4}, 1, .Normal, &mask), Error.None) + expect_gray(t, &bmp, 0, 0, 0, 0, "covered by the mask") + expect_gray(t, &bmp, 5, 0, 255, 0, "past the mask in x") + expect_gray(t, &bmp, 0, 3, 255, 0, "past the mask in y") +} + +// --------------------------------------------------------------------------- +// Degenerate transforms and arguments +// --------------------------------------------------------------------------- + +@(test) +test_shading_singular_ctm_paints_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + singular := [?]Matrix{ + {0, 0, 0, 0, 0, 0}, // collapses everything to a point + {1, 0, 0, 0, 0, 0}, // flattens y + {0, 0, 0, 1, 0, 0}, // flattens x + {2, 4, 1, 2, 3, 3}, // rank 1: the rows are parallel + } + sources := [?]string{ + FLAT_WHITE, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 0 4 4 4] " + RAMP + + " /Extend [true true] >>", + } + for src in sources { + sh := sh_load(t, doc, src) + for m in singular { + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + testing.expect_value(t, shading_paint(doc, sh, &bmp, m, + Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None) + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_untouched(t, &bmp, x, y, "a singular CTM") + } + } + } + } +} + +@(test) +test_shading_singular_ctm_mesh :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, + } + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + testing.expect_value(t, shading_paint(doc, sh, &bmp, Matrix{0, 0, 0, 0, 0, 0}, + Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None) + expect_untouched(t, &bmp, 4, 4, "a singular CTM on a mesh") +} + +@(test) +test_shading_non_finite_ctm_paints_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + + big := f64(1e308) + inf := big * 10 + testing.expect_value(t, shading_paint(doc, sh, &bmp, Matrix{1, 0, 0, 1, inf, 0}, + Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None) + expect_untouched(t, &bmp, 4, 4, "an infinite CTM") +} + +@(test) +test_shading_paint_bad_arguments :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, FLAT_WHITE) + bmp := sh_target(t, 4, 4) + defer bitmap_destroy(&bmp) + + testing.expect_value(t, shading_paint(doc, nil, &bmp, MATRIX_IDENTITY, + Raster_Clip{0, 0, 4, 4}, 1, .Normal, nil), Error.Invalid_Argument) + testing.expect_value(t, shading_paint(doc, sh, nil, MATRIX_IDENTITY, + Raster_Clip{0, 0, 4, 4}, 1, .Normal, nil), Error.Invalid_Argument) + // An empty clip is not an error, it is a no-op. + testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, + Raster_Clip{4, 4, 4, 4}, 1, .Normal, nil), Error.None) + expect_untouched(t, &bmp, 0, 0, "an empty clip") +} + +// A /None separation marks nothing at all. +@(test) +test_shading_none_colorspace_paints_nothing :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace [/Separation /None /DeviceGray " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>] " + + "/Coords [0 0 8 0] /Extend [true true] " + RAMP + " >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + expect_untouched(t, &bmp, 4, 4, "a /None separation") +} + +// --------------------------------------------------------------------------- +// /BBox +// --------------------------------------------------------------------------- + +@(test) +test_shading_bbox_limits_painting :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " + + "/BBox [2 2 6 6] " + RAMP + " >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + for y in 0 ..< 8 { + for x in 0 ..< 8 { + if x >= 2 && x <= 5 && y >= 2 && y <= 5 { + testing.expectf(t, sh_pixel(&bmp, x, y) != SH_BG, + "inside /BBox: pixel (%d,%d) was never painted", x, y) + } else { + expect_untouched(t, &bmp, x, y, "outside /BBox") + } + } + } +} + +@(test) +test_shading_bbox_limits_mesh :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 4 /BBox [0 0 4 8] " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 3, 4, sh_gray(3.5 / 8), 1, "inside /BBox") + expect_untouched(t, &bmp, 4, 4, "outside /BBox") + expect_untouched(t, &bmp, 7, 0, "outside /BBox") +} + +// --------------------------------------------------------------------------- +// Loading: what is refused +// --------------------------------------------------------------------------- + +@(private = "file") +expect_refused :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) { + sh, err := shading_load(doc, cf_parse(doc, src), nil) + testing.expect_value(t, err, Error.Bad_Shading, loc = loc) + testing.expect(t, sh == nil, "a refused shading must not be handed back", loc = loc) +} + +@(private = "file") +expect_stream_refused :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, + loc := #caller_location) { + sh, err := shading_load(doc, cf_stream(doc, dict_src, payload), nil) + testing.expect_value(t, err, Error.Bad_Shading, loc = loc) + testing.expect(t, sh == nil, "a refused shading must not be handed back", loc = loc) +} + +@(test) +test_shading_load_refuses_malformed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + expect_refused(t, doc, "<< >>") + expect_refused(t, doc, "42") + expect_refused(t, doc, "<< /ShadingType 0 /ColorSpace /DeviceGray >>") + expect_refused(t, doc, "<< /ShadingType 8 /ColorSpace /DeviceGray >>") + // No colour space at all. + expect_refused(t, doc, "<< /ShadingType 2 /Coords [0 0 8 0] " + RAMP + " >>") + // 8.7.4.5.1: a shading's colour space shall not be a Pattern space. + expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /Pattern /Coords [0 0 8 0] " + RAMP + " >>") + // Types 1 to 3 have no colour without a function. + expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] >>") + expect_refused(t, doc, "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 0 0 8 0 8] >>") + // Not enough coordinates. + expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8] " + RAMP + " >>") + expect_refused(t, doc, "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 0 0 8 0] " + RAMP + " >>") + // A mesh type has to be a stream: a dictionary carries no vertices. + expect_refused(t, doc, "<< /ShadingType 4 " + MESH_DICT) + expect_refused(t, doc, "<< /ShadingType 6 " + PATCH_DICT) + + _, nil_err := shading_load(nil, nil, nil) + testing.expect_value(t, nil_err, Error.Invalid_Argument) +} + +@(test) +test_shading_load_refuses_bad_mesh_layout :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + payload := [?]u8{0, 0, 0, 0, 0, 0, 0, 0} + + base :: "/ColorSpace /DeviceGray /Decode [0 65535 0 65535 0 1] " + // /BitsPerCoordinate is one of 1 2 4 8 12 16 24 32. + expect_stream_refused(t, doc, "<< /ShadingType 4 " + base + + "/BitsPerCoordinate 64 /BitsPerComponent 8 /BitsPerFlag 8 >>", payload[:]) + expect_stream_refused(t, doc, "<< /ShadingType 4 " + base + + "/BitsPerCoordinate 0 /BitsPerComponent 8 /BitsPerFlag 8 >>", payload[:]) + // /BitsPerComponent is one of 1 2 4 8 12 16. + expect_stream_refused(t, doc, "<< /ShadingType 4 " + base + + "/BitsPerCoordinate 16 /BitsPerComponent 7 /BitsPerFlag 8 >>", payload[:]) + // /BitsPerFlag is one of 2 4 8. + expect_stream_refused(t, doc, "<< /ShadingType 4 " + base + + "/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 3 >>", payload[:]) + // /Decode has to describe every field of a vertex. + expect_stream_refused(t, doc, "<< /ShadingType 4 /ColorSpace /DeviceGray " + + "/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 1 0 1] >>", payload[:]) + expect_stream_refused(t, doc, "<< /ShadingType 4 /ColorSpace /DeviceRGB " + + "/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 1 0 1 0 1] >>", payload[:]) +} + +// An absurd /VerticesPerRow is refused outright rather than sizing a buffer. +@(test) +test_shading_load_refuses_absurd_vertex_count :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + payload := [?]u8{0, 0, 0, 0, 0, 0, 0, 0} + + lattice :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/Decode [0 65535 0 65535 0 1] " + expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 99999999 >>", payload[:]) + expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 2147483647 >>", payload[:]) + expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow -4 >>", payload[:]) + // One vertex per row cannot form a triangle. + expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 1 >>", payload[:]) + expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + ">>", payload[:]) + + // The largest row the library will take still loads. + sh, err := shading_load(doc, cf_stream(doc, + "<< /ShadingType 5 " + lattice + "/VerticesPerRow 16384 >>", payload[:]), nil) + testing.expect_value(t, err, Error.None) + testing.expect(t, sh != nil, "a row at the limit should still load") +} + +// Without a /Function a mesh vertex carries one value per colour component, +// which is what makes a DeviceRGB vertex seven bytes wide here rather than +// five. +@(test) +test_shading_mesh_multi_component_vertices :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, // (0,0) red + 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0xFF, // (8,0) blue + 0x00, 0x00, 0x00, 0x08, 0xFF, 0x00, 0x00, // (0,8) red + 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0xFF, // (8,8) blue + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1 0 1 0 1] >>", payload[:]) + testing.expect_value(t, sh.n_comp, 3) + + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "an RGB lattice") + for x in 0 ..< 8 { + s := (f64(x) + 0.5) / 8 + expect_pixel(t, &bmp, x, 4, [3]u8{sh_gray(1 - s), 0, sh_gray(s)}, 2, "red to blue") + } +} + +// --------------------------------------------------------------------------- +// Bounds +// --------------------------------------------------------------------------- + +@(test) +test_shading_bounds :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + testing.expect_value(t, shading_bounds(nil, MATRIX_IDENTITY), RECT_EMPTY) + + // An axial shading is unbounded across its axis, whatever /Extend says. + axial := sh_load(t, doc, AXIAL_SRC + " >>") + testing.expect_value(t, shading_bounds(axial, MATRIX_IDENTITY), RECT_INFINITE) + + // /BBox is what bounds it, and it is in the shading's own space. + boxed := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /BBox [1 2 3 4] " + RAMP + " >>") + testing.expect_value(t, shading_bounds(boxed, MATRIX_IDENTITY), Rect{1, 2, 3, 4}) + testing.expect_value(t, shading_bounds(boxed, scale_matrix(2, 2)), Rect{2, 4, 6, 8}) + + // An unextended radial reaches no further than its two circles. + radial := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 1 10 4 2] " + RAMP + " >>") + testing.expect_value(t, shading_bounds(radial, MATRIX_IDENTITY), Rect{3, 2, 12, 6}) + testing.expect_value(t, shading_bounds(radial, scale_matrix(2, 2)), Rect{6, 4, 24, 12}) + + // An extended one keeps going. + radial_ext := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 1 10 4 2] /Extend [false true] " + + RAMP + " >>") + testing.expect_value(t, shading_bounds(radial_ext, MATRIX_IDENTITY), RECT_INFINITE) + + // A function-based shading is bounded by its domain through /Matrix. + fb := sh_type1(t, doc, + "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>") + testing.expect_value(t, shading_bounds(fb, MATRIX_IDENTITY), Rect{0, 0, 8, 8}) + testing.expect_value(t, shading_bounds(fb, translate_matrix(10, 20)), Rect{10, 20, 18, 28}) + + // A mesh is bounded by /Decode without reading a byte of the stream. + payload := [?]u8{0, 0, 0, 0, 0, 0} + mesh := sh_load_stream(t, doc, + "<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 8 /Decode [10 20 30 40 0 1] >>", payload[:]) + testing.expect_value(t, shading_bounds(mesh, MATRIX_IDENTITY), Rect{10, 30, 20, 40}) + testing.expect_value(t, shading_bounds(mesh, scale_matrix(2, 2)), Rect{20, 60, 40, 80}) +} + +// A rotated CTM turns the bounding box of a mesh into the box of the rotated +// box, which is what a caller intersecting it with a clip needs. +@(test) +test_shading_bounds_rotated :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{0, 0, 0, 0, 0, 0} + mesh := sh_load_stream(t, doc, + "<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 8 /Decode [0 10 0 20 0 1] >>", payload[:]) + got := shading_bounds(mesh, rotate_matrix(90)) + testing.expect(t, abs(got.x0 - (-20)) < 1e-9 && abs(got.y0 - 0) < 1e-9 && + abs(got.x1 - 0) < 1e-9 && abs(got.y1 - 10) < 1e-9, + "a rotated mesh bound") +} + +// --------------------------------------------------------------------------- +// Mesh bit packing and the remaining edge flags +// --------------------------------------------------------------------------- + +// The same two triangles as test_shading_type4_shared_edge, but with a 2-bit +// flag, so a vertex is 42 bits and the reader has to pad each record out to +// the next byte (8.7.4.5.5) and pull every field across byte boundaries. The +// bytes were packed by hand from the field widths: 2 bits of flag, 16 of x, +// 16 of y, 8 of colour, then 6 bits of padding. +@(test) +test_shading_type4_bit_packed :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0 + 0x00, 0x02, 0x00, 0x00, 0x3F, 0xC0, // flag 0, (8,0), grey 1 + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, // flag 0, (0,8), grey 0 + 0x40, 0x02, 0x00, 0x02, 0x3F, 0xC0, // flag 1, (8,8), grey 1 + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " + + "/BitsPerFlag 2 /Decode [0 65535 0 65535 0 1] >>", payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "a bit-packed mesh") + for y in 0 ..< 8 { + for x in 0 ..< 8 { + expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp") + } + } +} + +// A flag of 2 keeps the previous triangle's first and third vertices, where a +// flag of 1 keeps its second and third. The two produce different triangles +// from the same bytes, which is what this pins down. +@(test) +test_shading_type4_flag_two :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, va (0,0), grey 0 + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, vb (8,0), grey 1 + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, vc (0,8), grey 0 + 0x02, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 2: va, vc and (8,8) + } + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + // The second triangle is (0,0) (0,8) (8,8): below the main diagonal. + expect_gray(t, &bmp, 2, 7, sh_gray(2.5 / 8), 1, "inside the flag-2 triangle") + expect_gray(t, &bmp, 1, 1, sh_gray(1.5 / 8), 1, "inside the first triangle") + // A flag of 1 would have made it (8,0) (0,8) (8,8) and covered this pixel. + expect_untouched(t, &bmp, 6, 2, "covered by neither triangle") +} + +// A continuation flag with no triangle to continue is malformed. Starting a +// fresh triangle loses the least, and must not lose the whole mesh. +@(test) +test_shading_type4_leading_continuation_flag :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 1 with nothing before it + 0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + } + sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_gray(t, &bmp, 1, 1, sh_gray(1.5 / 8), 1, "the recovered triangle") + expect_untouched(t, &bmp, 7, 7, "outside it") +} + +// /Background is parsed but deliberately not painted: it applies to a pattern +// fill's uncovered area, not to the shading itself. +@(test) +test_shading_background_is_not_painted :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] /Background [1] " + + RAMP + " /Extend [false false] >>") + testing.expect(t, sh.has_background, "/Background should be read") + testing.expect_value(t, len(sh.background), 1) + + bmp := sh_target(t, 13, 1) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + expect_untouched(t, &bmp, 0, 0, "before the axis") + expect_untouched(t, &bmp, 12, 0, "past the axis") +} + +// --------------------------------------------------------------------------- +// The rules that only a rotated CTM or an extended end can reach +// --------------------------------------------------------------------------- + +// An extension continues the family of circles beyond the ends, but only while +// the radius stays non-negative (8.7.4.5.4). Here the circles shrink to a +// point at (6,4); behind that point the radius would be negative, so the +// mirror image of the cone must stay unpainted even though both ends are +// extended. +@(test) +test_shading_radial_extension_stops_at_zero_radius :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [6 4 0 0 4 3] " + RAMP + + " /Extend [true true] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_untouched(t, &bmp, 7, 4, "behind the apex, where the radius goes negative") + testing.expect(t, sh_pixel(&bmp, 2, 4) != SH_BG, "inside the cone") + testing.expect(t, sh_pixel(&bmp, 0, 4) != SH_BG, "the extended far end") +} + +// A patch whose colour runs down the page rather than across it. The u +// direction of the patch is the one the twelve control points walk down the +// sides, so this pins the other half of the point-to-grid mapping that a +// horizontal ramp cannot see. +@(test) +test_shading_type6_vertical_ramp_matches_mesh :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + patch := [?]u8{ + 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18, + 0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0xFF, 0xFF, // black along the top edge, white along the bottom + } + sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, patch[:]) + bmp := sh_target(t, 24, 24) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + mesh := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0 + 0x00, 0x18, 0x00, 0x00, 0x00, // (24,0) grey 0 + 0x00, 0x00, 0x00, 0x18, 0xFF, // (0,24) grey 1 + 0x00, 0x18, 0x00, 0x18, 0xFF, // (24,24) grey 1 + } + sh2 := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", mesh[:]) + ref := sh_target(t, 24, 24) + defer bitmap_destroy(&ref) + sh_paint(t, doc, sh2, &ref) + + expect_all_painted(t, &bmp, "the patch covers its square") + for y in 0 ..< 24 { + for x in 0 ..< 24 { + expect_pixel(t, &bmp, x, y, sh_pixel(&ref, x, y), 1, "patch against mesh") + } + } + expect_gray(t, &bmp, 12, 0, sh_gray(0.5 / 24), 1, "top edge") + expect_gray(t, &bmp, 12, 23, sh_gray(23.5 / 24), 1, "bottom edge") +} + +// rot45 turns 45 degrees about the middle of an 8x8 bitmap. +@(private = "file") +rot45 :: proc() -> Matrix { + return matrix_mul(matrix_mul(translate_matrix(-4, -4), rotate_matrix(45)), + translate_matrix(4, 4)) +} + +// /BBox is a rectangle in the shading's own space, so under a rotated CTM it +// is a diamond on the page. Testing a pixel against the transformed box's +// bounding rectangle would paint the corners of the diamond's bounding box; +// the test has to happen in the shading's space. +@(test) +test_shading_bbox_under_rotation :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_load(t, doc, + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " + + "/BBox [2 2 6 6] " + RAMP + " >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp, rot45()) + + testing.expect(t, sh_pixel(&bmp, 4, 4) != SH_BG, "the middle of the diamond") + // (1.5, 1.5) is inside the diamond's bounding box and outside the diamond. + expect_untouched(t, &bmp, 1, 1, "a corner of the rotated box's bounds") + expect_untouched(t, &bmp, 6, 6, "a corner of the rotated box's bounds") +} + +// The same argument for a function-based shading's /Domain. +@(test) +test_shading_function_based_domain_under_rotation :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + sh := sh_type1(t, doc, + "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>") + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp, rot45()) + + // Device (4.5, 4.5) maps back to (4.707, 4) in the shading's space, which + // /Matrix turns into a domain x of 0.588. + expect_gray(t, &bmp, 4, 4, sh_gray(4.7071067 / 8), 1, "the middle of the diamond") + expect_untouched(t, &bmp, 0, 0, "a corner outside the rotated domain") + expect_untouched(t, &bmp, 7, 7, "a corner outside the rotated domain") +} + +// A Pattern space in its array form has a component count, so the check that +// rejects it has to be on the kind and not on the count. +@(test) +test_shading_load_refuses_pattern_space :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + expect_refused(t, doc, + "<< /ShadingType 2 /ColorSpace [/Pattern /DeviceRGB] /Coords [0 0 8 0] " + + "/Function [<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " + + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>] >>") +} + +// Every mesh above uses a /Decode that happens to be the identity on the raw +// samples, which is what makes the payloads readable. This one does not: the +// coordinate range is twice the sample range, so a raw 4 is a device 8, and +// the colour range runs backwards, so a raw 0 is white. +@(test) +test_shading_mesh_decode_ranges :: proc(t: ^testing.T) { + doc := cf_open(t) + defer cf_close(t, doc) + + payload := [?]u8{ + 0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) -> device (0,0), grey 1 + 0x00, 0x04, 0x00, 0x00, 0xFF, // (4,0) -> device (8,0), grey 0 + 0x00, 0x00, 0x00, 0x04, 0x00, // (0,4) -> device (0,8), grey 1 + 0x00, 0x04, 0x00, 0x04, 0xFF, // (4,4) -> device (8,8), grey 0 + } + sh := sh_load_stream(t, doc, + "<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " + + "/BitsPerComponent 8 /Decode [0 131070 0 131070 1 0] >>", payload[:]) + bmp := sh_target(t, 8, 8) + defer bitmap_destroy(&bmp) + sh_paint(t, doc, sh, &bmp) + + expect_all_painted(t, &bmp, "a mesh whose /Decode doubles its coordinates") + for x in 0 ..< 8 { + expect_gray(t, &bmp, x, 4, sh_gray(1 - (f64(x) + 0.5) / 8), 1, "a reversed colour ramp") + } +} diff --git a/stroke.odin b/stroke.odin new file mode 100644 index 0000000..6f1e2b6 --- /dev/null +++ b/stroke.odin @@ -0,0 +1,831 @@ +package pdf + +import "core:math" + +// --------------------------------------------------------------------------- +// Stroking (ISO 32000-2, 8.4.3) +// +// A stroke is converted into an outline that is then FILLED with the nonzero +// rule. Even-odd would be wrong and not merely different: the two sides of a +// stroke overlap wherever a join doubles back, wherever a path crosses itself +// and wherever two dashes land on the same ink, and even-odd punches holes in +// exactly those places. +// +// The outline is built in USER space and then transformed. Line width is a +// user-space quantity (8.4.3.2), so an anisotropic CTM genuinely makes the pen +// elliptical: under `1 0 0 4 0 0 cm` a horizontal line is painted four times +// as thick as a vertical one of the same width. Scaling the width by +// matrix_expansion and stroking in device space would paint both at twice the +// width, which is wrong for both. +// +// Every contour emitted here is wound the same way (clockwise in build space, +// and so consistently one way in device space whatever the CTM does), with the +// single deliberate exception of the inner ring of a closed subpath, which is +// wound the other way so the nonzero rule leaves the hole unpainted. The +// consistency is load-bearing: two overlapping dash pieces wound oppositely +// would cancel and leave a hole where there should be ink. +// --------------------------------------------------------------------------- + +// HAIRLINE_WIDTH is the device-space width of the thinnest line the renderer +// will draw. One pixel: "a line width of 0 shall denote the thinnest line that +// can be rendered at device resolution" (8.4.3.2), and the same has to apply +// to any width that would come out thinner, or the table rules, underlines and +// form boxes that fill ordinary business documents vanish at screen scale. +@(private) +HAIRLINE_WIDTH :: 1.0 + +// MAX_ARC_SEGMENTS bounds one round join or cap, MAX_CIRCLE_SEGMENTS one dot. +// The segment count is normally driven by the flatness tolerance; the cap only +// bites for a pen thousands of pixels across, where the extra error is a +// fraction of a percent of the radius. +@(private) +MAX_ARC_SEGMENTS :: 128 +@(private) +MAX_CIRCLE_SEGMENTS :: 256 + +// MAX_DASH_PIECES is the hard ceiling on dashes generated from one path, and +// DASH_UNBOUNDED is the "rest of the path is solid" sentinel used once it is +// hit. A finite sentinel rather than an infinity, because no infinity may +// reach the geometry. +@(private) +MAX_DASH_PIECES :: 100_000 +@(private) +DASH_UNBOUNDED :: 1e30 + +// MAX_STROKE_POINTS bounds the whole outline. A path with a million segments +// and round joins would otherwise turn into hundreds of megabytes of geometry; +// a truncated stroke is a much better failure than an exhausted heap. +@(private) +MAX_STROKE_POINTS :: 1 << 21 + +// stroke_cull grows the caller's clip by the farthest a stroke of this pen can +// paint from the path it follows: half the widest device-space thickness -- +// floored at a hairline, because a hairline pen is one device pixel wide +// however thin the file said the line was -- times the miter limit for the +// spike a join can add, and two pixels of slack for the flattening tolerance +// and the rounding at the edge. Nothing outside that can mark the clip, so a +// curve that stays outside it may be replaced by its chord. +@(private) +stroke_cull :: proc(cull: Maybe(Rect), width, hi, miter: f64, dashed: bool) -> Maybe(Rect) { + box, have := cull.? + if !have || dashed { return nil } + reach := max(width * hi, HAIRLINE_WIDTH) * 0.5 * miter + 2 + if !is_finite(reach) { return nil } + return Rect{box.x0 - reach, box.y0 - reach, box.x1 + reach, box.y1 + reach} +} + +// stroke_flatten converts the stroke of `path` under `state` into a fillable +// outline, appending it to `out`. The result is in DEVICE space, already +// flattened, and must be filled with the NONZERO rule. +// +// Anything already in `out` is kept, so several strokes can be accumulated +// into one path -- which is what a clip built from `S n` needs. +// +// `flatness` is the maximum distance, in device pixels, between the true +// outline and the polygon that replaces it. +// +// `cull`, when given, is the device-space region the outline will be drawn +// through. It is passed on to `path_flatten` grown by the farthest the pen can +// reach from the path, so a curve that cannot paint into the clip is not +// subdivided -- unless the stroke is dashed, where it must be: dashes are laid +// out by arc length along the flattened polyline, so shortening an off-screen +// excursion would shift the phase of every dash after it, including the ones +// on screen. +stroke_flatten :: proc(out: ^Flat_Path, path: ^Path, state: ^Stroke_State, + ctm: Matrix, flatness := FLATNESS, cull: Maybe(Rect) = nil) -> Error { + if out == nil || path == nil { return .Invalid_Argument } + if !matrix_is_finite(ctm) { return .Invalid_Argument } + if len(path.verbs) == 0 { return .None } + + st := state != nil ? state^ : STROKE_STATE_DEFAULT + + // The device-space thickness of a stroke of width w varies with the + // direction of travel between w*lo and w*hi, so these two numbers are + // exactly what the hairline test and the dash-visibility test need. + lo, hi := stroke_scale_extremes(ctm) + if hi == 0 { return .None } + // A singular CTM -- or one so close to singular that its inverse is + // numerical noise -- maps the outline onto a line: zero area, nothing to + // paint. Content is deliberately hidden this way, so it is not an error. + if lo <= hi * 1e-9 { return .None } + inv, invertible := matrix_invert(ctm) + if !invertible { return .None } + + tol_dev := flatness + if !is_finite(tol_dev) || tol_dev <= 0 { tol_dev = FLATNESS } + + width := st.line_width + if !is_finite(width) || width < 0 { width = 0 } + + // The hairline test is against the *widest* thickness the stroke can + // reach. An anisotropic CTM that paints one pixel across one axis and + // fifty across the other is a real stroke, not a hairline, and promoting + // it would replace a wedge of ink with a thread. + hairline := width * hi < HAIRLINE_WIDTH + // ... but the narrow axis of that same wedge still has to be floored, and + // testing only the widest thickness never gets to it. What a stroke of + // width w comes out at in device space depends on the direction it runs + // in -- it is w times the stretch ACROSS that direction, anywhere between + // w*lo and w*hi -- so under `1 0 0 0.02 0 0 cm` a horizontal rule at + // `1 w` is painted 0.02 pixels tall: a 1% grey that reads as blank paper, + // and the same rule at `0 w` is a clean hairline. Widening the pen must + // not make a line disappear. + // + // The pen is an ellipse whenever the CTM is anisotropic, so the fix is to + // floor its short axis at one device pixel and keep its long one: the + // thin direction comes out a hairline, the thick direction keeps its true + // width, and no direction is ever painted thinner than the thinnest line + // the device can draw (8.4.3.2). mupdf and pdfium make this decision with + // a single averaged scale factor instead, which draws this rule but thins + // the wedge case the paragraph above is about. + squashed := !hairline && width * lo < HAIRLINE_WIDTH + sq_post, sq_build, sq_ok := Matrix{}, Matrix{}, false + if squashed { + sq_post, sq_build, sq_ok = stroke_squashed_pen(ctm, width, lo, hi) + } + + s := Stroker{ + out = out, + line_cap = st.line_cap, + line_join = st.line_join, + // A miter limit below 1 is geometrically impossible (the ratio is + // never less than 1) and files do carry 0; clamping keeps a bevel + // from being forced on every join. 8.4.3.5. + miter = is_finite(st.miter_limit) ? max(st.miter_limit, 1) : 10, + } + if hairline { + // A hairline is built directly in device space, so it comes out + // exactly one pixel wide in every direction rather than one pixel + // on average. + s.radius = HAIRLINE_WIDTH * 0.5 + s.to_build = ctm + s.post = MATRIX_IDENTITY + s.post_id = true + s.tol = tol_dev + } else if sq_ok { + // Build space is the one the floored pen is a circle in, exactly as + // for a hairline; the difference is that `post` stretches it back out + // along the CTM's long axis instead of being the identity. + s.radius = HAIRLINE_WIDTH * 0.5 + s.to_build = sq_build + s.post = sq_post + s.tol = max(tol_dev / max(width * hi, 1), 1e-9) + } else { + s.radius = width * 0.5 + s.to_build = MATRIX_IDENTITY + s.build_id = true + s.post = ctm + // hi rather than the average expansion: under an anisotropic CTM the + // stretched axis is the one that shows the faceting. + s.tol = max(tol_dev / hi, 1e-9) + } + s.pts = make([dynamic]Point, 0, 64) + defer stroke_destroy(&s) + + // Flatten into device space so the tolerance means device pixels, then + // pull the polyline back into user space: both the dash pattern and the + // pen are user-space quantities. + scratch := flat_path_make() + defer flat_path_destroy(&scratch) + path_flatten(&scratch, path, ctm, tol_dev, stroke_cull(cull, width, hi, s.miter, + stroke_has_dash(&st))) + if len(scratch.points) == 0 { return .None } + + device_len := 0.0 + for i in 0 ..< len(scratch.subpaths) { + pts := flat_path_slice(&scratch, i) + for k in 1 ..< len(pts) { + device_len += stroke_len(pts[k].x - pts[k - 1].x, pts[k].y - pts[k - 1].y) + } + if scratch.subpaths[i].closed && len(pts) > 1 { + device_len += stroke_len(pts[0].x - pts[len(pts) - 1].x, pts[0].y - pts[len(pts) - 1].y) + } + } + if !is_finite(device_len) || device_len < 0 { device_len = 0 } + + for &p in scratch.points { + p = transform_point(inv, p) + } + + dashed := stroke_has_dash(&st) + dash_count := min(st.dash_count, MAX_DASH_ENTRIES) + total := 0.0 + if dashed { + for i in 0 ..< dash_count { total += st.dash[i] } + // A pattern whose entire cycle is under one device pixel cannot read + // as anything but a solid line, and dashing it would emit one piece + // per sub-pixel step: unbounded geometry for no visible difference. + if !is_finite(total) || total * hi < 1 { dashed = false } + } + + if dashed { + d := Stroke_Dasher{s = &s, count = dash_count, total = total, phase = st.dash_phase} + for i in 0 ..< dash_count { d.pattern[i] = st.dash[i] } + // No more dashes can be told apart than there are pixels along the + // path, so the piece count is capped by the path's device-space + // length. Without it a `[1e-6 1e-6] 0 d` pattern on a 10000-pixel line + // would generate ten billion rectangles. + budget := f64(MAX_DASH_PIECES) + if device_len < budget { budget = device_len } + d.budget = int(budget) + 16 + d.piece = make([dynamic]Point, 0, 64) + d.head = make([dynamic]Point, 0, 16) + defer stroke_dasher_destroy(&d) + for i in 0 ..< len(scratch.subpaths) { + stroke_dash_polyline(&d, flat_path_slice(&scratch, i), scratch.subpaths[i].closed) + } + } else { + for i in 0 ..< len(scratch.subpaths) { + stroke_subpath(&s, flat_path_slice(&scratch, i), scratch.subpaths[i].closed) + } + } + return .None +} + +// --------------------------------------------------------------------------- +// The stroker +// --------------------------------------------------------------------------- + +// Stroker carries everything the outline builders need. "Build space" is the +// space the pen is circular in: user space for an ordinary stroke, device +// space for a hairline. +@(private) +Stroker :: struct { + out: ^Flat_Path, + radius: f64, + line_cap: Line_Cap, + line_join: Line_Join, + miter: f64, + // Flattening tolerance expressed in build space. + tol: f64, + to_build: Matrix, // user space -> build space + build_id: bool, + post: Matrix, // build space -> device space + post_id: bool, + // One subpath's points in build space, reused between subpaths. + pts: [dynamic]Point, + // Index into out.points at which the contour under construction started. + start: int, + // A non-finite point reached this contour, so the whole contour is + // dropped rather than letting a NaN through to the rasteriser. + bad: bool, + full: bool, +} + +@(private) +stroke_destroy :: proc(s: ^Stroker) { + delete(s.pts) + s.pts = nil +} + +// stroke_scale_extremes returns the singular values of the matrix's linear +// part: the smallest and largest factors by which it stretches a direction. +@(private) +stroke_scale_extremes :: proc "contextless" (m: Matrix) -> (lo, hi: f64) { + sum := m.a * m.a + m.b * m.b + m.c * m.c + m.d * m.d + det := m.a * m.d - m.b * m.c + disc := sum * sum - 4 * det * det + if disc < 0 || !is_finite(disc) { disc = 0 } + root := math.sqrt(disc) + hi2 := (sum + root) * 0.5 + lo2 := (sum - root) * 0.5 + if lo2 < 0 { lo2 = 0 } + if !is_finite(hi2) || hi2 < 0 { return 0, 0 } + return math.sqrt(lo2), math.sqrt(hi2) +} + +// stroke_squashed_pen builds the transform pair for a pen that is under one +// device pixel across its short axis but not its long one. In build space the +// pen is the same circle of radius HAIRLINE_WIDTH/2 a hairline uses, and +// `post` maps that circle onto the CTM's own ellipse with its short semi-axis +// raised to HAIRLINE_WIDTH/2. `post` composed after `to_build` is the CTM to +// the last bit, so the path itself still lands exactly where the CTM puts it; +// only the shape of the pen changes. +// +// ok = false for a CTM whose ellipse cannot be built (an axis that overflows, +// or a product that is not invertible), leaving the caller its ordinary path. +@(private) +stroke_squashed_pen :: proc "contextless" (ctm: Matrix, width, lo, hi: f64) -> (post, to_build: Matrix, ok: bool) { + r := width * 0.5 + unit := HAIRLINE_WIDTH * 0.5 + major := max(r * hi, unit) + minor := max(r * lo, unit) + + // The long axis of the ellipse is the left singular vector of the CTM's + // linear part belonging to `hi`. M*Mt is symmetric with the left singular + // vectors as its eigenvectors and hi^2, lo^2 as its eigenvalues, and for + // [[p q] [q s]] the eigenvector for lambda is (q, lambda-p), or equally + // (lambda-s, q); the longer of the two is the numerically sound one. + // + // M is divided through by `hi` first, which leaves the eigenvectors where + // they were and makes the largest eigenvalue exactly 1, so that a CTM + // scaled by 1e200 cannot overflow the squares. + ma, mb := ctm.a / hi, ctm.b / hi + mc, md := ctm.c / hi, ctm.d / hi + p := ma * ma + mc * mc + s := mb * mb + md * md + q := ma * mb + mc * md + lam := 1.0 + v1 := Point{q, lam - p} + v2 := Point{lam - s, q} + u := stroke_len(v1.x, v1.y) >= stroke_len(v2.x, v2.y) ? v1 : v2 + l := stroke_len(u.x, u.y) + // Both candidates vanish only when M*Mt is a multiple of the identity, in + // which case every direction is a singular vector and the axes may as + // well be the coordinate ones. + if l <= 0 { + u, l = Point{1, 0}, 1 + } + u = Point{u.x / l, u.y / l} + + g1 := major / unit + g2 := minor / unit + post = Matrix{a = u.x * g1, b = u.y * g1, c = -u.y * g2, d = u.x * g2, e = 0, f = 0} + inv := matrix_invert(post) or_return + to_build = matrix_mul(ctm, inv) + if !matrix_is_finite(post) || !matrix_is_finite(to_build) { + return MATRIX_IDENTITY, MATRIX_IDENTITY, false + } + return post, to_build, true +} + +@(private) +stroke_len :: proc "contextless" (dx, dy: f64) -> f64 { + l := math.sqrt(dx * dx + dy * dy) + return is_finite(l) ? l : 0 +} + +// --------------------------------------------------------------------------- +// Contour assembly +// --------------------------------------------------------------------------- + +@(private) +stroke_contour_begin :: proc(s: ^Stroker) { + s.start = len(s.out.points) + s.bad = false +} + +@(private) +stroke_emit :: proc(s: ^Stroker, p: Point) { + if s.full { return } + if len(s.out.points) >= MAX_STROKE_POINTS { + s.full = true + s.bad = true + return + } + if !is_finite(p.x) || !is_finite(p.y) { + s.bad = true + return + } + append(&s.out.points, p) +} + +@(private) +stroke_contour_end :: proc(s: ^Stroker) { + count := len(s.out.points) - s.start + // Fewer than three points bounds no area; emitting it would only cost the + // rasteriser a pass over edges that cancel. + if s.bad || count < 3 { + resize(&s.out.points, s.start) + return + } + if !s.post_id { + for k in s.start ..< len(s.out.points) { + q := transform_point(s.post, s.out.points[k]) + // stroke_emit checked these points in BUILD space, and the trip + // to device space can overflow one that was perfectly finite + // there: an outline point of order 5e159 under a 1e150 CTM is + // +Inf, and a CTM that rotates or skews adds +Inf to -Inf and + // gets NaN. Neither may reach a Flat_Path -- ARCHITECTURE.md + // rule 5, and this is below path.odin's construction-time filter + // -- and the rasteriser's clamping is not a defence, because it + // is not the only thing that reads one. + if !is_finite(q.x) || !is_finite(q.y) { + resize(&s.out.points, s.start) + return + } + s.out.points[k] = q + } + } + append(&s.out.subpaths, Flat_Subpath{start = s.start, count = count, closed = true}) +} + +// --------------------------------------------------------------------------- +// One subpath +// --------------------------------------------------------------------------- + +// stroke_subpath takes a polyline in USER space, moves it into build space, +// removes the zero-length segments that would leave a normal undefined, and +// emits its outline. +@(private) +stroke_subpath :: proc(s: ^Stroker, user_pts: []Point, closed: bool) { + if s.full || len(user_pts) == 0 { return } + clear(&s.pts) + for up in user_pts { + q := s.build_id ? up : transform_point(s.to_build, up) + if !is_finite(q.x) || !is_finite(q.y) { continue } + if len(s.pts) > 0 { + prev := s.pts[len(s.pts) - 1] + if stroke_len(q.x - prev.x, q.y - prev.y) <= 0 { continue } + } + append(&s.pts, q) + } + if closed { + // A flattened closed subpath does not repeat its first point, but a + // path written "m 0 0 l 10 0 l 0 0 h" ends where it started and the + // repeat would be a zero-length closing segment. + for len(s.pts) > 1 { + back := s.pts[len(s.pts) - 1] + if stroke_len(back.x - s.pts[0].x, back.y - s.pts[0].y) > 0 { break } + pop(&s.pts) + } + } + if len(s.pts) == 0 { return } + if len(s.pts) == 1 { + stroke_dot(s, s.pts[0]) + return + } + + pts := s.pts[:] + if closed { + // A closed subpath joins its end to its start instead of capping, and + // it needs two rings: the outer one in the direction of travel and the + // inner one against it, so the nonzero rule leaves the middle empty. + stroke_contour_begin(s) + stroke_side(s, pts, true, false) + stroke_contour_end(s) + stroke_contour_begin(s) + stroke_side(s, pts, true, true) + stroke_contour_end(s) + return + } + // An open subpath is one loop: up one side, round the far cap, back down + // the other side (which is the same walk over the reversed points), round + // the near cap. + stroke_contour_begin(s) + _, end_normal, ok := stroke_side(s, pts, false, false) + if ok { + stroke_cap(s, pts[len(pts) - 1], end_normal) + _, start_normal, _ := stroke_side(s, pts, false, true) + stroke_cap(s, pts[0], start_normal) + } + stroke_contour_end(s) +} + +// stroke_side walks one side of the polyline, emitting the offset segments and +// the join geometry between them. The offset is taken along the left normal of +// the direction of travel, so walking the points backwards traces the other +// side. `first` and `last` are the normals of the first and last segments, +// which is what the caller needs to place the caps. +@(private) +stroke_side :: proc(s: ^Stroker, pts: []Point, closed, reverse: bool) -> (first, last: Point, ok: bool) { + n := len(pts) + if n < 2 { return } + r := s.radius + seg_count := closed ? n : n - 1 + for i in 0 ..< seg_count { + j := (i + 1) % n + a := pts[reverse ? n - 1 - i : i] + b := pts[reverse ? n - 1 - j : j] + dx := b.x - a.x + dy := b.y - a.y + l := stroke_len(dx, dy) + if l <= 0 { continue } + nrm := Point{-dy / l, dx / l} + if ok { + stroke_join(s, a, last, nrm) + } else { + first = nrm + ok = true + } + stroke_emit(s, Point{a.x + r * nrm.x, a.y + r * nrm.y}) + stroke_emit(s, Point{b.x + r * nrm.x, b.y + r * nrm.y}) + last = nrm + } + if closed && ok { + stroke_join(s, pts[reverse ? n - 1 : 0], last, first) + } + return +} + +// stroke_join emits whatever lies between the offset point the previous +// segment ended on (v + r*n0) and the one the next segment starts from +// (v + r*n1). Both of those points are emitted by the caller, so a bevel is +// literally nothing at all. +@(private) +stroke_join :: proc(s: ^Stroker, v, n0, n1: Point) { + cross := n0.x * n1.y - n0.y * n1.x + dot := n0.x * n1.x + n0.y * n1.y + r := s.radius + + // Every vertex of a flattened curve is a join between nearly collinear + // segments. When the two offset points are within the flattening + // tolerance of each other, everything a join could add between them -- + // arc, miter point or the vertex itself -- lies within that tolerance of + // the straight edge already there. Dropping those keeps a stroked curve + // to roughly twice the size of the curve instead of three times. + cx := r * (n1.x - n0.x) + cy := r * (n1.y - n0.y) + if cx * cx + cy * cy <= s.tol * s.tol { return } + + if cross > 0 { + // The inside of the turn: the two offset segments already overlap, so + // there is no gap to fill. Taking the contour through the vertex keeps + // it a single loop and the nonzero rule paints the overlap once. + stroke_emit(s, v) + return + } + if cross == 0 && dot > 0 { return } // collinear: the offsets already meet + + switch s.line_join { + case .Bevel: + // Nothing. The straight edge between the two offset points is the + // bevel. + case .Round: + stroke_arc(s, v, n0, n1) + case .Miter: + denom := 1 + dot + if denom > 1e-12 { + // The miter point is where the two offset lines meet: + // (q-v).n0 == (q-v).n1 == r has the solution q = v + r(n0+n1)/(1+n0.n1). + k := r / denom + q := Point{v.x + k * (n0.x + n1.x), v.y + k * (n0.y + n1.y)} + // Miter length over line width, which is the ratio /MiterLimit + // bounds (8.4.3.5): |q-v|/r == sqrt(2/(1+cos)). + if math.sqrt(2 / denom) <= s.miter { + stroke_emit(s, q) + return + } + } + // Over the miter limit, or a 180-degree reversal whose miter runs off + // to infinity: the format says to substitute a bevel, which is what + // emitting nothing gives. + } +} + +// stroke_cap emits whatever lies between v + r*n and v - r*n at the end of an +// open subpath. `n` is the left normal of the last direction of travel, so the +// direction of travel itself is (n.y, -n.x). +@(private) +stroke_cap :: proc(s: ^Stroker, v, n: Point) { + r := s.radius + switch s.line_cap { + case .Butt: + // Nothing: the contour closes straight across the end. + case .Square: + dx := n.y + dy := -n.x + stroke_emit(s, Point{v.x + r * (n.x + dx), v.y + r * (n.y + dy)}) + stroke_emit(s, Point{v.x + r * (dx - n.x), v.y + r * (dy - n.y)}) + case .Round: + stroke_arc(s, v, n, Point{-n.x, -n.y}) + } +} + +// stroke_arc emits the interior points of the arc that sweeps clockwise from +// n0 to n1 around `centre`. Clockwise is the direction that stays on the outer +// side of a join and outside the end of a cap, which is what keeps every +// contour wound the same way. +@(private) +stroke_arc :: proc(s: ^Stroker, centre, n0, n1: Point) { + r := s.radius + if r <= 0 { return } + a0 := math.atan2(n0.y, n0.x) + a1 := math.atan2(n1.y, n1.x) + sweep := a0 - a1 + if sweep < 0 { sweep += math.TAU } + if !(sweep > 0) { return } + + count := stroke_arc_steps(sweep, r, s.tol, MAX_ARC_SEGMENTS) + for k in 1 ..< count { + ang := a0 - sweep * f64(k) / f64(count) + stroke_emit(s, Point{centre.x + r * math.cos(ang), centre.y + r * math.sin(ang)}) + } +} + +// stroke_arc_steps is how many chords an arc of `sweep` radians on a circle of +// radius r needs to stay within `tol` of the true circle. The sagitta of a +// chord subtending t radians is r*(1-cos(t/2)), which is r*t^2/8 for small t, +// so t = sqrt(8*tol/r) is the largest step that keeps inside the tolerance. +@(private) +stroke_arc_steps :: proc "contextless" (sweep, r, tol: f64, limit: int) -> int { + step := math.sqrt(8 * tol / r) + if !(step > 0) || !is_finite(step) { return limit } + want := sweep / step + if !is_finite(want) || want >= f64(limit) { return limit } + n := int(math.ceil(want)) + return max(n, 1) +} + +// stroke_dot paints a degenerate subpath: a single point, or a closed subpath +// with no length. Strictly 8.4.3.3 paints one only under round caps, but every +// viewer in practice also paints the projecting square, and a dotted line +// written `[0 3] 0 d` with square caps is a real idiom, so both are honoured. +@(private) +stroke_dot :: proc(s: ^Stroker, v: Point) { + r := s.radius + if r <= 0 { return } + switch s.line_cap { + case .Butt: + // A degenerate subpath under butt caps paints nothing at all. + case .Round: + count := stroke_arc_steps(math.TAU, r, s.tol, MAX_CIRCLE_SEGMENTS) + count = max(count, 4) + stroke_contour_begin(s) + // Negative angles: clockwise, matching every other contour. + for k in 0 ..< count { + ang := -math.TAU * f64(k) / f64(count) + stroke_emit(s, Point{v.x + r * math.cos(ang), v.y + r * math.sin(ang)}) + } + stroke_contour_end(s) + case .Square: + // The direction of travel is undefined here, so the square is + // axis-aligned in build space. + stroke_contour_begin(s) + stroke_emit(s, Point{v.x - r, v.y - r}) + stroke_emit(s, Point{v.x - r, v.y + r}) + stroke_emit(s, Point{v.x + r, v.y + r}) + stroke_emit(s, Point{v.x + r, v.y - r}) + stroke_contour_end(s) + } +} + +// --------------------------------------------------------------------------- +// Dashing (ISO 32000-2, 8.4.3.6) +// +// The pattern is measured along the path in user space, starting `phase` units +// into it, and the line cap is applied to both ends of every dash. +// --------------------------------------------------------------------------- + +@(private) +Stroke_Dasher :: struct { + s: ^Stroker, + pattern: [MAX_DASH_ENTRIES]f64, + count: int, + total: f64, + phase: f64, + idx: int, + on: bool, + // Length left in the current pattern entry. + left: f64, + // The dash being accumulated, in user space. + piece: [dynamic]Point, + // The opening dash of a closed subpath, held back until the walk comes + // back round to the start; see stroke_dash_polyline. + head: [dynamic]Point, + hold: bool, + budget: int, +} + +@(private) +stroke_dasher_destroy :: proc(d: ^Stroke_Dasher) { + delete(d.piece) + delete(d.head) + d.piece = nil + d.head = nil +} + +// stroke_dash_reset restarts the pattern at the beginning of a subpath and +// winds it forward by the phase. Each subpath restarts: the phase is measured +// from the start of the subpath, not from the start of the path. +@(private) +stroke_dash_reset :: proc(d: ^Stroke_Dasher) { + clear(&d.piece) + clear(&d.head) + d.hold = false + d.idx = 0 + d.on = true + d.left = d.pattern[0] + + // An odd-length pattern repeats only after twice its sum, because the + // second pass through it swaps on for off. + cycle := d.count % 2 == 0 ? d.total : d.total * 2 + ph := d.phase + if !is_finite(ph) || ph < 0 { ph = 0 } + if cycle > 0 && ph >= cycle { + ph -= math.floor(ph / cycle) * cycle + } + if !is_finite(ph) || ph < 0 { ph = 0 } + + // Two passes over the array is all a phase inside one cycle can need; the + // fuel is there because a pattern of zeros would otherwise spin. + fuel := 2 * d.count + 4 + for ph > 0 && fuel > 0 { + if ph < d.left { + d.left -= ph + break + } + ph -= d.left + d.on = !d.on + d.idx = (d.idx + 1) % d.count + d.left = d.pattern[d.idx] + fuel -= 1 + } +} + +@(private) +stroke_dash_flush :: proc(d: ^Stroke_Dasher) { + if len(d.piece) == 0 { return } + if d.hold { + // The opening dash of a closed subpath is not stroked where it ends; + // stroke_dash_polyline decides what it belongs to once the walk gets + // back to the start. + clear(&d.head) + append(&d.head, ..d.piece[:]) + d.hold = false + clear(&d.piece) + return + } + stroke_subpath(d.s, d.piece[:], false) + clear(&d.piece) + d.budget -= 1 +} + +@(private) +stroke_dash_polyline :: proc(d: ^Stroke_Dasher, pts: []Point, closed: bool) { + n := len(pts) + if n == 0 { return } + stroke_dash_reset(d) + if n == 1 { + stroke_subpath(d.s, pts, false) + return + } + if d.on { append(&d.piece, pts[0]) } + + // A dash that is still on when the walk returns to the start of a closed + // subpath is ONE dash crossing that vertex, not two dashes that end + // there: 8.4.3.6 has dashes "wrap around curves and corners just as solid + // stroked lines do", and only the ends of a dash are capped. So the + // opening piece is held back and whatever comes back round is joined onto + // the front of it, which leaves the seam vertex in the middle of a single + // piece and gives it the join every other vertex it crosses gets. Without + // this a dashed rectangle loses one of its four corners. + d.hold = closed && d.on + + seg_count := closed ? n : n - 1 + for i in 0 ..< seg_count { + a := pts[i] + b := pts[(i + 1) % n] + dx := b.x - a.x + dy := b.y - a.y + seg := stroke_len(dx, dy) + if seg <= 0 { continue } + if d.budget <= 0 { + d.on = true + d.left = DASH_UNBOUNDED + } + pos := 0.0 + for d.left < seg - pos { + pos += d.left + u := pos / seg + p := Point{a.x + dx * u, a.y + dy * u} + if d.on { + append(&d.piece, p) + stroke_dash_flush(d) + } else { + clear(&d.piece) + append(&d.piece, p) + } + d.on = !d.on + d.idx = (d.idx + 1) % d.count + d.left = d.pattern[d.idx] + if d.budget <= 0 { + // Out of budget. Draw the rest of the path solid rather than + // stopping: a line that loses its dashes reads as a missing + // pattern, a line that stops halfway reads as a broken + // renderer. + d.on = true + d.left = DASH_UNBOUNDED + if len(d.piece) == 0 { append(&d.piece, p) } + break + } + } + d.left -= seg - pos + if d.on { append(&d.piece, b) } + } + if d.hold { + // No gap ever opened, so the whole closed subpath is one dash and is + // stroked exactly as the undashed one would be: two rings, a join at + // every vertex, and no cap anywhere. + d.hold = false + if len(d.piece) > 0 { + stroke_subpath(d.s, d.piece[:], true) + clear(&d.piece) + d.budget -= 1 + } + return + } + if len(d.head) > 0 { + // The held piece begins at the seam vertex, which the piece that came + // back round ends at, so appending it whole leaves one repeated point + // that stroke_subpath drops. + if !d.on || len(d.piece) == 0 { + // The pattern went off before the walk got back: the two pieces + // really are separate dashes, and each keeps its own caps. + stroke_dash_flush(d) + } + append(&d.piece, ..d.head[:]) + clear(&d.head) + } + stroke_dash_flush(d) +} diff --git a/stroke_test.odin b/stroke_test.odin new file mode 100644 index 0000000..0530b94 --- /dev/null +++ b/stroke_test.odin @@ -0,0 +1,1373 @@ +package pdf + +import "core:math" +import "core:testing" + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +@(private = "file") +run_stroke :: proc(p: ^Path, st: Stroke_State, ctm := MATRIX_IDENTITY, + flatness := FLATNESS) -> (Flat_Path, Error) { + fp := flat_path_make() + state := st + err := stroke_flatten(&fp, p, &state, ctm, flatness) + return fp, err +} + +@(private = "file") +seg_path :: proc(x0, y0, x1, y1: f64) -> Path { + p := path_make() + path_move_to(&p, x0, y0) + path_line_to(&p, x1, y1) + return p +} + +@(private = "file") +sub_bounds :: proc(fp: ^Flat_Path, i: int) -> Rect { + pts := flat_path_slice(fp, i) + if len(pts) == 0 { return RECT_EMPTY } + r := Rect{pts[0].x, pts[0].y, pts[0].x, pts[0].y} + for pt in pts[1:] { + r.x0 = min(r.x0, pt.x); r.y0 = min(r.y0, pt.y) + r.x1 = max(r.x1, pt.x); r.y1 = max(r.y1, pt.y) + } + return r +} + +// winding_at is the nonzero winding number of the whole outline at a point, +// counted the way the rasteriser will count it. Every contour the stroker +// emits is closed, so the edge from the last point back to the first counts. +@(private = "file") +winding_at :: proc(fp: ^Flat_Path, px, py: f64) -> int { + w := 0 + for i in 0 ..< flat_path_count(fp) { + pts := flat_path_slice(fp, i) + if len(pts) < 2 { continue } + for k in 0 ..< len(pts) { + a := pts[k] + b := pts[(k + 1) % len(pts)] + if a.y <= py && b.y > py { + if a.x + (py - a.y) * (b.x - a.x) / (b.y - a.y) > px { w += 1 } + } else if b.y <= py && a.y > py { + if a.x + (py - a.y) * (b.x - a.x) / (b.y - a.y) > px { w -= 1 } + } + } + } + return w +} + +@(private = "file") +covered :: proc(fp: ^Flat_Path, px, py: f64) -> bool { + return winding_at(fp, px, py) != 0 +} + +@(private = "file") +has_point :: proc(fp: ^Flat_Path, px, py, eps: f64) -> bool { + for pt in fp.points { + if abs(pt.x - px) <= eps && abs(pt.y - py) <= eps { return true } + } + return false +} + +@(private = "file") +near :: proc(a, b, eps: f64) -> bool { + return abs(a - b) <= eps +} + +// --------------------------------------------------------------------------- +// Caps +// --------------------------------------------------------------------------- + +// A 100-long horizontal segment of width 10 has an outline whose box is +// determined exactly by the cap style: butt stops at the ends, projecting +// square overhangs by half the width, round reaches the same overhang but as +// a polygon inscribed in the semicircle, so it falls short by at most the +// flatness tolerance. +@(test) +test_stroke_cap_bounds :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 10 + + { + st.line_cap = .Butt + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 1) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -5, 100, 5}) + } + { + st.line_cap = .Square + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_bounds(&fp), Rect{-5, -5, 105, 5}) + } + { + st.line_cap = .Round + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + b := flat_path_bounds(&fp) + // The round cap never overshoots and never falls short by more than + // the flatness tolerance. + testing.expect(t, b.x0 <= -5 + FLATNESS && b.x0 >= -5, "round cap start overhang") + testing.expect(t, b.x1 >= 105 - FLATNESS && b.x1 <= 105, "round cap end overhang") + testing.expect(t, near(b.y0, -5, 1e-12) && near(b.y1, 5, 1e-12), "round cap sides") + // Every cap point is on the circle of radius 5 about an end point. + for pt in fp.points { + d0 := stroke_len(pt.x - 0, pt.y) + d1 := stroke_len(pt.x - 100, pt.y) + on_body := pt.x >= -1e-12 && pt.x <= 100 + 1e-12 && near(abs(pt.y), 5, 1e-12) + testing.expect(t, on_body || near(d0, 5, 1e-9) || near(d1, 5, 1e-9), + "round cap points lie on the end circles") + } + } +} + +// --------------------------------------------------------------------------- +// Joins +// --------------------------------------------------------------------------- + +// A right angle: the miter ratio is sqrt(2), so a limit of 10 keeps the spike +// and a limit of 1.2 replaces it with a bevel. The three styles are told apart +// by two probe points in the outer corner rather than by the bounding box, +// which is the same for all of them. +@(test) +test_stroke_joins :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 10, 0) + path_line_to(&p, 10, 10) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 // radius 1: the outer corner is the unit square + // [10,11] x [-1,0] + st.line_cap = .Butt + + // Inside the bevel triangle, inside the round arc, inside the miter. + IN_ALL_X :: 10.3 + IN_ALL_Y :: -0.2 + // Outside the bevel, inside the round arc and the miter. + ROUND_X :: 10.6 + ROUND_Y :: -0.6 + // Outside the bevel and the round arc, inside the miter only. + MITER_X :: 10.9 + MITER_Y :: -0.9 + + { + st.line_join = .Miter + st.miter_limit = 10 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect(t, has_point(&fp, 11, -1, 1e-9), "miter point should be emitted") + testing.expect(t, covered(&fp, IN_ALL_X, IN_ALL_Y), "miter covers the corner") + testing.expect(t, covered(&fp, ROUND_X, ROUND_Y), "miter covers past the bevel") + testing.expect(t, covered(&fp, MITER_X, MITER_Y), "miter covers past the arc") + } + { + // sqrt(2) > 1.2, so the limit is exceeded and the join bevels. + st.line_join = .Miter + st.miter_limit = 1.2 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect(t, !has_point(&fp, 11, -1, 1e-9), "miter limit should suppress the spike") + testing.expect(t, covered(&fp, IN_ALL_X, IN_ALL_Y), "bevel still covers the corner") + testing.expect(t, !covered(&fp, ROUND_X, ROUND_Y), "bevel stops short of the arc") + } + { + st.line_join = .Bevel + st.miter_limit = 10 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect(t, !has_point(&fp, 11, -1, 1e-9), "bevel emits no miter point") + testing.expect(t, covered(&fp, IN_ALL_X, IN_ALL_Y), "bevel covers the corner") + testing.expect(t, !covered(&fp, ROUND_X, ROUND_Y), "bevel stops short of the arc") + } + { + st.line_join = .Round + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect(t, covered(&fp, ROUND_X, ROUND_Y), "round join covers past the bevel") + testing.expect(t, !covered(&fp, MITER_X, MITER_Y), "round join stops short of the miter") + // Everything the join adds sits on the circle of radius 1 about the + // corner. + for pt in fp.points { + if pt.x > 10 + 1e-9 && pt.y < -1e-9 { + testing.expect(t, near(stroke_len(pt.x - 10, pt.y), 1, 1e-9), + "round join points lie on the corner circle") + } + } + } +} + +// The miter limit is compared against the miter length over the line width, so +// it must fire on the angle and not on the width: the same corner at a +// different width behaves identically. +@(test) +test_stroke_miter_limit_is_scale_free :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 100, 0) + path_line_to(&p, 100, 100) + + st := STROKE_STATE_DEFAULT + st.line_cap = .Butt + st.line_join = .Miter + st.line_width = 20 // radius 10, miter point at (110, -10) + st.miter_limit = 1.5 // sqrt(2) = 1.414 < 1.5, so the spike survives + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect(t, has_point(&fp, 110, -10, 1e-9), "miter survives at 1.5") + + st.miter_limit = 1.4 // sqrt(2) = 1.414 > 1.4, so it does not + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect(t, !has_point(&fp2, 110, -10, 1e-9), "miter is cut at 1.4") +} + +// A 30-degree corner has a miter ratio of 1/sin(15 deg) = 3.86, so the PDF +// default limit of 10 keeps it and a limit of 3 does not. This is the case +// the limit exists for: without it a nearly-doubled-back path grows a spike +// hundreds of times the line width. +@(test) +test_stroke_miter_sharp_corner :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + ang := 30.0 * math.PI / 180 + path_move_to(&p, 100, 0) + path_line_to(&p, 0, 0) + path_line_to(&p, 100 * math.cos(ang), 100 * math.sin(ang)) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_join = .Miter + st.line_cap = .Butt + + ratio := 1 / math.sin(ang / 2) // 3.8637... + + st.miter_limit = 10 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + b := flat_path_bounds(&fp) + // The spike runs backwards out of the corner along the bisector, to a + // distance of ratio * radius: x = -cos(15 deg) / sin(15 deg). + testing.expectf(t, near(b.x0, -math.cos(ang / 2) * ratio, 1e-9), + "sharp miter should reach x = %v, got %v", -math.cos(ang / 2) * ratio, b.x0) + testing.expect(t, has_point(&fp, -math.cos(ang / 2) * ratio, -1, 1e-9), + "the miter point itself is emitted") + + st.miter_limit = 3 + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + b2 := flat_path_bounds(&fp2) + // Bevelled, so the outline stops at the offset point of the second + // segment: x = -sin(30 deg) * radius. + testing.expect(t, near(b2.x0, -math.sin(ang), 1e-9), + "miter limit 3 cuts the spike back to a bevel") +} + +// --------------------------------------------------------------------------- +// Hairlines +// --------------------------------------------------------------------------- + +// "0 w" means the thinnest line the device can draw (8.4.3.2), and so does any +// width that would come out under a pixel. Both must be exactly one device +// pixel across whatever the CTM does; a width that is genuinely visible must +// not be promoted. +@(test) +test_stroke_hairline :: proc(t: ^testing.T) { + p := seg_path(0, 0, 1, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_cap = .Butt + + Case :: struct { width: f64, scale: f64, want_h: f64, want_w: f64 } + cases := []Case{ + {0, 1, 1, 1}, // 0 w at 1x + {0, 100, 1, 100}, // 0 w at 100x + {0.001, 1, 1, 1}, // sub-pixel at 1x + {0.001, 100, 1, 100}, // still sub-pixel at 100x (0.1 device units) + {0.02, 100, 2, 100}, // 2 device units: a real stroke, not promoted + {2, 1, 2, 1}, // 2 units at 1x: not promoted + } + for c in cases { + st.line_width = c.width + fp, err := run_stroke(&p, st, scale_matrix(c.scale, c.scale)) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + b := flat_path_bounds(&fp) + testing.expectf(t, near(rect_height(b), c.want_h, 1e-9), + "width %v at %vx should be %v device units thick, got %v", + c.width, c.scale, c.want_h, rect_height(b)) + testing.expectf(t, near(rect_width(b), c.want_w, 1e-9), + "width %v at %vx should be %v device units long, got %v", + c.width, c.scale, c.want_w, rect_width(b)) + } +} + +// A hairline is one pixel across in *every* direction, so it is built in +// device space: under an anisotropic CTM it must not come out elliptical. +@(test) +test_stroke_hairline_is_isotropic :: proc(t: ^testing.T) { + st := STROKE_STATE_DEFAULT + st.line_width = 0 + st.line_cap = .Butt + ctm := scale_matrix(1, 40) + + h := seg_path(0, 0, 10, 0) + defer path_destroy(&h) + fp, _ := run_stroke(&h, st, ctm) + defer flat_path_destroy(&fp) + testing.expect(t, near(rect_height(flat_path_bounds(&fp)), 1, 1e-9), + "hairline across a horizontal line is one pixel") + + v := seg_path(0, 0, 0, 10) + defer path_destroy(&v) + fp2, _ := run_stroke(&v, st, ctm) + defer flat_path_destroy(&fp2) + testing.expect(t, near(rect_width(flat_path_bounds(&fp2)), 1, 1e-9), + "hairline across a vertical line is one pixel") +} + +// --------------------------------------------------------------------------- +// Anisotropy +// --------------------------------------------------------------------------- + +// Line width is a user-space quantity, so under `1 0 0 4 0 0 cm` a horizontal +// stroke is four times as thick as a vertical one. Scaling the width by +// matrix_expansion -- sqrt(|det|) = 2 here -- would make both 20 units and be +// wrong for both. +@(test) +test_stroke_anisotropic_ctm :: proc(t: ^testing.T) { + st := STROKE_STATE_DEFAULT + st.line_width = 10 + st.line_cap = .Butt + ctm := scale_matrix(1, 4) + + h := seg_path(0, 0, 100, 0) + defer path_destroy(&h) + fp, _ := run_stroke(&h, st, ctm) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -20, 100, 20}) + + v := seg_path(0, 0, 0, 100) + defer path_destroy(&v) + fp2, _ := run_stroke(&v, st, ctm) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_bounds(&fp2), Rect{-5, 0, 5, 400}) +} + +// The device thickness of a stroke depends on the direction it runs in, so a +// pen that is a whole pixel across one axis and a fiftieth across the other +// has to be floored on the narrow axis alone. Testing only the widest +// thickness leaves the result non-monotonic in the line width: the same rule +// comes out a clean hairline at `0 w` and invisible -- 0.02 of a pixel, a 1% +// grey -- at `1 w`, so widening the pen makes the line disappear. +@(test) +test_stroke_anisotropic_hairline_floor :: proc(t: ^testing.T) { + h := seg_path(0, 400, 40, 400) + defer path_destroy(&h) + st := STROKE_STATE_DEFAULT + st.line_cap = .Butt + ctm := Matrix{1, 0, 0, 0.02, 0, 0} // `1 0 0 0.02 0 0 cm` + + // Every width whose thickness across this line is under a pixel draws the + // same hairline, in the same place: 40 device units long, one device + // pixel tall, centred on y = 8. + for w in ([5]f64{0, 0.999, 1, 1.5, 3}) { + st.line_width = w + fp, err := run_stroke(&h, st, ctm) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + b := flat_path_bounds(&fp) + testing.expectf(t, near(rect_height(b), 1, 1e-9) && near(rect_width(b), 40, 1e-9) && + near(b.y0, 7.5, 1e-9) && near(b.x0, 0, 1e-9), + "width %v under a 1:50 squash should draw a 40x1 hairline at y 7.5, got %v", w, b) + } + // Past a pixel the true thickness takes over: 60 * 0.02 = 1.2. + st.line_width = 60 + wide, _ := run_stroke(&h, st, ctm) + defer flat_path_destroy(&wide) + testing.expect(t, near(rect_height(flat_path_bounds(&wide)), 1.2, 1e-9), + "a stroke over a pixel thick keeps its own thickness") + + // The floor is on the short axis only. Under a CTM that squashes x by 50 + // and stretches y by 10, a horizontal line is stretched ACROSS and must + // keep all ten pixels of it; the vertical one, a fiftieth of a pixel + // across, is the one that gets promoted. An averaged scale factor -- + // sqrt(|det|) is 0.45 here -- would thin the horizontal line to 1. + ctm2 := Matrix{0.02, 0, 0, 10, 0, 0} + flat := seg_path(0, 2, 400, 2) + defer path_destroy(&flat) + tall := seg_path(400, 0, 400, 4) + defer path_destroy(&tall) + Case :: struct { width, want_flat_h, want_tall_w: f64 } + for c in ([3]Case{{0, 1, 1}, {1, 10, 1}, {3, 30, 1}}) { + st.line_width = c.width + fh, _ := run_stroke(&flat, st, ctm2) + defer flat_path_destroy(&fh) + ft, _ := run_stroke(&tall, st, ctm2) + defer flat_path_destroy(&ft) + testing.expectf(t, near(rect_height(flat_path_bounds(&fh)), c.want_flat_h, 1e-9), + "width %v across the stretched axis: want %v, got %v", + c.width, c.want_flat_h, rect_height(flat_path_bounds(&fh))) + testing.expectf(t, near(rect_width(flat_path_bounds(&ft)), c.want_tall_w, 1e-9), + "width %v across the squashed axis: want %v, got %v", + c.width, c.want_tall_w, rect_width(flat_path_bounds(&ft))) + } +} + +// A rotation must not change the thickness, and a rotated stroke's corners +// must land where the rotation puts them. +@(test) +test_stroke_rotated_ctm :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + st := STROKE_STATE_DEFAULT + st.line_width = 10 + st.line_cap = .Butt + + fp, _ := run_stroke(&p, st, rotate_matrix(90)) + defer flat_path_destroy(&fp) + b := flat_path_bounds(&fp) + testing.expect(t, near(rect_width(b), 10, 1e-9), "rotated stroke keeps its width") + testing.expect(t, near(rect_height(b), 100, 1e-9), "rotated stroke keeps its length") +} + +// --------------------------------------------------------------------------- +// Dashes +// --------------------------------------------------------------------------- + +// [10 10] on a 100-long line draws five 10-long dashes: 50 units of ink in +// five separate pieces, each a plain rectangle under butt caps. +@(test) +test_stroke_dash_pieces :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + st.dash = {} + st.dash[0] = 10 + st.dash[1] = 10 + st.dash_count = 2 + + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 5) + + drawn := 0.0 + for i in 0 ..< flat_path_count(&fp) { + b := sub_bounds(&fp, i) + testing.expect_value(t, len(flat_path_slice(&fp, i)), 4) + testing.expectf(t, near(b.x0, f64(i) * 20, 1e-9), "dash %d starts at %v", i, b.x0) + testing.expectf(t, near(b.x1, f64(i) * 20 + 10, 1e-9), "dash %d ends at %v", i, b.x1) + testing.expect(t, near(b.y0, -1, 1e-9) && near(b.y1, 1, 1e-9), "dash keeps its width") + drawn += rect_width(b) + } + testing.expect(t, near(drawn, 50, 1e-9), "half the line is inked") +} + +// The phase starts the pattern part-way in: five units of "on" are already +// spent, so the first dash is short and a sixth appears at the far end. +@(test) +test_stroke_dash_phase :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + st.dash[0] = 10 + st.dash[1] = 10 + st.dash_count = 2 + st.dash_phase = 5 + + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 6) + + want_x0 := []f64{0, 15, 35, 55, 75, 95} + want_x1 := []f64{5, 25, 45, 65, 85, 100} + drawn := 0.0 + for i in 0 ..< flat_path_count(&fp) { + b := sub_bounds(&fp, i) + testing.expectf(t, near(b.x0, want_x0[i], 1e-9), "dash %d starts at %v", i, b.x0) + testing.expectf(t, near(b.x1, want_x1[i], 1e-9), "dash %d ends at %v", i, b.x1) + drawn += rect_width(b) + } + testing.expect(t, near(drawn, 50, 1e-9), "half the line is still inked") + + // A phase of a whole cycle is the same as no phase at all. + st.dash_phase = 20 + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_count(&fp2), 5) + testing.expect(t, near(sub_bounds(&fp2, 0).x0, 0, 1e-9), "a full-cycle phase is a no-op") +} + +// An odd-length pattern only repeats after twice its sum, because the second +// pass through it swaps on for off: [10] draws 0..10, 20..30, 40..50, and a +// phase of 10 shifts every dash by 10 rather than leaving it alone. +@(test) +test_stroke_dash_odd_pattern :: proc(t: ^testing.T) { + p := seg_path(0, 0, 60, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + st.dash[0] = 10 + st.dash_count = 1 + + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 3) + testing.expect(t, near(sub_bounds(&fp, 0).x0, 0, 1e-9), "first dash at 0") + testing.expect(t, near(sub_bounds(&fp, 1).x0, 20, 1e-9), "second dash at 20") + testing.expect(t, near(sub_bounds(&fp, 2).x0, 40, 1e-9), "third dash at 40") + + st.dash_phase = 10 + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_count(&fp2), 3) + testing.expect(t, near(sub_bounds(&fp2, 0).x0, 10, 1e-9), + "an odd pattern's phase of one entry inverts it rather than repeating") +} + +// Every dash end gets a cap, so a round-capped dash pattern overhangs its own +// on-length at both ends. +@(test) +test_stroke_dash_caps_each_piece :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 4 // radius 2 + st.line_cap = .Square + st.dash[0] = 10 + st.dash[1] = 10 + st.dash_count = 2 + + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 5) + b := sub_bounds(&fp, 1) + testing.expect(t, near(b.x0, 18, 1e-9) && near(b.x1, 32, 1e-9), + "projecting square caps overhang both ends of every dash") +} + +// A zero-length "on" entry is a dot, which is how `[0 5] 0 d` with round caps +// writes a dotted line. Under butt caps the same pattern paints nothing. +@(test) +test_stroke_dash_zero_length_dots :: proc(t: ^testing.T) { + p := seg_path(0, 0, 22, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 4 // radius 2 + st.dash[0] = 0 + st.dash[1] = 5 + st.dash_count = 2 + + st.line_cap = .Round + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 5) + for i in 0 ..< flat_path_count(&fp) { + // The polygon that approximates a circle is not symmetric about its + // centre, so check the vertices against the centre rather than the + // midpoint of the box: every one is exactly a pen radius away from + // where the dash pattern put the dot. + want := f64(i) * 5 + pts := flat_path_slice(&fp, i) + testing.expect(t, len(pts) >= 4, "a round dot is a polygon") + for pt in pts { + testing.expectf(t, near(stroke_len(pt.x - want, pt.y), 2, 1e-9), + "dot %d: point %v is not on the circle of radius 2 at x = %v", i, pt, want) + } + testing.expectf(t, covered(&fp, want, 0), "dot %d is filled", i) + } + + st.line_cap = .Butt + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_count(&fp2), 0) +} + +// An all-zero or negative dash array is solid: a dasher that never advances +// would loop forever (8.4.3.6). +@(test) +test_stroke_dash_degenerate_is_solid :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + + st.dash[0] = 0 + st.dash[1] = 0 + st.dash_count = 2 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 1) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -1, 100, 1}) + + st.dash[0] = 5 + st.dash[1] = -5 + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_count(&fp2), 1) + testing.expect_value(t, flat_path_bounds(&fp2), Rect{0, -1, 100, 1}) +} + +// A pattern whose whole cycle is smaller than a device pixel is drawn solid. +// Dashing it would emit one piece per sub-pixel step for no visible +// difference -- 5 million rectangles for this line. +@(test) +test_stroke_dash_subpixel_is_solid :: proc(t: ^testing.T) { + p := seg_path(0, 0, 1000, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + st.dash[0] = 0.0001 + st.dash[1] = 0.0001 + st.dash_count = 2 + + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 1) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -1, 1000, 1}) +} + +// A pattern that is visible in one direction but repeats far more often than +// there are pixels along the path is cut off by the budget, and the rest of +// the path is drawn solid rather than left out. The CTM here squashes x by +// 100 and stretches y by 100, so the cycle is 4 device units across a vertical +// stroke -- not sub-pixel -- while the line is only 100 device units long. +@(test) +test_stroke_dash_output_is_bounded :: proc(t: ^testing.T) { + p := seg_path(0, 0, 10000, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 1 + st.line_cap = .Butt + st.dash[0] = 0.02 + st.dash[1] = 0.02 + st.dash_count = 2 + + fp, err := run_stroke(&p, st, scale_matrix(0.01, 100)) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + // 250000 dashes would fit; the budget is the path's device length plus a + // small margin. + testing.expect(t, flat_path_count(&fp) > 1, "dashing happened at all") + testing.expectf(t, flat_path_count(&fp) <= 140, + "dash count is capped by device length, got %d", flat_path_count(&fp)) + // The remainder is drawn, solid, all the way to the end of the path. + testing.expect(t, near(flat_path_bounds(&fp).x1, 100, 1e-6), + "the path is drawn to its end after the budget runs out") +} + +// A dash is measured in user space, so the same pattern under a scaled CTM +// still produces the same number of pieces. +@(test) +test_stroke_dash_is_user_space :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + st.dash[0] = 10 + st.dash[1] = 10 + st.dash_count = 2 + + fp, _ := run_stroke(&p, st, scale_matrix(3, 3)) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 5) + testing.expect(t, near(sub_bounds(&fp, 1).x0, 60, 1e-9), + "dash lengths scale with the CTM, the pattern does not") +} + +// A dash that is still on when the pattern walks back round to the start of a +// closed subpath is one dash crossing that vertex, so the vertex gets a join +// like every other one a dash crosses (8.4.3.6). Emitting the two halves as +// separate capped pieces instead chips the corner of every dashed rectangle +// -- table cells, form boxes, cut-here frames -- at exactly one of its four +// corners. +@(test) +test_stroke_dash_closed_seam_is_joined :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + // Perimeter 80, seam at the corner (8, 8). The pattern's cycle is 12 and + // 80 = 6*12 + 8, so the walk comes back round with the dash still on. + path_rect(&p, 8, 8, 20, 20) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 // radius 1 + st.line_join = .Miter + st.miter_limit = 10 + st.line_cap = .Butt + st.dash[0] = 8 + st.dash[1] = 4 + st.dash_count = 2 + + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + // Seven dashes' worth of ink, but the first and the last are one dash + // wrapped round the seam, so six pieces come out. + testing.expect_value(t, flat_path_count(&fp), 6) + + // The corners sit at 0, 20, 40 and 60 along the perimeter. A dash crosses + // the one at 40 and the seam at 0, so both get a miter; the dash ending at + // 20 and the one starting at 60 land exactly on their corners, so those + // two are butt-capped and their outer wedge stays clear. The test point is + // inside the miter and outside the bevel that would replace it. + testing.expect(t, covered(&fp, 7.4, 7.4), "the seam corner is mitered") + testing.expect(t, covered(&fp, 28.6, 28.6), "a corner a dash crosses is mitered") + testing.expect(t, !covered(&fp, 28.6, 7.4), "a corner a dash ends on is capped") + testing.expect(t, !covered(&fp, 7.4, 28.6), "a corner a dash starts on is capped") + testing.expect_value(t, flat_path_bounds(&fp), Rect{7, 7, 29, 29}) +} + +// The same seam with the pattern never going off at all: the whole closed +// subpath is a single dash, which is the solid closed stroke exactly -- two +// rings with a hole between them, not one capped strip. +@(test) +test_stroke_dash_closed_all_on_is_a_ring :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, 8, 8, 20, 20) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_join = .Miter + st.miter_limit = 10 + st.line_cap = .Square // would show at once as a bulge at the seam + solid, _ := run_stroke(&p, st) + defer flat_path_destroy(&solid) + + st.dash[0] = 200 // longer than the 80-unit perimeter + st.dash[1] = 5 + st.dash_count = 2 + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), flat_path_count(&solid)) + testing.expect_value(t, flat_path_bounds(&fp), flat_path_bounds(&solid)) + testing.expect(t, covered(&fp, 7.4, 7.4), "the seam corner is mitered") + testing.expect_value(t, winding_at(&fp, 18, 18), 0) + testing.expect(t, len(fp.points) == len(solid.points), "the same outline as a solid stroke") +} + +// --------------------------------------------------------------------------- +// Degenerate subpaths +// --------------------------------------------------------------------------- + +// A single-point subpath paints a dot under round and projecting-square caps +// and nothing under butt caps. So does a closed subpath with no length. +@(test) +test_stroke_single_point_subpath :: proc(t: ^testing.T) { + st := STROKE_STATE_DEFAULT + st.line_width = 10 // radius 5 + + build :: proc(closed: bool) -> Path { + p := path_make() + path_move_to(&p, 5, 5) + if closed { path_close(&p) } + return p + } + + for closed in ([2]bool{false, true}) { + p := build(closed) + defer path_destroy(&p) + + st.line_cap = .Butt + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 0) + + st.line_cap = .Square + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_count(&fp2), 1) + testing.expect_value(t, flat_path_bounds(&fp2), Rect{0, 0, 10, 10}) + testing.expect(t, covered(&fp2, 5, 5), "the square dot is filled") + + st.line_cap = .Round + fp3, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp3) + testing.expect_value(t, flat_path_count(&fp3), 1) + testing.expect(t, covered(&fp3, 5, 5), "the round dot is filled") + testing.expect(t, !covered(&fp3, 5 + 5.5, 5), "the round dot has radius 5") + for pt in fp3.points { + testing.expect(t, near(stroke_len(pt.x - 5, pt.y - 5), 5, 1e-9), + "every point of a round dot is on the circle") + } + } +} + +// A subpath whose points all coincide is the same degenerate case, whether it +// arrived as one point or as several. +@(test) +test_stroke_repeated_point_subpath :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 5, 5) + path_line_to(&p, 5, 5) + path_line_to(&p, 5, 5) + + st := STROKE_STATE_DEFAULT + st.line_width = 10 + st.line_cap = .Square + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_count(&fp), 1) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, 0, 10, 10}) +} + +// A closed subpath that doubles back on itself has joins at both ends rather +// than caps, and a 180-degree miter falls back to a bevel -- which is the same +// shape a butt cap would have given, so the outline is exactly the rectangle. +@(test) +test_stroke_closed_degenerate_line :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 10, 0) + path_close(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Square // ignored: a closed subpath is never capped + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -1, 10, 1}) + testing.expect(t, covered(&fp, 5, 0), "the doubled-back stroke is filled") +} + +// --------------------------------------------------------------------------- +// Closed subpaths +// --------------------------------------------------------------------------- + +// A closed subpath joins its end to its start instead of capping, and needs +// two rings so the nonzero rule leaves the middle of the shape unpainted. +@(test) +test_stroke_closed_triangle :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_line_to(&p, 30, 0) + path_line_to(&p, 15, 20) + path_close(&p) + + st := STROKE_STATE_DEFAULT + st.line_width = 2 // radius 1 + st.line_join = .Miter + st.miter_limit = 10 + st.line_cap = .Butt + + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + // One outer ring and one inner ring. A capped (i.e. not closed) stroke + // would be a single contour. + testing.expect_value(t, flat_path_count(&fp), 2) + + // The middle of the triangle is a hole: the two rings cancel exactly. + testing.expect_value(t, winding_at(&fp, 15, 20.0 / 3), 0) + // The edges are inked, on both sides of the path. + testing.expect(t, covered(&fp, 15, 0), "on the bottom edge") + testing.expect(t, covered(&fp, 15, -0.5), "outside the bottom edge") + testing.expect(t, covered(&fp, 15, 0.5), "inside the bottom edge") + // Just past half the line width is clear again. + testing.expect(t, !covered(&fp, 15, -1.5), "beyond the outer edge") + testing.expect(t, !covered(&fp, 15, 1.5), "beyond the inner edge") + + // Joined, not capped: the corner at the origin grows a miter spike + // outwards. A butt cap would have stopped at x = 0. + b := flat_path_bounds(&fp) + testing.expect(t, b.x0 < -1.0, "the corner is mitered, not capped") + testing.expect(t, b.y0 < -0.999, "the outer ring clears the bottom edge") +} + +// The same closed shape under round joins has no spikes at all: every point of +// the outline is within half a line width plus the tolerance of the path. +@(test) +test_stroke_closed_round_join_has_no_spikes :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, 0, 0, 20, 20) + + st := STROKE_STATE_DEFAULT + st.line_width = 4 // radius 2 + st.line_join = .Round + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + + testing.expect_value(t, flat_path_count(&fp), 2) + b := flat_path_bounds(&fp) + testing.expect(t, b.x0 >= -2 - 1e-9 && b.y0 >= -2 - 1e-9, "round joins do not overshoot") + testing.expect(t, b.x1 <= 22 + 1e-9 && b.y1 <= 22 + 1e-9, "round joins do not overshoot") + testing.expect(t, near(b.x0, -2, FLATNESS) && near(b.x1, 22, FLATNESS), + "round joins reach the full offset") + testing.expect_value(t, winding_at(&fp, 10, 10), 0) + testing.expect(t, covered(&fp, 0, 10), "the left edge is inked") +} + +// A closed rectangle is the commonest closed stroke there is; check the ink +// lands where it should on both sides of every edge. +@(test) +test_stroke_closed_rect_coverage :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_rect(&p, 10, 10, 80, 40) + + st := STROKE_STATE_DEFAULT + st.line_width = 4 // radius 2 + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + + testing.expect(t, covered(&fp, 50, 10), "on the bottom edge") + testing.expect(t, covered(&fp, 50, 8.5), "outside the bottom edge") + testing.expect(t, covered(&fp, 50, 11.5), "inside the bottom edge") + testing.expect(t, !covered(&fp, 50, 7.5), "clear below") + testing.expect(t, !covered(&fp, 50, 13), "clear above") + testing.expect(t, !covered(&fp, 50, 30), "the middle is a hole") + testing.expect(t, covered(&fp, 10, 50), "the top-left corner is inked") +} + +// --------------------------------------------------------------------------- +// Curves +// --------------------------------------------------------------------------- + +// A stroked curve is an annulus about the flattened curve: every point of the +// outline is half a line width from the curve, give or take the tolerance. +@(test) +test_stroke_curve_band :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_curve_to(&p, 0, 100, 100, 100, 100, 0) + + st := STROKE_STATE_DEFAULT + st.line_width = 10 + st.line_join = .Round + st.line_cap = .Butt + + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 1) + + // The curve peaks at y = 75 (t = 0.5), so the outline must reach 80. + b := flat_path_bounds(&fp) + testing.expect(t, near(b.y1, 80, 0.5), "the outline clears the curve by half the width") + testing.expect(t, b.x0 >= -5 - 1e-9 && b.x1 <= 105 + 1e-9, "the outline does not overshoot") + + // The centre of the curve is inked and a point well outside the band is + // not. + testing.expect(t, covered(&fp, 50, 75), "the crest of the curve is inked") + testing.expect(t, !covered(&fp, 50, 40), "the middle of the arc is empty") + testing.expect(t, !covered(&fp, 50, 90), "above the curve is empty") + + for pt in fp.points { + testing.expect(t, is_finite(pt.x) && is_finite(pt.y), "no non-finite points") + } +} + +// A thin pen turns the near-collinear joins that a flattened curve is full of +// into sub-tolerance detail, and they must then cost nothing: two offset +// points per segment per side is the floor, and a join point at every vertex +// would push the outline half as large again. +@(test) +test_stroke_curve_stays_compact :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + path_move_to(&p, 0, 0) + path_curve_to(&p, 0, 100, 100, 100, 100, 0) + + flat := flat_path_make() + defer flat_path_destroy(&flat) + path_flatten(&flat, &p, MATRIX_IDENTITY) + segments := len(flat.points) - 1 + + st := STROKE_STATE_DEFAULT + st.line_width = 1 // radius 0.5: every join is under the tolerance + st.line_join = .Miter + fp, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp) + + testing.expectf(t, len(fp.points) <= 4 * segments + 4, + "stroked curve used %d points for %d segments", len(fp.points), segments) + + // A wide pen is a different matter: at radius 5 the same turns move the + // offset by half a unit, so the joins are real geometry and must be kept. + st.line_width = 10 + wide, _ := run_stroke(&p, st) + defer flat_path_destroy(&wide) + testing.expect(t, len(wide.points) > 4 * segments + 4, + "a wide pen keeps the joins it needs") +} + +// --------------------------------------------------------------------------- +// Accumulation, robustness +// --------------------------------------------------------------------------- + +// Existing contents of `out` survive, so several strokes can be accumulated +// into one path. +@(test) +test_stroke_appends_to_out :: proc(t: ^testing.T) { + rect := path_make() + defer path_destroy(&rect) + path_rect(&rect, 0, 0, 10, 10) + + fp := flat_path_make() + defer flat_path_destroy(&fp) + path_flatten(&fp, &rect, MATRIX_IDENTITY) + testing.expect_value(t, flat_path_count(&fp), 1) + + line := seg_path(100, 100, 200, 100) + defer path_destroy(&line) + st := STROKE_STATE_DEFAULT + st.line_width = 2 + st.line_cap = .Butt + err := stroke_flatten(&fp, &line, &st, MATRIX_IDENTITY) + + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 2) + testing.expect_value(t, sub_bounds(&fp, 0), Rect{0, 0, 10, 10}) + testing.expect_value(t, sub_bounds(&fp, 1), Rect{100, 99, 200, 101}) +} + +@(test) +test_stroke_rejects_bad_arguments :: proc(t: ^testing.T) { + p := seg_path(0, 0, 10, 0) + defer path_destroy(&p) + fp := flat_path_make() + defer flat_path_destroy(&fp) + st := STROKE_STATE_DEFAULT + + testing.expect_value(t, stroke_flatten(nil, &p, &st, MATRIX_IDENTITY), Error.Invalid_Argument) + testing.expect_value(t, stroke_flatten(&fp, nil, &st, MATRIX_IDENTITY), Error.Invalid_Argument) + + nan := math.nan_f64() + bad := Matrix{1, 0, 0, 1, nan, 0} + testing.expect_value(t, stroke_flatten(&fp, &p, &st, bad), Error.Invalid_Argument) + testing.expect_value(t, flat_path_count(&fp), 0) + + // A nil state is the PDF default state, not a crash. + testing.expect_value(t, stroke_flatten(&fp, &p, nil, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -0.5, 10, 0.5}) + + // An empty path is nothing to do, not an error. + empty := path_make() + defer path_destroy(&empty) + fp2 := flat_path_make() + defer flat_path_destroy(&fp2) + testing.expect_value(t, stroke_flatten(&fp2, &empty, &st, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, flat_path_count(&fp2), 0) +} + +// A CTM that collapses the plane onto a line or a point maps the outline to +// zero area. Content is hidden this way on purpose, so it is not an error -- +// but nothing may be emitted, and certainly nothing non-finite. +@(test) +test_stroke_singular_ctm :: proc(t: ^testing.T) { + p := seg_path(0, 0, 10, 10) + defer path_destroy(&p) + st := STROKE_STATE_DEFAULT + st.line_width = 5 + + for ctm in ([3]Matrix{ + Matrix{1, 0, 0, 0, 0, 0}, // flattened onto the x axis + Matrix{0, 0, 0, 0, 0, 0}, // collapsed to a point + Matrix{1, 1, 2, 2, 0, 0}, // rank one + }) { + fp, err := run_stroke(&p, st, ctm) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 0) + } +} + +// A CTM and a line width that are each finite, and each accepted by every +// guard on the way in, can still multiply into an infinity -- or, once a +// rotation adds +Inf to -Inf, into a NaN. The finiteness check in stroke_emit +// runs in BUILD space and cannot see it, so the product must be caught where +// the outline is transformed. Nothing below the Device line may hold either +// (ARCHITECTURE.md rule 5); a Flat_Path is read by the clip-mask builder and +// the display list as well as by the rasteriser, and only the rasteriser +// clamps. Both literals are ones `parse_number` accepts out of a real file. +@(test) +test_stroke_overflowing_ctm_emits_nothing :: proc(t: ^testing.T) { + p := seg_path(0, 0, 1, 1) + defer path_destroy(&p) + st := STROKE_STATE_DEFAULT + st.line_width = 1e160 + + for ctm in ([3]Matrix{ + Matrix{1e150, 0, 0, 1e150, 0, 0}, // the offsets overflow + Matrix{1e150, 1e150, -1e150, 1e150, 0, 0}, // rotated: +Inf plus -Inf + Matrix{1e300, 0, 0, 1e300, 0, 0}, + }) { + fp, err := run_stroke(&p, st, ctm) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + for pt in fp.points { + testing.expectf(t, is_finite(pt.x) && is_finite(pt.y), + "ctm %v let the non-finite point %v through", ctm, pt) + } + // Every contour of this stroke overflows, so there is nothing left to + // paint and no bounds to compute from. + testing.expect_value(t, flat_path_count(&fp), 0) + testing.expect_value(t, flat_path_bounds(&fp), RECT_EMPTY) + } +} + +// Nonsense in the stroke state must degrade, not crash or hang: a negative +// width, a non-finite width, a miter limit below the geometric minimum of 1, +// a non-finite phase and an over-long dash count are all things files carry. +@(test) +test_stroke_bad_state_degrades :: proc(t: ^testing.T) { + p := seg_path(0, 0, 100, 0) + defer path_destroy(&p) + + st := STROKE_STATE_DEFAULT + st.line_cap = .Butt + st.line_width = -4 + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + // A negative width is treated as zero, so it draws a hairline. + testing.expect_value(t, flat_path_bounds(&fp), Rect{0, -0.5, 100, 0.5}) + + st.line_width = math.nan_f64() + fp2, _ := run_stroke(&p, st) + defer flat_path_destroy(&fp2) + testing.expect_value(t, flat_path_bounds(&fp2), Rect{0, -0.5, 100, 0.5}) + + // A non-finite miter limit falls back to the default rather than + // silently bevelling every join in the document. + st.line_width = 2 + st.line_cap = .Butt + st.line_join = .Miter + st.miter_limit = math.nan_f64() + corner := path_make() + defer path_destroy(&corner) + path_move_to(&corner, 0, 0) + path_line_to(&corner, 10, 0) + path_line_to(&corner, 10, 10) + fpm, _ := run_stroke(&corner, st) + defer flat_path_destroy(&fpm) + testing.expect(t, has_point(&fpm, 11, -1, 1e-9), "a NaN miter limit uses the default of 10") + + st.line_width = 10 + st.miter_limit = 0 + st.dash_phase = math.inf_f64(1) + st.dash[0] = 10 + st.dash[1] = 10 + st.dash_count = 999 // beyond MAX_DASH_ENTRIES + fp3, err3 := run_stroke(&p, st) + defer flat_path_destroy(&fp3) + testing.expect_value(t, err3, Error.None) + testing.expect(t, flat_path_count(&fp3) > 0, "a garbage state still draws something") + for pt in fp3.points { + testing.expect(t, is_finite(pt.x) && is_finite(pt.y), "no non-finite points escape") + } +} + +// A path with thousands of subpaths and joins must terminate and stay within +// the output bound. +@(test) +test_stroke_many_subpaths :: proc(t: ^testing.T) { + p := path_make() + defer path_destroy(&p) + for i in 0 ..< 2000 { + x := f64(i) + path_move_to(&p, x, 0) + path_line_to(&p, x + 0.5, 1) + path_line_to(&p, x + 1, 0) + } + + st := STROKE_STATE_DEFAULT + st.line_width = 3 + st.line_join = .Round + st.line_cap = .Round + fp, err := run_stroke(&p, st) + defer flat_path_destroy(&fp) + testing.expect_value(t, err, Error.None) + testing.expect_value(t, flat_path_count(&fp), 2000) + testing.expect(t, len(fp.points) < MAX_STROKE_POINTS, "output stays inside its bound") +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +// The hairline test and the dash-visibility test both key off the extreme +// scale factors of the CTM, so they had better be the real singular values. +@(test) +test_stroke_scale_extremes :: proc(t: ^testing.T) { + check :: proc(t: ^testing.T, m: Matrix, want_lo, want_hi: f64) { + lo, hi := stroke_scale_extremes(m) + testing.expectf(t, near(lo, want_lo, 1e-9) && near(hi, want_hi, 1e-9), + "extremes of %v: got (%v, %v) want (%v, %v)", m, lo, hi, want_lo, want_hi) + // Brute force: no direction is stretched outside [lo, hi]. + for i in 0 ..< 360 { + ang := f64(i) * math.PI / 180 + v := transform_vector(m, Point{math.cos(ang), math.sin(ang)}) + l := stroke_len(v.x, v.y) + testing.expect(t, l >= lo - 1e-9 && l <= hi + 1e-9, "direction stretched outside range") + } + } + check(t, MATRIX_IDENTITY, 1, 1) + check(t, scale_matrix(100, 100), 100, 100) + check(t, scale_matrix(1, 4), 1, 4) + check(t, scale_matrix(-3, 2), 2, 3) + check(t, rotate_matrix(37), 1, 1) + check(t, matrix_mul(scale_matrix(2, 5), rotate_matrix(30)), 2, 5) + check(t, Matrix{0, 0, 0, 0, 9, 9}, 0, 0) +} + +// The floored pen must not move the path an inch: `post` composed after +// `to_build` is the CTM itself. What changes is only the pen, whose short +// device semi-axis comes out at half a pixel and whose long one is left at +// half the line width times the CTM's largest stretch. +@(test) +test_stroke_squashed_pen :: proc(t: ^testing.T) { + for m in ([5]Matrix{ + Matrix{1, 0, 0, 0.02, 0, 0}, + Matrix{0.02, 0, 0, 10, 0, 0}, + matrix_mul(Matrix{1, 0, 0, 0.02, 0, 0}, rotate_matrix(30)), + matrix_mul(rotate_matrix(-72), Matrix{0.5, 0, 0, 0.001, 3, -4}), + Matrix{1, 0.4, -0.3, 0.01, 12, 7}, + }) { + width := 1.0 + lo, hi := stroke_scale_extremes(m) + post, to_build, ok := stroke_squashed_pen(m, width, lo, hi) + testing.expectf(t, ok, "no pen for %v", m) + + c := matrix_mul(to_build, post) + testing.expectf(t, + near(c.a, m.a, 1e-9) && near(c.b, m.b, 1e-9) && near(c.c, m.c, 1e-9) && + near(c.d, m.d, 1e-9) && near(c.e, m.e, 1e-9) && near(c.f, m.f, 1e-9), + "%v: post after to_build came out %v", m, c) + + // post maps the build-space pen -- a circle of radius 1/2 -- onto the + // device ellipse, so its singular values are the semi-axes doubled. + plo, phi := stroke_scale_extremes(post) + testing.expectf(t, near(plo * 0.5, HAIRLINE_WIDTH * 0.5, 1e-9), + "%v: short semi-axis is %v, want half a pixel", m, plo * 0.5) + testing.expectf(t, near(phi * 0.5, max(width * 0.5 * hi, HAIRLINE_WIDTH * 0.5), 1e-9), + "%v: long semi-axis is %v, want %v", m, phi * 0.5, width * 0.5 * hi) + } +} + +// The chord count for an arc has to come from the tolerance, and the result +// has to stay inside it whatever the radius. +@(test) +test_stroke_arc_steps :: proc(t: ^testing.T) { + for r in ([5]f64{0.5, 1, 10, 1000, 100000}) { + n := stroke_arc_steps(math.PI, r, 0.25, MAX_ARC_SEGMENTS) + testing.expect(t, n >= 1 && n <= MAX_ARC_SEGMENTS, "step count is bounded") + if n < MAX_ARC_SEGMENTS { + sagitta := r * (1 - math.cos(math.PI / f64(n) / 2)) + testing.expectf(t, sagitta <= 0.25 + 1e-9, + "radius %v: %d chords leave a sagitta of %v", r, n, sagitta) + } + } + // A zero tolerance must saturate rather than divide by zero. + testing.expect_value(t, stroke_arc_steps(math.PI, 10, 0, MAX_ARC_SEGMENTS), MAX_ARC_SEGMENTS) +} + +// --------------------------------------------------------------------------- +// Flattening through the clip +// --------------------------------------------------------------------------- + +// The path is flattened before the pen is applied, so the caller's clip has to +// be grown by everything the pen adds -- half the widest device thickness and +// the spike a miter join can put on a corner -- before a curve may be judged +// out of reach. Too small a margin loses ink, and it is ink at the edge of the +// clip, where it is least likely to be noticed. The outline built with the +// clip in hand is therefore held to the one built without it, point by point +// over the whole clip. +// +// Dashes are the case where the shortcut is not available at all: they are +// laid out by arc length along the flattened polyline, so replacing an +// off-screen excursion with its chord shifts the phase of every dash after it. +// Both spellings are checked here. +@(test) +test_stroke_cull_keeps_the_same_ink :: proc(t: ^testing.T) { + // Two shapes: one whose curves leave the clip by thousands of units, which + // is what the cull is for, and one that sits just outside it -- close + // enough that a nine-wide pen paints back in, which is what the margin is + // for. The second is stroked on its own, because ink from the rest of a + // path would hide a difference at the edge. + far := path_make() + defer path_destroy(&far) + path_move_to(&far, 10, 10) + path_curve_to(&far, 3000, -3000, -3000, 3000, 90, 60) + path_curve_to(&far, -800, 1200, 1500, 900, 40, 20) + path_curve_to(&far, 5000, 5000, 6000, 5000, 80, 70) + path_line_to(&far, 10, 10) + + near_edge := path_make() + defer path_destroy(&near_edge) + path_move_to(&near_edge, -2, 20) + path_curve_to(&near_edge, -6, 25, -6, 35, -2, 40) + + paths := [2]^Path{&far, &near_edge} + for path, pi in paths { + for dashed in ([2]bool{false, true}) { + st := STROKE_STATE_DEFAULT + st.line_width = 9 + st.line_cap = .Round + st.miter_limit = 10 + if dashed { + st.dash_count = 2 + st.dash[0] = 7 + st.dash[1] = 3 + } + + full := flat_path_make() + defer flat_path_destroy(&full) + culled := flat_path_make() + defer flat_path_destroy(&culled) + testing.expect_value(t, stroke_flatten(&full, path, &st, MATRIX_IDENTITY), Error.None) + testing.expect_value(t, stroke_flatten(&culled, path, &st, MATRIX_IDENTITY, FLATNESS, + Rect{-1, -1, 121, 91}), Error.None) + + ink, differ := 0, 0 + for y := 0.5; y < 90; y += 1 { + for x := 0.5; x < 120; x += 1 { + a := covered(&full, x, y) + if a { ink += 1 } + if a != covered(&culled, x, y) { differ += 1 } + } + } + testing.expectf(t, ink > 20, "path %d dashed=%v: it has to reach the clip", pi, dashed) + testing.expectf(t, differ == 0, "path %d dashed=%v: %d sampled pixels differ", + pi, dashed, differ) + } + } +} diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000..0c60394 --- /dev/null +++ b/testdata/README.md @@ -0,0 +1,155 @@ +# testdata — the golden PDFs + +Fourteen committed files, one per structural case, asserted by +`golden_test.odin`. They are deliberately tiny (370 B to 1.4 KB) and, wherever +the format allows, plain text: open one in an editor and the whole of it is +visible at once. + +## Why these exist at all + +The rest of the suite builds its PDFs in memory with the `pb_*` builders in +`xref_test.odin`, and should keep doing so — the builder records byte offsets as +it writes, so a test never asserts a hand-counted number. But a builder is part +of the same codebase as the reader, and there are three things it therefore +cannot show: + +1. that `open_file` works on a real file on disk, not just on a byte slice; +2. that byte offsets written by *some other tool* are the ones we parse — a + committed file's offsets cannot silently follow a change in how we emit them; +3. that a refactor did not quietly change what the library accepts by changing + the builder at the same time. These bytes are frozen; the builder is not. + +They also give the render layer, when it arrives, fourteen pages it can draw +without inventing its own fixtures. + +## What every file has in common + +- One page (two in `page-tree-deep.pdf`) whose content stream draws a filled + blue rectangle and a red stroked diagonal: + + ``` + % + 0.20 0.40 0.90 rg + 20 20 160 100 re + f + 0.90 0.10 0.10 RG + 4 w + 20 20 m 180 180 l + S + ``` + + The first line names the file, so a test that decodes a content stream can say + *which* file's stream it got. `golden_test.odin` asserts the marker line on + page 0 and `160 100 re` on every page. + +- `/MediaBox [0 0 200 200]`, except where the file's whole point is a different + box: `incremental-update.pdf` (300×300 after the update) and + `page-tree-deep.pdf` (300×400, inherited). + +- No `/Filter` on the content stream. Compression is exercised by the filter + unit tests, which have the format's own vectors; a golden file that had to be + inflated before it could be read would be a worse golden file. + +## The files + +| File | Exercises | Opens with | +|---|---|---| +| `classic-xref.pdf` | the minimal well-formed document: four objects, one `xref` subsection, `trailer`, `startxref` | no repairs | +| `xref-stream.pdf` | PDF 1.5 `/Type /XRef` stream, `/W [1 4 2]`, FlateDecode + `/Predictor 12 /Columns 7` | no repairs | +| `object-stream.pdf` | catalog, page-tree root and page living inside a `/Type /ObjStm`; xref stream with type-2 entries | no repairs | +| `incremental-update.pdf` | two generations; the second rewrites object 3 and adds object 5, with a three-subsection table and `/Prev` | no repairs | +| `junk-before-header.pdf` | `%PDF-` at offset 70, every offset in the file measured from the header rather than from byte zero | no repairs | +| `broken-xref.pdf` | `startxref` pointing into a run of garbage; recovery is by scanning for `N G obj` | `Bad_Startxref`, `Rebuilt_Xref` | +| `bad-length.pdf` | a content stream whose `/Length` says 12 and whose body is 92 bytes; `endstream` has to be found by scanning | `Bad_Stream_Length` | +| `no-xref.pdf` | no table, no `trailer`, no `startxref` at all — objects and `%%EOF`, nothing else | `Bad_Startxref`, `Rebuilt_Xref`, `Missing_Root` | +| `page-tree-broken.pdf` | a `/Kids` cycle (2 → 3 → 2) with a real page hanging off the far side of it | `Cyclic_Page_Tree` | +| `encrypted-rc4-40.pdf` | standard security handler, `/V 1 /R 2`, RC4 40-bit | no repairs | +| `encrypted-rc4-128.pdf` | `/V 2 /R 3 /Length 128`, RC4 128-bit | no repairs | +| `encrypted-aes128.pdf` | `/V 4 /R 4`, crypt filter `/StdCF` with `/CFM /AESV2` | no repairs | +| `encrypted-aes256.pdf` | `/V 5 /R 6`, `/CFM /AESV3`, with `/UE`, `/OE` and `/Perms` | no repairs | +| `page-tree-deep.pdf` | three nested `/Pages` levels; `/MediaBox` and `/Rotate` declared only on the root, `/Resources` only on the middle node; page 2 overrides `/Rotate` alone | no repairs | + +"Opens with" is the *exact* set of `Repair_Kind`s the file must record once every +page's content stream has been read — `golden_test.odin` compares the set, not a +subset. A file that starts opening more cleanly than this is as much a +regression as one that starts opening less cleanly, and both fail here. + +Two details worth knowing before editing one of these: + +- `broken-xref.pdf`'s garbage run is bare keywords (`garbagegarbage…`), not a + `%` comment. A comment is skipped by the lexer, which would carry the probe + straight on to the real `xref` table and make the bad offset work by accident. + It is also longer than 2 × `XREF_LOCATE_WINDOW` so that the ±64-byte search + around the bad offset cannot reach an object header either. +- `bad-length.pdf`'s repair is recorded when the *stream* is parsed, which is + lazy. A test that reads `doc.repairs` straight after `open_file` sees nothing. + +## Encryption + +All four encrypted files share one arrangement: + +- **user password empty** — the file opens without prompting; +- **owner password `owner`** — so the owner branch of the key derivation is + exercised by a real file and not only by a unit vector; +- `/P -44`: bits 3 (modify) and 5 (annotate) clear, everything else set. R2 only + ever defined four permission bits, so `encrypted-rc4-40.pdf` reports + `{Print, Copy}` where the others report six; +- a `/Info` dictionary whose `/Title` is the encrypted string `Golden File`, so + that string decryption is covered as well as stream decryption; +- a fixed `/ID` of `9A1C2B3D4E5F60718293A4B5C6D7E8F9` (both halves), which is an + input to the R2–R4 key derivation and therefore may not be changed on its own; +- a classic `xref` table rather than a cross-reference stream, because an xref + stream is read before the file key exists and is never encrypted — which would + make the file exercise less, not more. + +`encrypted-aes256.pdf` additionally fixes the file encryption key itself (it is +wrapped in `/UE` and `/OE` rather than derived), the two 8-byte salts in `/U` and +the two in `/O`. `/Perms` is the 16 bytes `P` (little-endian) ‖ `FF FF FF FF` ‖ +`'T'` ‖ `"adb"` ‖ `"odin"`, AES-256-ECB encrypted with the file key. + +AES payloads are `IV ‖ CBC(PKCS#7(plaintext))`. The IVs are fixed, not random: +`iv[i] = obj_num XOR (i*17 + 3)`. Nothing checks them, and a golden file whose +bytes changed on every regeneration would not be golden. + +## Regenerating one + +There is deliberately **no generator in the repository**. The point of these +files is that the bytes are fixed: a generator that ran at test time would be +one more piece of the same codebase, and would answer none of the three +questions at the top of this file. Editing a file by hand in a text editor is +the expected way to change one — every offset that matters is in the `xref` +table at the end, and `golden_test.odin` will tell you immediately if you got +one wrong. + +If you do need to rebuild one from scratch, the recipe that produced these was a +throwaway `@(test)` procedure in the package (so that it could use the +`@(private)` `pb_*` builders and the `crypt.odin` primitives), which then wrote +the result with `os.write_entire_file`. The parts that are not obvious: + +- **Structure.** `pb_make`, `pb_obj`, `pb_classic_xref`, `pb_trailer` and + `pb_startxref` from `xref_test.odin` do the whole of `classic-xref.pdf`. The + content-stream objects were written with a local variant of `pb_stream_obj` + that omits the double space an empty dictionary head leaves behind. +- **Cross-reference streams.** `xref_rows_encode(rows, 1, 4, 2)` lays out the + rows; `png_up_encode(raw, 7)` applies the PNG `Up` filter for `/Predictor 12`; + `zlib_stored` wraps the result in stored DEFLATE blocks, which is a valid zlib + stream and keeps the output byte-stable without needing a compressor. +- **Object streams.** `objstm_payload(nums, bodies)` returns the payload and the + `/First` the dictionary must declare. The one here is left unfiltered so that + the pair table and the three objects stay legible. +- **`/O` and `/U` for R2–R4.** The library only ever needs the *inverse* of + Algorithm 3 (`crypt_owner_password`, which decrypts `/O` back to the user + password), so the encrypting half has to be written out: pad the owner + password, MD5 it, iterate MD5 50 times over the full 16-byte digest when + R ≥ 3, take the first `n` bytes as an RC4 key, RC4 the padded *user* password, + and for R ≥ 3 repeat 19 more times with the key XORed by `i` for `i` = 1…19 — + ascending, where the decrypting direction descends. `/U` is then just + `crypt_key_r234` followed by `crypt_user_value`, both of which the library + already has. +- **R6.** `crypt_hash_r56` is Algorithm 2.B and is used as-is for all four + hashes. `/UE` and `/OE` are `aes_cbc_encrypt` of the 32-byte file key under + the key-salt hash with a zero IV and no padding. + +After regenerating, update the row in `golden_test.odin`'s `GOLDEN` table if the +page size, page count, marker line or repair set changed. Nothing else in the +suite reads these files. diff --git a/testdata/bad-length.pdf b/testdata/bad-length.pdf new file mode 100644 index 0000000..4b20165 --- /dev/null +++ b/testdata/bad-length.pdf @@ -0,0 +1,37 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 12 >> +stream +% bad-length +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +trailer +<< /Size 5 /Root 1 0 R >> +startxref +367 +%%EOF diff --git a/testdata/broken-xref.pdf b/testdata/broken-xref.pdf new file mode 100644 index 0000000..19e385f --- /dev/null +++ b/testdata/broken-xref.pdf @@ -0,0 +1,45 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 93 >> +stream +% broken-xref +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +garbagegarbagegarbage garbagegarbagegarbage garbagegarbage +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +trailer +<< /Size 5 /Root 1 0 R >> +startxref +568 +%%EOF diff --git a/testdata/classic-xref.pdf b/testdata/classic-xref.pdf new file mode 100644 index 0000000..2452749 --- /dev/null +++ b/testdata/classic-xref.pdf @@ -0,0 +1,37 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 94 >> +stream +% classic-xref +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +trailer +<< /Size 5 /Root 1 0 R >> +startxref +369 +%%EOF diff --git a/testdata/encrypted-aes128.pdf b/testdata/encrypted-aes128.pdf new file mode 100644 index 0000000..0ff5d7e --- /dev/null +++ b/testdata/encrypted-aes128.pdf @@ -0,0 +1,37 @@ +%PDF-1.6 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 128 >> +stream +!2C\m~�˜©ºËäõ»8Îá­Y~mR’‡CÇ®$í®†6:T¼á‡$L³¸¸dägVÛê[ûVe‡ødå3™B¼ƒR]ÚRxHäkšÏ%ðç’w_‹�úôoë~^lÞôªо轀nX¼‰l_2F í.=âö¹õ4*œ +endstream +endobj +5 0 obj +<< /Title <06112033425D6C7F8E99A8BBCAE5F40791B0DFC20181D644F6FA19EB2BF92931> >> +endobj +6 0 obj +<< /Filter /Standard /V 4 /R 4 /Length 128 /CF << /StdCF << /CFM /AESV2 /AuthEvent /DocOpen /Length 16 >> >> /StmF /StdCF /StrF /StdCF /EncryptMetadata true /O <566FA873EE33C797CD3B904FDADF814AFA34DF9A38F6ED41B984E2C6DA2AA6F5> /U <04E69B49575902CD47A461F90975799100000000000000000000000000000000> /P -44 >> +endobj +xref +0 7 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +0000000404 00000 n +0000000499 00000 n +trailer +<< /Size 7 /Root 1 0 R /Info 5 0 R /Encrypt 6 0 R /ID [<9A1C2B3D4E5F60718293A4B5C6D7E8F9> <9A1C2B3D4E5F60718293A4B5C6D7E8F9>] >> +startxref +821 +%%EOF diff --git a/testdata/encrypted-aes256.pdf b/testdata/encrypted-aes256.pdf new file mode 100644 index 0000000..4c518b2 --- /dev/null +++ b/testdata/encrypted-aes256.pdf @@ -0,0 +1,38 @@ +%PDF-2.0 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 128 >> +stream +!2C\m~�˜©ºËäõ¯�›ÇZkÚ 1CŠœÖÙ)J¢JŽÚõ»Ç¿ÿ,� >> +endobj +6 0 obj +<< /Filter /Standard /V 5 /R 6 /Length 256 /CF << /StdCF << /CFM /AESV3 /AuthEvent /DocOpen /Length 32 >> >> /StmF /StdCF /StrF /StdCF /EncryptMetadata true /O <363382C35280D1B1F38BD862F8A55B4B76CCB803C384613CBE26F2062B67C6B811223344556677888877665544332211> /U <1E500E81EF817EB3BECC79AA210AE9A4B51CAB5B51D1CC3772D51B1E8807AF570123456789ABCDEFFEDCBA9876543210> /OE <3544CF567AA1F49774347ABCDC9603E4461596DF24C8F6A8429E9EFC24FD40E8> /UE <5BCA59428091C155EA83D8F0B8C3567316081A3614AA3084A5508F72CAE98140> /Perms /P -44 >> +endobj +xref +0 7 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +0000000404 00000 n +0000000499 00000 n +trailer +<< /Size 7 /Root 1 0 R /Info 5 0 R /Encrypt 6 0 R /ID [<9A1C2B3D4E5F60718293A4B5C6D7E8F9> <9A1C2B3D4E5F60718293A4B5C6D7E8F9>] >> +startxref +1069 +%%EOF diff --git a/testdata/encrypted-rc4-128.pdf b/testdata/encrypted-rc4-128.pdf new file mode 100644 index 0000000..0b027fa Binary files /dev/null and b/testdata/encrypted-rc4-128.pdf differ diff --git a/testdata/encrypted-rc4-40.pdf b/testdata/encrypted-rc4-40.pdf new file mode 100644 index 0000000..e5ca4b8 --- /dev/null +++ b/testdata/encrypted-rc4-40.pdf @@ -0,0 +1,37 @@ +%PDF-1.6 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 98 >> +stream +pj¢›(Q[㦤OK*bÝ¢Ñìõ(%÷Ç=º*-žò›jóùð`ýÛü 䥪�pʪ‰Ä°.Wi–|„ ¸à"±]œ&MæÍÏe ü~d$¢“,ÇñÁb$OkR +endstream +endobj +5 0 obj +<< /Title <5CC0D3FC3ED8A04994CBA4> >> +endobj +6 0 obj +<< /Filter /Standard /V 1 /R 2 /O /U /P -44 >> +endobj +xref +0 7 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +0000000373 00000 n +0000000426 00000 n +trailer +<< /Size 7 /Root 1 0 R /Info 5 0 R /Encrypt 6 0 R /ID [<9A1C2B3D4E5F60718293A4B5C6D7E8F9> <9A1C2B3D4E5F60718293A4B5C6D7E8F9>] >> +startxref +622 +%%EOF diff --git a/testdata/incremental-update.pdf b/testdata/incremental-update.pdf new file mode 100644 index 0000000..c78e634 --- /dev/null +++ b/testdata/incremental-update.pdf @@ -0,0 +1,66 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 113 >> +stream +% incremental-update generation 1 +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +trailer +<< /Size 5 /Root 1 0 R /ID [<0102030405060708090A0B0C0D0E0F10> <0102030405060708090A0B0C0D0E0F10>] >> +startxref +389 +%%EOF +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Resources << >> /Contents 5 0 R >> +endobj +5 0 obj +<< /Length 113 >> +stream +% incremental-update generation 2 +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 1 +0000000000 65535 f +3 1 +0000000628 00000 n +5 1 +0000000732 00000 n +trailer +<< /Size 6 /Root 1 0 R /Prev 389 /ID [<0102030405060708090A0B0C0D0E0F10> <1112131415161718191A1B1C1D1E1F20>] >> +startxref +896 +%%EOF diff --git a/testdata/junk-before-header.pdf b/testdata/junk-before-header.pdf new file mode 100644 index 0000000..88b60d9 --- /dev/null +++ b/testdata/junk-before-header.pdf @@ -0,0 +1,41 @@ +HTTP/1.1 200 OK +Content-Type: application/pdf +Content-Length: 999 + +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 100 >> +stream +% junk-before-header +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000225 00000 n +trailer +<< /Size 5 /Root 1 0 R >> +startxref +376 +%%EOF diff --git a/testdata/no-xref.pdf b/testdata/no-xref.pdf new file mode 100644 index 0000000..263b18d --- /dev/null +++ b/testdata/no-xref.pdf @@ -0,0 +1,26 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >> +endobj +4 0 obj +<< /Length 89 >> +stream +% no-xref +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +%%EOF diff --git a/testdata/object-stream.pdf b/testdata/object-stream.pdf new file mode 100644 index 0000000..c80f1f0 Binary files /dev/null and b/testdata/object-stream.pdf differ diff --git a/testdata/page-tree-broken.pdf b/testdata/page-tree-broken.pdf new file mode 100644 index 0000000..8109c5f --- /dev/null +++ b/testdata/page-tree-broken.pdf @@ -0,0 +1,41 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Pages /Parent 2 0 R /Kids [2 0 R 4 0 R] /Count 1 >> +endobj +4 0 obj +<< /Type /Page /Parent 3 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 5 0 R >> +endobj +5 0 obj +<< /Length 98 >> +stream +% page-tree-broken +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000198 00000 n +0000000302 00000 n +trailer +<< /Size 6 /Root 1 0 R >> +startxref +450 +%%EOF diff --git a/testdata/page-tree-deep.pdf b/testdata/page-tree-deep.pdf new file mode 100644 index 0000000..80e94de --- /dev/null +++ b/testdata/page-tree-deep.pdf @@ -0,0 +1,64 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 2 /MediaBox [0 0 300 400] /Rotate 90 >> +endobj +3 0 obj +<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 2 /Resources << >> >> +endobj +4 0 obj +<< /Type /Pages /Parent 3 0 R /Kids [5 0 R 6 0 R] /Count 2 >> +endobj +5 0 obj +<< /Type /Page /Parent 4 0 R /Contents 7 0 R >> +endobj +6 0 obj +<< /Type /Page /Parent 4 0 R /Rotate 180 /Contents 8 0 R >> +endobj +7 0 obj +<< /Length 103 >> +stream +% page-tree-deep page 1 +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +8 0 obj +<< /Length 103 >> +stream +% page-tree-deep page 2 +0.20 0.40 0.90 rg +20 20 160 100 re +f +0.90 0.10 0.10 RG +4 w +20 20 m 180 180 l +S + +endstream +endobj +xref +0 9 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000156 00000 n +0000000244 00000 n +0000000321 00000 n +0000000384 00000 n +0000000459 00000 n +0000000613 00000 n +trailer +<< /Size 9 /Root 1 0 R >> +startxref +767 +%%EOF diff --git a/testdata/xref-stream.pdf b/testdata/xref-stream.pdf new file mode 100644 index 0000000..7bb7070 Binary files /dev/null and b/testdata/xref-stream.pdf differ diff --git a/xref.odin b/xref.odin new file mode 100644 index 0000000..c9e51f0 --- /dev/null +++ b/xref.odin @@ -0,0 +1,1227 @@ +package pdf + +// Cross-reference reading: classic tables (ISO 32000-2, 7.5.4), cross-reference +// streams (7.5.8), the trailer chain, and rebuild-by-scanning. +// +// The layout of this file follows the order a reader meets the file's +// structure in: find `startxref`, walk the /Prev chain from the newest section +// to the oldest, merge the trailers, and -- when any of that does not add up -- +// throw the lot away and find the objects by scanning. +// +// Two rules run through everything here. +// +// The first is that a *newer* section always wins. The chain is walked newest +// to oldest and an entry that has already been established is never +// overwritten, because that is precisely what an incremental update means: the +// bytes at the end of the file supersede the ones before them. +// +// The second is that nothing in this file is allowed to fail the open while a +// rebuild is still possible. A cross-reference table is an index, not content; +// every object it describes is still in the file whether or not the index +// agrees, and a reader that refuses a document because its index is wrong is +// refusing a document that every other viewer opens. + +// Bytes scanned backwards from the end of the file for `startxref` before +// falling back to searching the whole file. The format requires it to be in +// the last few dozen bytes; 2 KiB covers the files that append a signature, +// a trailing comment banner or a stray page of junk after it. +@(private) +STARTXREF_WINDOW :: 2048 + +// How many `startxref` keywords are tried, newest first. A file whose last +// startxref was written by a tool that crashed part way through still has the +// previous one, and following that is far better than a full rebuild. +@(private) +MAX_STARTXREF_CANDIDATES :: 4 + +// How far either side of a claimed section offset the section is looked for. +// Producers that miscount by a byte or two are common; a window this small +// cannot wander into an unrelated part of the file. +@(private) +XREF_LOCATE_WINDOW :: 64 + +// Smallest a classic cross-reference entry can be written in. The format says +// 20 bytes exactly, real files use 19, and the tolerant reader below will take +// "0 0 n" -- 5 -- so that is what allocations are bounded against. +@(private) +XREF_ENTRY_MIN_BYTES :: 5 + +// Bytes searched past the end of the subsections for the `trailer` keyword +// when it is not where it should be. +@(private) +XREF_TRAILER_WINDOW :: 4096 + +// Trailer dictionaries kept while rebuilding. Files with more `trailer` +// keywords than this are not going to be diagnosed by reading more of them. +@(private) +MAX_REBUILD_TRAILERS :: 64 + +// Bytes of an object's body looked at before deciding whether it is worth +// parsing during a rebuild. An /ObjStm, /XRef or /Catalog dictionary names its +// own type long before this. +@(private) +REBUILD_PEEK_BYTES :: 4096 + +@(private) +Xref_Section_Kind :: enum u8 { + None = 0, + Classic, // "xref" followed by subsections and a trailer + Stream, // an indirect object holding a /Type /XRef stream +} + +// Xref_Loader is the state one call to `xref_load` (or `xref_rebuild`) needs +// beyond what the Document already holds. +// +// `defined` is parallel to `doc.xref` and is what makes "newer wins" work: a +// free entry written by a newer section has to shadow an in-use entry from an +// older one, and `Xref_Entry` on its own cannot tell "explicitly free" from +// "never mentioned". +@(private) +Xref_Loader :: struct { + doc: ^Document, + defined: [dynamic]bool, + visited: map[i64]bool, + // Newest first; merged oldest-to-newest so the newest entry wins. + trailers: [dynamic]^Dict, + max_objects: int, + // Entries established that are not free. A table that describes no live + // object at all is not a table. + n_live: int, + // Objects a rebuild's scan flagged as worth parsing: the ones whose bytes + // mention /ObjStm, /XRef or /Catalog. + candidates: [dynamic]u32, +} + +// --------------------------------------------------------------------------- +// Entry points +// --------------------------------------------------------------------------- + +// xref_load fills in `doc.xref` and `doc.trailer`. +// +// It returns an error only when the file has no recoverable structure at all: +// anything less than that -- a missing startxref, an offset pointing at +// nothing, a table that stops parsing half way, a /Root that is not a +// dictionary -- falls back to `xref_rebuild` and is recorded as a repair. +xref_load :: proc(doc: ^Document) -> Error { + if doc == nil { return .Invalid_Argument } + + ld := xref_loader_make(doc) + ok := xref_load_chain(&ld) && xref_finish(&ld) + + err := Error.None + if !ok { + err = xref_rebuild(doc) + } + + // Everything resolved above was resolved against a table that was still + // being built, and -- in an encrypted document -- before the file key + // existed. Neither is a state to leave a cache in. + clear(&doc.cache) + if dict_has(doc.trailer, "Encrypt") { + clear(&doc.objstm_cache) + } + return err +} + +// xref_rebuild discards the cross-reference table and finds the objects by +// scanning the whole file for "N G obj" headers. +// +// Later definitions of the same object number win, which is what an +// incremental update means when read this way. Objects that exist only inside +// an object stream are picked up afterwards, by reading the header of every +// /ObjStm the scan found. +xref_rebuild :: proc(doc: ^Document) -> Error { + if doc == nil { return .Invalid_Argument } + + add_repair(doc, .Rebuilt_Xref, 0) + clear(&doc.xref) + clear(&doc.cache) + clear(&doc.objstm_cache) + doc.trailer = nil + + ld := xref_loader_make(doc) + catalog: Ref + xref_scan_objects(&ld) + xref_scan_objstms(&ld, &catalog) + xref_rebuild_trailer(&ld, catalog) + + if ld.n_live == 0 { + // No object header anywhere in the file. There is nothing to read. + return .No_Xref + } + return .None +} + +@(private) +xref_loader_make :: proc(doc: ^Document) -> Xref_Loader { + ld := Xref_Loader{ + doc = doc, + max_objects = xref_max_objects(doc), + } + ld.defined = make([dynamic]bool, 0, 64, doc.allocator) + ld.visited = make(map[i64]bool, 8, doc.allocator) + ld.trailers = make([dynamic]^Dict, 0, 8, doc.allocator) + ld.candidates = make([dynamic]u32, 0, 16, doc.allocator) + return ld +} + +// xref_max_objects bounds the table by what the file could possibly describe. +// Every object costs bytes wherever it is written -- a header and an `endobj` +// at the top level, a "number offset" pair inside an object stream -- so the +// file's own length is a ceiling on how many there can be. The floor keeps a +// tiny hand-written file from being clamped below its own /Size. +@(private) +xref_max_objects :: proc(doc: ^Document) -> int { + n := max(len(doc.data), 1024) + return min(n, MAX_OBJECT_NUMBER + 1) +} + +// --------------------------------------------------------------------------- +// Table storage +// --------------------------------------------------------------------------- + +@(private) +xref_grow :: proc(ld: ^Xref_Loader, count: int) { + n := min(count, ld.max_objects) + old := len(ld.doc.xref) + if n <= old { return } + // `resize` reallocates to exactly the length asked for, and the table is + // grown one object at a time while a file is scanned. Reserving + // geometrically first is the difference between a linear rebuild and a + // quadratic one -- and, on an arena, between one copy of the table and + // thousands of abandoned ones. + if cap(ld.doc.xref) < n { + want := min(max(n, cap(ld.doc.xref) * 2, 64), ld.max_objects) + reserve(&ld.doc.xref, max(want, n)) + reserve(&ld.defined, max(want, n)) + } + resize(&ld.doc.xref, n) + resize(&ld.defined, n) + // Not every allocator hands back zeroed memory when a dynamic array is + // regrown into capacity it already held, and a stale entry here is an + // offset into the middle of the file. + for i in old ..< len(ld.doc.xref) { + ld.doc.xref[i] = {} + } + for i in old ..< len(ld.defined) { + ld.defined[i] = false + } +} + +// xref_set records an entry unless a newer section already spoke for it. +// +// `over_free` is the one exception, and it exists for hybrid-reference files: +// there the classic table deliberately marks every compressed object free so +// that a reader which cannot follow /XRefStm ignores them, and the stream that +// follows carries their real entries (ISO 32000-2, 7.5.8.4). +@(private) +xref_set :: proc(ld: ^Xref_Loader, num: u32, entry: Xref_Entry, over_free := false) { + if num == 0 || int(num) >= ld.max_objects { return } + xref_grow(ld, int(num) + 1) + if int(num) >= len(ld.doc.xref) { return } + + if ld.defined[num] { + replaces_free := over_free && + ld.doc.xref[num].kind == .Free && entry.kind != .Free + // The entry being replaced was free, so it was never counted as live. + if !replaces_free { return } + } + ld.doc.xref[num] = entry + ld.defined[num] = true + if entry.kind != .Free { + ld.n_live += 1 + } +} + +// xref_put overwrites unconditionally. Only the rebuild uses it: there the +// scan runs from the front of the file to the back, so a later definition is a +// newer one and must replace what came before it. +@(private) +xref_put :: proc(ld: ^Xref_Loader, num: u32, entry: Xref_Entry) { + if num == 0 || int(num) >= ld.max_objects { return } + xref_grow(ld, int(num) + 1) + if int(num) >= len(ld.doc.xref) { return } + + if ld.defined[num] && ld.doc.xref[num].kind != .Free { + ld.n_live -= 1 + } + ld.doc.xref[num] = entry + ld.defined[num] = true + if entry.kind != .Free { + ld.n_live += 1 + } +} + +@(private) +xref_entry_at :: proc(ld: ^Xref_Loader, num: u32) -> Xref_Entry { + if int(num) >= len(ld.doc.xref) { return {} } + return ld.doc.xref[num] +} + +// --------------------------------------------------------------------------- +// startxref +// --------------------------------------------------------------------------- + +// xref_last_startxref finds the last `startxref` before `before` whose keyword +// is followed by a number, and reports the number and where the keyword was. +// +// The search runs backwards because that is where the newest one is, and it is +// only ever called again -- for the one before it -- when the newest turned out +// to point at nothing. +@(private) +xref_last_startxref :: proc(doc: ^Document, before: int) -> (value: i64, at: int, ok: bool) { + needle := transmute([]byte)string("startxref") + limit := min(before, len(doc.data)) + for limit > 0 { + idx := last_index_bytes(doc.data[:limit], needle) + if idx < 0 { break } + limit = idx + if v, vok := xref_int_after(doc, idx + len(needle)); vok { + return v, idx, true + } + } + return 0, 0, false +} + +// xref_first_startxref looks in the last couple of kilobytes, where the format +// puts the keyword, before paying for a scan of the whole file. +@(private) +xref_first_startxref :: proc(doc: ^Document) -> (value: i64, at: int, ok: bool) { + tail := max(0, len(doc.data) - STARTXREF_WINDOW) + if tail > 0 { + needle := transmute([]byte)string("startxref") + if idx := last_index_bytes(doc.data[tail:], needle); idx >= 0 { + if v, vok := xref_int_after(doc, tail + idx + len(needle)); vok { + return v, tail + idx, true + } + } + } + return xref_last_startxref(doc, len(doc.data)) +} + +@(private) +xref_int_after :: proc(doc: ^Document, pos: int) -> (value: i64, ok: bool) { + if pos < 0 || pos > len(doc.data) { return 0, false } + lex := Lexer{data = doc.data, pos = pos} + tok := lexer_next_structural(&lex) + if tok.kind != .Integer || tok.int_value < 0 { return 0, false } + return tok.int_value, true +} + +// --------------------------------------------------------------------------- +// Locating a section +// --------------------------------------------------------------------------- + +// xref_probe reports what kind of cross-reference section starts at `off`, if +// any. It is the only thing that decides an offset is worth following, so it +// insists the keyword or object header begins on a token boundary: "xref" also +// occurs as the tail of "startxref". +@(private) +xref_probe :: proc(doc: ^Document, off: i64) -> Xref_Section_Kind { + if off < 0 || off >= i64(len(doc.data)) { return .None } + + lex := Lexer{data = doc.data, pos = int(off)} + t1 := lexer_next_structural(&lex) + if t1.start > 0 && is_pdf_regular(doc.data[t1.start - 1]) { return .None } + + if t1.kind == .Keyword && t1.text == "xref" { + return .Classic + } + if t1.kind != .Integer || t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER { + return .None + } + t2 := lexer_next_structural(&lex) + // "12 obj" -- a header whose generation number was dropped. + if t2.kind == .Keyword && t2.text == "obj" { return .Stream } + if t2.kind != .Integer || t2.int_value < 0 || t2.int_value > 65535 { return .None } + t3 := lexer_next_structural(&lex) + if t3.kind == .Keyword && t3.text == "obj" { return .Stream } + return .None +} + +// xref_locate turns an offset read out of the file into one that actually +// points at a section. +// +// Two things can be wrong with it. A file with junk before "%PDF-" measures +// its offsets from the header rather than from byte zero (and the ones written +// by a tool that did not notice the junk measure them from byte zero anyway, +// so both are tried). And a producer can simply be off by a few bytes, which +// is worth a small search rather than a whole-file rebuild. +@(private) +xref_locate :: proc(doc: ^Document, raw: i64) -> (off: i64, kind: Xref_Section_Kind) { + bases := [2]i64{raw + doc.header_offset, raw} + n_bases := doc.header_offset != 0 ? 2 : 1 + + for i in 0 ..< n_bases { + if k := xref_probe(doc, bases[i]); k != .None { + return bases[i], k + } + } + // Nearest first, so an exact-but-late match beats a spurious object header + // found behind us. + for d in 1 ..= i64(XREF_LOCATE_WINDOW) { + for i in 0 ..< n_bases { + for cand in ([2]i64{bases[i] + d, bases[i] - d}) { + if cand < 0 || cand >= i64(len(doc.data)) { continue } + if is_pdf_regular(doc.data[cand]) && cand > 0 && is_pdf_regular(doc.data[cand - 1]) { + continue + } + if k := xref_probe(doc, cand); k != .None { + return cand, k + } + } + } + } + return 0, .None +} + +// --------------------------------------------------------------------------- +// The chain +// --------------------------------------------------------------------------- + +@(private) +xref_load_chain :: proc(ld: ^Xref_Loader) -> bool { + doc := ld.doc + + raw, at, found := xref_first_startxref(doc) + if !found { + add_repair(doc, .Bad_Startxref, 0) + return false + } + + off, kind := xref_locate(doc, raw) + // A tool that crashed part way through an incremental update leaves a + // startxref pointing at nothing and the previous one still intact. + for _ in 1 ..< MAX_STARTXREF_CANDIDATES { + if kind != .None { break } + add_repair(doc, .Bad_Startxref, raw) + v, a, more := xref_last_startxref(doc, at) + if !more { break } + raw, at = v, a + off, kind = xref_locate(doc, raw) + } + if kind == .None { + add_repair(doc, .Bad_Startxref, raw) + return false + } + + got_any := false + for _ in 0 ..< MAX_XREF_CHAIN { + if ld.visited[off] or_else false { + // /Prev loops are a real shape, not a hypothetical one: a file + // whose last two sections point at each other opens in every + // viewer because every viewer keeps this set. + add_repair(doc, .Bad_Startxref, off) + break + } + ld.visited[off] = true + + trailer, ok := xref_read_section(ld, off, false) + if !ok { break } + got_any = true + append(&ld.trailers, trailer) + + xref_read_hybrid(ld, trailer) + + prev, has_prev := to_int(dict_get_raw(trailer, "Prev")) + if !has_prev { break } + poff, pkind := xref_locate(doc, prev) + if pkind == .None { + add_repair(doc, .Bad_Startxref, prev) + break + } + off = poff + } + return got_any +} + +// xref_read_hybrid reads the cross-reference stream a classic trailer points +// at with /XRefStm (ISO 32000-2, 7.5.8.4). +// +// It is read after the classic section it belongs to, so the classic entries +// win -- except where the classic table marked an object free, which in a +// hybrid file is how it hides compressed objects from older readers. +@(private) +xref_read_hybrid :: proc(ld: ^Xref_Loader, trailer: ^Dict) { + if trailer == nil { return } + raw, ok := to_int(dict_get_raw(trailer, "XRefStm")) + if !ok { return } + + off, kind := xref_locate(ld.doc, raw) + if kind != .Stream { return } + if ld.visited[off] or_else false { return } + ld.visited[off] = true + + // Its own /Prev is deliberately not followed: the classic chain drives. + stm_dict, sok := xref_read_stream(ld, off, true) + if sok && stm_dict != nil { + append(&ld.trailers, stm_dict) + } +} + +@(private) +xref_read_section :: proc(ld: ^Xref_Loader, off: i64, over_free: bool) -> (trailer: ^Dict, ok: bool) { + switch xref_probe(ld.doc, off) { + case .Classic: + return xref_read_table(ld, off) + case .Stream: + return xref_read_stream(ld, off, over_free) + case .None: + return nil, false + } + return nil, false +} + +// --------------------------------------------------------------------------- +// Classic cross-reference tables (ISO 32000-2, 7.5.4) +// --------------------------------------------------------------------------- + +// Xref_Raw_Entry is one line of a classic table, before it is decided which +// object number it belongs to. +@(private) +Xref_Raw_Entry :: struct { + offset: i64, + gen: i64, + used: bool, // 'n' rather than 'f' + ok: bool, +} + +// xref_next_entry reads one entry by tokens rather than by fixed offsets. +// +// The spec fixes the entry at 20 bytes, but 19-byte entries (a single LF +// instead of CRLF) are written by a large fraction of producers, and entries +// with a single space before the EOL, with no leading zeros, or with the +// fields separated by a tab all occur. None of that changes the tokens. +@(private) +xref_next_entry :: proc(doc: ^Document, lex: ^Lexer) -> (e: Xref_Raw_Entry) { + save := lex.pos + t1 := lexer_next_structural(lex) + if t1.kind != .Integer || t1.int_value < 0 { + lex.pos = save + return + } + t2 := lexer_next_structural(lex) + if t2.kind != .Integer || t2.int_value < 0 { + lex.pos = save + return + } + t3 := lexer_next_structural(lex) + if t3.kind != .Keyword || len(t3.text) == 0 { + lex.pos = save + return + } + c := t3.text[0] + if c != 'n' && c != 'f' { + lex.pos = save + return + } + // "...00000 n0000000123 00000 n": no separator at all after the type + // letter, so the tokeniser swallowed the next entry's offset with it. + if len(t3.text) > 1 { + lex.pos = t3.start + 1 + } + + e.offset = t1.int_value + e.gen = t2.int_value + e.used = c == 'n' + e.ok = true + return +} + +@(private) +xref_read_table :: proc(ld: ^Xref_Loader, off: i64) -> (trailer: ^Dict, ok: bool) { + doc := ld.doc + lex := Lexer{data = doc.data, pos = int(off)} + if !lexer_accept_keyword(&lex, "xref") { return nil, false } + + // Each subsection header costs at least four bytes ("0 0"), and the loop + // only continues while it is consuming tokens, so this bound is never the + // thing that stops it -- it is here because a table is file data. + fuel := len(doc.data) / 4 + 16 + first_sub := true + + for fuel > 0 { + fuel -= 1 + lexer_skip_whitespace(&lex) + if lex.pos >= len(doc.data) { break } + if lexer_at_keyword(&lex, "trailer") { break } + + save := lex.pos + t1 := lexer_next_structural(&lex) + t2 := lexer_next_structural(&lex) + if t1.kind != .Integer || t2.kind != .Integer { + lex.pos = save + break + } + xref_read_subsection(ld, &lex, t1.int_value, t2.int_value, first_sub) + first_sub = false + } + + return xref_read_trailer(ld, &lex), true +} + +@(private) +xref_read_subsection :: proc(ld: ^Xref_Loader, lex: ^Lexer, start_in, count_in: i64, first_sub: bool) { + doc := ld.doc + + if start_in < 0 || start_in > MAX_OBJECT_NUMBER { return } + + count := count_in + if count <= 0 { return } + // A subsection cannot describe more objects than the rest of the file has + // room to spell out, however large a number the header claims. + room := i64(len(doc.data) - lex.pos) / XREF_ENTRY_MIN_BYTES + 1 + if count > room { + add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos)) + count = room + } + count = min(count, i64(ld.max_objects)) + + // Look at the first few entries without consuming them: whether the + // declared starting object number is right is decided from what the file + // actually has at the offsets they name. + peek: [8]Xref_Raw_Entry + n_peek := 0 + { + probe := lex^ + for n_peek < len(peek) { + e := xref_next_entry(doc, &probe) + if !e.ok { break } + peek[n_peek] = e + n_peek += 1 + } + } + start := xref_correct_start(doc, peek[:n_peek], start_in, first_sub) + if start != start_in { + add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos), Ref{u32(min(start_in, MAX_OBJECT_NUMBER)), 0}) + } + if start + count > MAX_OBJECT_NUMBER + 1 { + count = MAX_OBJECT_NUMBER + 1 - start + } + + for i in 0 ..< count { + e := xref_next_entry(doc, lex) + if !e.ok { + // The table stops making sense here. Everything read so far is + // still good, and the entries after the damage are picked up from + // an older section or, failing that, by a rebuild. + add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos)) + break + } + num := start + i + if num < 1 { continue } + + gen := u16(clamp(e.gen, 0, 65535)) + if !e.used { + // A free entry's first field is the next free object number, not + // an offset; nothing here needs the free list. + xref_set(ld, u32(num), Xref_Entry{kind = .Free, gen = gen}) + continue + } + // Byte 0 is the header, so an in-use object can never be there. An + // offset past the end of the file is nonsense too. Leaving such an + // entry undefined lets an older section supply the object instead. + if e.offset <= 0 || e.offset >= i64(len(doc.data)) { continue } + at := xref_normalize_offset(doc, e.offset, num) + xref_set(ld, u32(num), Xref_Entry{kind = .Normal, offset = at, gen = gen}) + } +} + +// xref_object_at reports whether the header of object `num` is exactly at +// `off`. +@(private) +xref_object_at :: proc(doc: ^Document, off: i64, num: i64) -> bool { + if num < 1 || num > MAX_OBJECT_NUMBER { return false } + if off < 0 || off >= i64(len(doc.data)) { return false } + lex := Lexer{data = doc.data, pos = int(off)} + t1 := lexer_next_structural(&lex) + if t1.kind != .Integer || t1.int_value != num { return false } + t2 := lexer_next_structural(&lex) + // "12 obj": a header whose generation number was dropped. + if t2.kind == .Keyword && t2.text == "obj" { return true } + if t2.kind != .Integer { return false } + t3 := lexer_next_structural(&lex) + return t3.kind == .Keyword && t3.text == "obj" +} + +// xref_offset_is_object reports whether "num G obj" really is at `off`, from +// either origin, for the same reason `xref_locate` tries both. +@(private) +xref_offset_is_object :: proc(doc: ^Document, off: i64, num: i64) -> bool { + return xref_object_at(doc, off, num) || + (doc.header_offset != 0 && xref_object_at(doc, off + doc.header_offset, num)) +} + +// xref_normalize_offset resolves which origin an entry's offset was measured +// from, and returns one that indexes `doc.data`. +// +// A file with junk before "%PDF-" has two plausible readings of every offset in +// its table: from byte zero, and from the header. Producers do both -- the +// wrapper is usually added by a tool that never looked inside -- and the +// difference is every object in the file, so it is settled here, against the +// bytes, rather than left for the object reader to guess at one reference at a +// time. +@(private) +xref_normalize_offset :: proc(doc: ^Document, off: i64, num: i64) -> i64 { + if doc.header_offset == 0 { return off } + if xref_object_at(doc, off, num) { return off } + shifted := off + doc.header_offset + if xref_object_at(doc, shifted, num) { return shifted } + return off +} + +@(private) +xref_score_start :: proc(doc: ^Document, ents: []Xref_Raw_Entry, start: i64) -> int { + score := 0 + for e, i in ents { + if !e.used { continue } + if xref_offset_is_object(doc, e.offset, start + i64(i)) { + score += 1 + } + } + return score +} + +// xref_correct_start repairs the off-by-one in a subsection header. +// +// A table whose first subsection is written "1 N" when it really starts at 0 +// (or "0 N" when it really starts at 1) is one of the most common +// malformations there is -- it shifts every object number in the file by one, +// so a reader that trusts the header reads the wrong object for every +// reference. The header is checked against the file: whichever starting number +// makes the most of the entries land on the object they claim wins. +@(private) +xref_correct_start :: proc(doc: ^Document, ents: []Xref_Raw_Entry, start: i64, first_sub: bool) -> i64 { + if len(ents) == 0 { return start } + + base := xref_score_start(doc, ents, start) + best := start + best_score := base + for delta in ([2]i64{-1, 1}) { + cand := start + delta + if cand < 0 || cand > MAX_OBJECT_NUMBER { continue } + if s := xref_score_start(doc, ents, cand); s > best_score { + best, best_score = cand, s + } + } + if best != start { + return best + } + if base > 0 || !first_sub { + return start + } + + // Nothing verified either way -- the entries may point into a part of the + // file this section does not describe. Fall back to what the format + // guarantees: object 0 is the head of the free list and can never be in + // use, and the head is always generation 65535 (7.5.4). + if start == 1 && ents[0].ok && !ents[0].used && ents[0].gen == 65535 { + return 0 + } + if start == 0 && ents[0].ok && ents[0].used { + return 1 + } + return start +} + +@(private) +xref_read_trailer :: proc(ld: ^Xref_Loader, lex: ^Lexer) -> ^Dict { + doc := ld.doc + + if !lexer_accept_keyword(lex, "trailer") { + // Some producers drop the keyword and write the dictionary straight + // after the table; that is unambiguous, so take it. + probe := lex^ + tok := lexer_next_structural(&probe) + if tok.kind != .Dict_Open { + // Junk between the last entry and the keyword. Worth a bounded + // look: the alternative is losing /Root and rebuilding the file. + at := xref_find_token(doc.data, lex.pos, "trailer") + if at < 0 || at - lex.pos > XREF_TRAILER_WINDOW { return nil } + lex.pos = at + len("trailer") + } + } + + p := parser_make(doc, doc.data, doc.allocator) + p.lex.pos = lex.pos + // The trailer is never encrypted (ISO 32000-2, 7.6.2), and the zero + // obj_ref is what says so to the parser. + obj, err := parse_object(&p) + if err != .None { return nil } + d, ok := to_dict(obj) + if !ok { return nil } + return d +} + +// --------------------------------------------------------------------------- +// Cross-reference streams (ISO 32000-2, 7.5.8) +// --------------------------------------------------------------------------- + +@(private) +xref_read_stream :: proc(ld: ^Xref_Loader, off: i64, over_free: bool) -> (trailer: ^Dict, ok: bool) { + doc := ld.doc + + obj, _, err := parse_indirect_object_at(doc, off, Ref{}) + if err != .None { return nil, false } + s, is_stream := to_stream(obj) + if !is_stream || s == nil || s.dict == nil { return nil, false } + d := s.dict + + // /Type is required here, but a dictionary carrying /W and /Size is + // unambiguous enough that a missing one is not worth refusing. + if ty := dict_name(doc, d, "Type"); ty != "" && ty != "XRef" { return nil, false } + + // Field widths. A width of 0 means the field is not stored and takes its + // default: type 1 for the first field, 0 for the others (7.5.8.2). + w := dict_array(doc, d, "W") + n_w := array_len(w) + if n_w < 1 || n_w > 32 { return nil, false } + widths: [32]int + row := 0 + for i in 0 ..< n_w { + v := array_int(doc, w, i, 0) + if v < 0 || v > 8 { return nil, false } + widths[i] = int(v) + row += widths[i] + } + if row <= 0 { return nil, false } + + data, derr := stream_data(doc, s) + if derr != .None { return nil, false } + n_rows := len(data) / row + if n_rows == 0 { + // A /Type /XRef stream that decoded to nothing still carries the + // trailer, and the entries may all live in an older section. + return d, true + } + + w0 := widths[0] + w1 := n_w > 1 ? widths[1] : 0 + w2 := n_w > 2 ? widths[2] : 0 + off0 := 0 + off1 := w0 + off2 := w0 + w1 + + size := dict_int(doc, d, "Size", 0) + index := dict_array(doc, d, "Index") + n_pairs := array_len(index) / 2 + if n_pairs == 0 { + n_pairs = 1 // the default /Index is [0 /Size] + } + + consumed := 0 + for p in 0 ..< n_pairs { + start := i64(0) + count := size + if array_len(index) >= 2 { + start = array_int(doc, index, p * 2, 0) + count = array_int(doc, index, p * 2 + 1, 0) + } + if start < 0 || start > MAX_OBJECT_NUMBER { continue } + if count <= 0 { continue } + if count > i64(n_rows - consumed) { + add_repair(doc, .Xref_Entry_Mismatch, off) + count = i64(n_rows - consumed) + } + if start + count > MAX_OBJECT_NUMBER + 1 { + count = MAX_OBJECT_NUMBER + 1 - start + } + + for k in 0 ..< count { + base := (consumed + int(k)) * row + f0 := w0 == 0 ? u64(1) : xref_read_field(data, base + off0, w0) + f1 := xref_read_field(data, base + off1, w1) + f2 := xref_read_field(data, base + off2, w2) + xref_stream_entry(ld, u32(start + k), f0, f1, f2, over_free) + } + consumed += int(count) + if consumed >= n_rows { break } + } + return d, true +} + +@(private) +xref_stream_entry :: proc(ld: ^Xref_Loader, num: u32, f0, f1, f2: u64, over_free: bool) { + doc := ld.doc + switch f0 { + case 0: + xref_set(ld, num, Xref_Entry{kind = .Free, gen = u16(min(f2, 65535))}, over_free) + case 1: + if f1 == 0 || f1 >= u64(len(doc.data)) { return } + xref_set(ld, num, Xref_Entry{ + kind = .Normal, + offset = xref_normalize_offset(doc, i64(f1), i64(num)), + gen = u16(min(f2, 65535)), + }, over_free) + case 2: + if f1 == 0 || f1 > MAX_OBJECT_NUMBER { return } + if f2 > u64(max(u32)) { return } + xref_set(ld, num, Xref_Entry{ + kind = .Compressed, + container = u32(f1), + index = u32(f2), + }, over_free) + case: + // "Any other value shall be interpreted as a reference to the null + // object" (7.5.8.3). It still shadows older sections. + xref_set(ld, num, Xref_Entry{kind = .Free}, over_free) + } +} + +@(private) +xref_read_field :: proc(data: []byte, pos, width: int) -> u64 { + v := u64(0) + for i in 0 ..< width { + j := pos + i + if j < 0 || j >= len(data) { break } + v = v << 8 | u64(data[j]) + } + return v +} + +// --------------------------------------------------------------------------- +// Trailers +// --------------------------------------------------------------------------- + +// xref_merge_trailers folds the chain into one dictionary. +// +// The list runs newest first and is walked backwards, so a key present in +// several trailers takes its newest value while a key that only an older +// trailer had -- an /Info or an /Encrypt that a later incremental update did +// not repeat -- survives. +@(private) +xref_merge_trailers :: proc(ld: ^Xref_Loader) -> (merged: ^Dict, size: i64) { + doc := ld.doc + merged = new_dict(doc.allocator, 16) + for i := len(ld.trailers) - 1; i >= 0; i -= 1 { + t := ld.trailers[i] + if t == nil { continue } + for k, v in t^ { + // Both are properties of one section, not of the document, and + // keeping them in the merged trailer would invite a second walk. + if k == "Prev" || k == "XRefStm" { continue } + merged[k] = v + } + if v, ok := to_int(dict_get_raw(t, "Size")); ok && v > size { + size = v + } + } + if size > 0 { + merged["Size"] = size + } + return +} + +@(private) +xref_finish :: proc(ld: ^Xref_Loader) -> bool { + doc := ld.doc + if len(ld.trailers) == 0 { return false } + + merged, size := xref_merge_trailers(ld) + doc.trailer = merged + + if size > 0 { + xref_grow(ld, int(min(size, i64(ld.max_objects)))) + } + if ld.n_live == 0 { return false } + + if dict_has(merged, "Encrypt") { + // The catalog of an encrypted document may sit inside an object stream + // that cannot be decoded until crypt_init has run, so there is nothing + // to check yet beyond /Root being present at all. + return dict_get_raw(merged, "Root") != nil + } + root, ok := to_dict(dict_get(doc, merged, "Root")) + return ok && root != nil +} + +// --------------------------------------------------------------------------- +// Rebuilding by scanning +// --------------------------------------------------------------------------- + +// Kw_Cursor remembers where a keyword was last found so the scan below stays +// linear: every search resumes from the previous hit instead of from the +// caller's position. +@(private) +Kw_Cursor :: struct { + kw: string, + at: int, + done: bool, +} + +@(private) +kw_cursor :: proc(kw: string) -> Kw_Cursor { + return Kw_Cursor{kw = kw, at = -1} +} + +@(private) +kw_at_or_after :: proc(c: ^Kw_Cursor, data: []byte, from: int) -> int { + if c.done { return -1 } + if c.at >= from { return c.at } + c.at = xref_find_token(data, from, c.kw) + if c.at < 0 { c.done = true } + return c.at +} + +// xref_find_token finds `kw` as a whole token: "obj" must not match inside +// "endobj", and "stream" must not match inside "endstream". +@(private) +xref_find_token :: proc(data: []byte, from: int, kw: string) -> int { + if from < 0 || len(kw) == 0 { return -1 } + needle := transmute([]byte)kw + i := max(from, 0) + for i < len(data) { + idx := index_bytes(data[i:], needle) + if idx < 0 { return -1 } + p := i + idx + after := p + len(kw) + before_ok := p == 0 || !is_pdf_regular(data[p - 1]) + after_ok := after >= len(data) || !is_pdf_regular(data[after]) + if before_ok && after_ok { return p } + i = p + 1 + } + return -1 +} + +// xref_header_before reads the "N G" that precedes the `obj` keyword at `at`. +@(private) +xref_header_before :: proc(data: []byte, at: int) -> (num: u32, gen: u16, start: int, ok: bool) { + k := at - 1 + for k >= 0 && is_pdf_whitespace(data[k]) { k -= 1 } + gen_end := k + 1 + for k >= 0 && data[k] >= '0' && data[k] <= '9' { k -= 1 } + gen_start := k + 1 + if gen_start == gen_end { return } + + if k < 0 || !is_pdf_whitespace(data[k]) { return } + for k >= 0 && is_pdf_whitespace(data[k]) { k -= 1 } + num_end := k + 1 + for k >= 0 && data[k] >= '0' && data[k] <= '9' { k -= 1 } + num_start := k + 1 + if num_start == num_end { return } + if k >= 0 && is_pdf_regular(data[k]) { return } + + n, nok := xref_digits(data[num_start:num_end]) + g, gok := xref_digits(data[gen_start:gen_end]) + if !nok || !gok { return } + if n < 1 || n > MAX_OBJECT_NUMBER || g > 65535 { return } + return u32(n), u16(g), num_start, true +} + +@(private) +xref_digits :: proc(s: []byte) -> (value: i64, ok: bool) { + // A run this long is not an object number whatever it is. + if len(s) == 0 || len(s) > 12 { return 0, false } + for c in s { + if c < '0' || c > '9' { return 0, false } + value = value * 10 + i64(c - '0') + } + return value, true +} + +// xref_skip_object_body returns where scanning should resume after an object +// header, so that "N G obj" sequences inside a stream's binary payload are not +// mistaken for object headers of their own. +@(private) +xref_skip_object_body :: proc(data: []byte, from: int, c_stream, c_endobj, c_endstream: ^Kw_Cursor) -> int { + s := kw_at_or_after(c_stream, data, from) + e := kw_at_or_after(c_endobj, data, from) + if s >= 0 && (e < 0 || s < e) { + if es := kw_at_or_after(c_endstream, data, s + len("stream")); es >= 0 { + return es + len("endstream") + } + // Truncated inside a stream: nothing after this is readable. + return len(data) + } + if e >= 0 { return e + len("endobj") } + return from +} + +// xref_scan_objects walks the whole file recording every object header and +// every trailer dictionary it meets, in file order, so that a later definition +// of an object number replaces an earlier one. +@(private) +xref_scan_objects :: proc(ld: ^Xref_Loader) { + doc := ld.doc + data := doc.data + + c_obj := kw_cursor("obj") + c_trailer := kw_cursor("trailer") + c_stream := kw_cursor("stream") + c_endobj := kw_cursor("endobj") + c_endstream := kw_cursor("endstream") + + pos := 0 + fuel := len(data) + 16 + for fuel > 0 { + fuel -= 1 + o := kw_at_or_after(&c_obj, data, pos) + t := kw_at_or_after(&c_trailer, data, pos) + if o < 0 && t < 0 { break } + + if t >= 0 && (o < 0 || t < o) { + p := parser_make(doc, data, doc.allocator) + p.lex.pos = t + len("trailer") + if obj, err := parse_object(&p); err == .None { + if d, dok := to_dict(obj); dok && d != nil { + // At the cap the oldest goes, not the newest: the trailers + // worth keeping are the ones nearest the end of the file. + if len(ld.trailers) >= MAX_REBUILD_TRAILERS { + ordered_remove(&ld.trailers, 0) + } + append(&ld.trailers, d) + } + } + pos = t + len("trailer") + continue + } + + num, gen, start, hok := xref_header_before(data, o) + if hok { + xref_put(ld, num, Xref_Entry{kind = .Normal, offset = i64(start), gen = gen}) + } + pos = xref_skip_object_body(data, o + len("obj"), &c_stream, &c_endobj, &c_endstream) + if pos <= o { + pos = o + len("obj") + } + // The /Type test happens here rather than in a second pass because + // this is the only place the object's extent is known: bounding the + // window by it keeps the whole scan linear in the file's length + // instead of quadratic in its object count. + if hok { + hi := min(start + REBUILD_PEEK_BYTES, max(pos, start)) + if xref_head_is_interesting(data[start:min(hi, len(data))]) { + append(&ld.candidates, num) + } + } + } + + // The scan found them oldest first; everything downstream expects newest + // first. + for i, j := 0, len(ld.trailers) - 1; i < j; i, j = i + 1, j - 1 { + ld.trailers[i], ld.trailers[j] = ld.trailers[j], ld.trailers[i] + } +} + +// xref_head_is_interesting reports whether the head of an object mentions one +// of the three /Type values a rebuild has to act on. One pass with a switch on +// the leading byte, because this runs over every object in a damaged file and +// three separate substring searches over the same window do not. +@(private) +xref_head_is_interesting :: proc(head: []byte) -> bool { + for i in 0 ..< len(head) { + switch head[i] { + case 'O': if xref_starts_with(head[i:], "ObjStm") { return true } + case 'C': if xref_starts_with(head[i:], "Catalog") { return true } + case 'X': if xref_starts_with(head[i:], "XRef") { return true } + } + } + return false +} + +@(private) +xref_starts_with :: proc(data: []byte, prefix: string) -> bool { + if len(data) < len(prefix) { return false } + return string(data[:len(prefix)]) == prefix +} + +// xref_scan_objstms registers the contents of every object stream the scan +// found, and picks up the /Type /XRef dictionaries and the catalog on the way. +// +// Without this pass a file whose cross-reference stream is damaged loses every +// object that lives in an object stream -- which, in a modern file, is most of +// them. +@(private) +xref_scan_objstms :: proc(ld: ^Xref_Loader, catalog: ^Ref) { + doc := ld.doc + + // Parsing every object in the file to read its /Type would cost the arena + // a second copy of the document; the scan already narrowed it down to the + // objects whose bytes name one of the three types that matter here. + objstms := make([dynamic]u32, 0, 8, doc.allocator) + for num in ld.candidates { + e := xref_entry_at(ld, num) + if e.kind != .Normal { continue } + obj, got, err := parse_indirect_object_at(doc, e.offset, Ref{num, e.gen}) + if err != .None || got.num != num { continue } + d, dok := to_dict(obj) + if !dok || d == nil { continue } + + switch dict_name(doc, d, "Type") { + case "ObjStm": + append(&objstms, num) + case "XRef": + if len(ld.trailers) < MAX_REBUILD_TRAILERS { + // Lowest priority: a real `trailer` keyword, if the file has + // one, is more likely to be the newest word on the subject. + append(&ld.trailers, d) + } + case "Catalog": + catalog^ = Ref{num, e.gen} + } + } + + // Object streams come last, because whether it is safe to read them at all + // depends on what the trailers just collected say. In an encrypted + // document the file key does not exist yet -- `crypt_init` runs after this + // -- so every object stream would decode to ciphertext, and a pair table + // read out of ciphertext invents object numbers that would then shadow the + // real, recoverable entries the scan just found. Losing the compressed + // objects is bad; overwriting the ones that survived is worse. + if xref_trailers_encrypted(ld) { + add_repair(doc, .Missing_Object, 0) + return + } + for num in objstms { + e := xref_entry_at(ld, num) + if e.kind != .Normal { continue } + xref_register_objstm(ld, num, e.offset) + } +} + +@(private) +xref_trailers_encrypted :: proc(ld: ^Xref_Loader) -> bool { + for t in ld.trailers { + if dict_has(t, "Encrypt") { return true } + } + return false +} + +@(private) +xref_register_objstm :: proc(ld: ^Xref_Loader, container: u32, container_offset: i64) { + doc := ld.doc + stm, err := objstm_load(doc, container) + if err != .None || stm == nil { return } + // The dictionary survives whatever went wrong with the payload, so /N + // disagreeing with the pair table means the bytes were not the pair table. + // In the normal path a short table is worth keeping; here, where the + // entries it produces overwrite ones a scan established, it is not. + if stm.declared_n > 0 && len(stm.nums) != stm.declared_n { return } + + for n, i in stm.nums { + if n == 0 || n == container { continue } + cur := xref_entry_at(ld, n) + // A top-level definition that sits later in the file than the object + // stream is the newer of the two and keeps its place. + if cur.kind == .Normal && cur.offset > container_offset { continue } + xref_put(ld, n, Xref_Entry{kind = .Compressed, container = container, index = u32(i)}) + } +} + +@(private) +xref_rebuild_trailer :: proc(ld: ^Xref_Loader, catalog: Ref) { + doc := ld.doc + merged, _ := xref_merge_trailers(ld) + doc.trailer = merged + merged["Size"] = i64(len(doc.xref)) + + if root, ok := to_dict(dict_get(doc, merged, "Root")); ok && root != nil { + if dict_type(doc, root) == "Catalog" || dict_has(root, "Pages") { + return + } + } + if catalog.num != 0 { + merged["Root"] = catalog + add_repair(doc, .Missing_Root, 0, catalog) + } +} diff --git a/xref_test.odin b/xref_test.odin new file mode 100644 index 0000000..ffed8b7 --- /dev/null +++ b/xref_test.odin @@ -0,0 +1,1264 @@ +package pdf + +import "core:testing" + +// --------------------------------------------------------------------------- +// Sample-PDF builders +// +// Every structural test in the library needs a file whose byte offsets are +// right, and hand-counting them is how a test ends up asserting the wrong +// thing. These assemble a document and record the offsets as they go, so a +// test says what shape it wants and never a number. +// +// They are `@(private)` rather than file-private on purpose: the page tree, +// annotation and encryption layers all want the same three-object skeleton. +// --------------------------------------------------------------------------- + +@(private) +Pdf_Builder :: struct { + buf: [dynamic]byte, + // Byte offset of each object header, indexed by object number; -1 for an + // object number that was never written. + offs: [dynamic]int, +} + +@(private) +pb_make :: proc(header := "%PDF-1.7\n") -> Pdf_Builder { + pb: Pdf_Builder + pb.buf = make([dynamic]byte) + pb.offs = make([dynamic]int) + pb_str(&pb, header) + return pb +} + +@(private) +pb_destroy :: proc(pb: ^Pdf_Builder) { + delete(pb.buf) + delete(pb.offs) +} + +@(private) +pb_str :: proc(pb: ^Pdf_Builder, s: string) { + append(&pb.buf, ..transmute([]byte)s) +} + +@(private) +pb_raw :: proc(pb: ^Pdf_Builder, s: []byte) { + if len(s) > 0 { + append(&pb.buf, ..s) + } +} + +@(private) +pb_num :: proc(pb: ^Pdf_Builder, v: int) { + digits: [24]byte + n := 0 + x := v + if x < 0 { + append(&pb.buf, '-') + x = -x + } + if x == 0 { + digits[0] = '0' + n = 1 + } + for x > 0 { + digits[n] = byte('0' + x % 10) + x /= 10 + n += 1 + } + for i := n - 1; i >= 0; i -= 1 { + append(&pb.buf, digits[i]) + } +} + +@(private) +pb_pad :: proc(pb: ^Pdf_Builder, v, width: int) { + digits: [24]byte + n := 0 + x := max(v, 0) + if x == 0 { + digits[0] = '0' + n = 1 + } + for x > 0 { + digits[n] = byte('0' + x % 10) + x /= 10 + n += 1 + } + for _ in n ..< width { + append(&pb.buf, '0') + } + for i := n - 1; i >= 0; i -= 1 { + append(&pb.buf, digits[i]) + } +} + +@(private) +pb_here :: proc(pb: ^Pdf_Builder) -> int { + return len(pb.buf) +} + +@(private) +pb_mark :: proc(pb: ^Pdf_Builder, num: u32) { + for len(pb.offs) <= int(num) { + append(&pb.offs, -1) + } + pb.offs[num] = len(pb.buf) +} + +// pb_obj writes "N 0 obj endobj". +@(private) +pb_obj :: proc(pb: ^Pdf_Builder, num: u32, body: string) { + pb_mark(pb, num) + pb_num(pb, int(num)) + pb_str(pb, " 0 obj\n") + pb_str(pb, body) + pb_str(pb, "\nendobj\n") +} + +// pb_stream_obj writes a stream object, appending the /Length the payload +// actually has. `dict_head` is the dictionary without its << >>. +@(private) +pb_stream_obj :: proc(pb: ^Pdf_Builder, num: u32, dict_head: string, payload: []byte) { + pb_mark(pb, num) + pb_num(pb, int(num)) + pb_str(pb, " 0 obj\n<< ") + pb_str(pb, dict_head) + pb_str(pb, " /Length ") + pb_num(pb, len(payload)) + pb_str(pb, " >>\nstream\n") + pb_raw(pb, payload) + pb_str(pb, "\nendstream\nendobj\n") +} + +@(private) +pb_xref_begin :: proc(pb: ^Pdf_Builder) -> int { + at := len(pb.buf) + pb_str(pb, "xref\n") + return at +} + +@(private) +pb_xref_sub :: proc(pb: ^Pdf_Builder, start, count: int) { + pb_num(pb, start) + pb_str(pb, " ") + pb_num(pb, count) + pb_str(pb, "\n") +} + +// pb_entry writes one table entry. The default `eol` gives the 20-byte form +// the spec requires; passing "\n" gives the 19-byte form most producers write. +@(private) +pb_entry :: proc(pb: ^Pdf_Builder, offset, gen: int, used: bool, eol := " \n") { + pb_pad(pb, offset, 10) + pb_str(pb, " ") + pb_pad(pb, gen, 5) + pb_str(pb, used ? " n" : " f") + pb_str(pb, eol) +} + +// pb_classic_xref_ex writes one subsection covering objects `real_start` +// upwards, but declaring it starts at `declared_start`. The two differ only in +// the tests for the off-by-one repair. +@(private) +pb_classic_xref_ex :: proc(pb: ^Pdf_Builder, declared_start, real_start: int, eol := " \n") -> int { + at := pb_xref_begin(pb) + n := len(pb.offs) + pb_xref_sub(pb, declared_start, n - real_start) + for i in real_start ..< n { + if i == 0 || pb.offs[i] < 0 { + pb_entry(pb, 0, 65535, false, eol) + } else { + pb_entry(pb, pb.offs[i], 0, true, eol) + } + } + return at +} + +@(private) +pb_classic_xref :: proc(pb: ^Pdf_Builder) -> int { + return pb_classic_xref_ex(pb, 0, 0) +} + +@(private) +pb_trailer :: proc(pb: ^Pdf_Builder, body: string, xref_at: int) { + pb_str(pb, "trailer\n") + pb_str(pb, body) + pb_str(pb, "\n") + pb_startxref(pb, xref_at) +} + +@(private) +pb_startxref :: proc(pb: ^Pdf_Builder, at: int) { + pb_str(pb, "startxref\n") + pb_num(pb, at) + pb_str(pb, "\n%%EOF\n") +} + +// pb_placeholder reserves ten digits that pb_patch fills in later, for the +// forward references a /Prev loop needs. +@(private) +pb_placeholder :: proc(pb: ^Pdf_Builder) -> int { + at := len(pb.buf) + pb_str(pb, "0000000000") + return at +} + +@(private) +pb_patch :: proc(pb: ^Pdf_Builder, at, value: int) { + x := max(value, 0) + for i := 9; i >= 0; i -= 1 { + if at + i < len(pb.buf) { + pb.buf[at + i] = byte('0' + x % 10) + } + x /= 10 + } +} + +// pb_skeleton writes the three objects every document needs: a catalog, a page +// tree root and one page. +@(private) +pb_skeleton :: proc(pb: ^Pdf_Builder) { + pb_obj(pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + pb_obj(pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") +} + +// pb_sample_classic is the smallest well-formed file: three objects and a +// classic single-section table. +@(private) +pb_sample_classic :: proc() -> Pdf_Builder { + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", at) + return pb +} + +// --------------------------------------------------------------------------- +// Cross-reference stream payloads +// --------------------------------------------------------------------------- + +@(private) +Xref_Row :: struct { + f0, f1, f2: u64, +} + +// xref_rows_encode lays out rows in the /W field widths given. +@(private) +xref_rows_encode :: proc(rows: []Xref_Row, w0, w1, w2: int, allocator := context.allocator) -> []byte { + row := w0 + w1 + w2 + out := make([]byte, len(rows) * row, allocator) + for r, i in rows { + base := i * row + xref_put_be(out[base:base + w0], r.f0) + xref_put_be(out[base + w0:base + w0 + w1], r.f1) + xref_put_be(out[base + w0 + w1:base + row], r.f2) + } + return out +} + +@(private) +xref_put_be :: proc(dst: []byte, v: u64) { + x := v + for i := len(dst) - 1; i >= 0; i -= 1 { + dst[i] = byte(x & 0xff) + x >>= 8 + } +} + +// png_up_encode applies the PNG "Up" filter, which is what a producer writing +// /Predictor 12 does. Every row gains a leading tag byte. +@(private) +png_up_encode :: proc(src: []byte, row_len: int, allocator := context.allocator) -> []byte { + if row_len <= 0 { return nil } + rows := len(src) / row_len + out := make([]byte, rows * (row_len + 1), allocator) + prev := make([]byte, row_len, allocator) + defer delete(prev, allocator) + for r in 0 ..< rows { + o := r * (row_len + 1) + out[o] = 2 + for c in 0 ..< row_len { + out[o + 1 + c] = byte((int(src[r * row_len + c]) - int(prev[c])) & 0xff) + } + copy(prev, src[r * row_len:(r + 1) * row_len]) + } + return out +} + +// zlib_stored wraps bytes in an RFC 1950 stream of stored (uncompressed) +// DEFLATE blocks. A test does not need compression, only a payload the +// library's inflate must accept. +@(private) +zlib_stored :: proc(src: []byte, allocator := context.allocator) -> []byte { + n_blocks := max(1, (len(src) + 65534) / 65535) + out := make([dynamic]byte, 0, len(src) + n_blocks * 5 + 6, allocator) + append(&out, 0x78, 0x01) + i := 0 + for b in 0 ..< n_blocks { + n := min(65535, len(src) - i) + append(&out, b == n_blocks - 1 ? 0x01 : 0x00) + append(&out, byte(n & 0xff), byte((n >> 8) & 0xff)) + m := 0xffff - n + append(&out, byte(m & 0xff), byte((m >> 8) & 0xff)) + if n > 0 { + append(&out, ..src[i:i + n]) + } + i += n + } + s1, s2 := u32(1), u32(0) + for c in src { + s1 = (s1 + u32(c)) % 65521 + s2 = (s2 + s1) % 65521 + } + adler := s2 << 16 | s1 + append(&out, byte(adler >> 24), byte(adler >> 16), byte(adler >> 8), byte(adler)) + return out[:] +} + +// objstm_payload builds the body of a /Type /ObjStm: the pair table followed +// by the objects. `first` is the /First the dictionary must declare. +@(private) +objstm_payload :: proc(nums: []u32, bodies: []string, allocator := context.allocator) -> + (payload: []byte, first: int) { + pb := Pdf_Builder{buf = make([dynamic]byte, allocator), offs = make([dynamic]int, allocator)} + defer delete(pb.offs) + + rel := make([]int, len(bodies), allocator) + defer delete(rel, allocator) + total := 0 + for body, i in bodies { + rel[i] = total + total += len(body) + 1 + } + for num, i in nums { + if i >= len(rel) { break } + pb_num(&pb, int(num)) + pb_str(&pb, " ") + pb_num(&pb, rel[i]) + pb_str(&pb, " ") + } + first = len(pb.buf) + for body in bodies { + pb_str(&pb, body) + pb_str(&pb, " ") + } + return pb.buf[:], first +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +@(private) +pb_open :: proc(pb: ^Pdf_Builder) -> (^Document, Error) { + // The page tree is a layer above this one; xref tests must not depend on + // it being able to walk what they built. + return open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) +} + +@(private) +doc_has_repair :: proc(doc: ^Document, kind: Repair_Kind) -> bool { + if doc == nil { return false } + for r in doc.repairs { + if r.kind == kind { return true } + } + return false +} + +@(private = "file") +expect_page :: proc(t: ^testing.T, doc: ^Document, num: u32, width: f64) { + d := get_dict(doc, Ref{num, 0}) + if !testing.expect(t, d != nil, "page object did not resolve to a dictionary") { + return + } + testing.expect(t, dict_name(doc, d, "Type") == "Page", "object is not a /Page") + r := dict_rect(doc, d, "MediaBox") + testing.expectf(t, r.x1 == width, "MediaBox width %v, wanted %v", r.x1, width) +} + +// --------------------------------------------------------------------------- +// Classic tables +// --------------------------------------------------------------------------- + +@(test) +test_xref_classic_single_section :: proc(t: ^testing.T) { + pb := pb_sample_classic() + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + testing.expect(t, doc != nil, "no document") + if doc == nil { return } + + testing.expect(t, !doc.is_damaged, "a well-formed file recorded a repair") + testing.expectf(t, object_count(doc) == 4, "object_count %v, wanted 4", object_count(doc)) + testing.expect(t, doc.catalog != nil, "no catalog") + testing.expect(t, doc.xref[1].kind == .Normal, "object 1 is not a normal entry") + testing.expect(t, doc.xref[0].kind == .Free, "object 0 must be free") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_classic_19_byte_entries :: proc(t: ^testing.T) { + // The spec fixes the entry at 20 bytes; a large fraction of producers + // write 19 by using a bare LF. Both must read the same. + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref_ex(&pb, 0, 0, "\n") + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, !doc.is_damaged, "19-byte entries should not be a repair") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_incremental_update :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + x1 := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", x1) + + // Generation two: object 3 is rewritten with a different page size, and + // object 4 appears for the first time. + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] >>") + pb_obj(&pb, 4, "<< /Type /ExtGState /LW 3 >>") + x2 := pb_xref_begin(&pb) + pb_xref_sub(&pb, 3, 2) + pb_entry(&pb, pb.offs[3], 0, true) + pb_entry(&pb, pb.offs[4], 0, true) + pb_str(&pb, "trailer\n<< /Size 5 /Root 1 0 R /Prev ") + pb_num(&pb, x1) + pb_str(&pb, " >>\n") + pb_startxref(&pb, x2) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + // The newest definition of object 3 wins ... + expect_page(t, doc, 3, 400) + // ... an object only the newest section has is there ... + testing.expect(t, dict_name(doc, get_dict(doc, Ref{4, 0}), "Type") == "ExtGState", + "object added by the update is missing") + // ... and objects only the first generation defined are still reachable. + testing.expect(t, doc.catalog != nil, "catalog from the older section was lost") + testing.expect(t, dict_name(doc, get_dict(doc, Ref{2, 0}), "Type") == "Pages", + "object from the older section was lost") +} + +@(test) +test_xref_trailer_merge_keeps_older_keys :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + pb_obj(&pb, 4, "<< /Producer (first) >>") + x1 := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 5 /Root 1 0 R /Info 4 0 R /ID [<01> <02>] >>", x1) + + pb_obj(&pb, 5, "<< /Type /ExtGState >>") + x2 := pb_xref_begin(&pb) + pb_xref_sub(&pb, 5, 1) + pb_entry(&pb, pb.offs[5], 0, true) + // The newest trailer omits /Info entirely, which is legal and common. + pb_str(&pb, "trailer\n<< /Size 6 /Root 1 0 R /ID [<03> <04>] /Prev ") + pb_num(&pb, x1) + pb_str(&pb, " >>\n") + pb_startxref(&pb, x2) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + info := dict_dict(doc, doc.trailer, "Info") + testing.expect(t, info != nil, "/Info from the older trailer was dropped") + + id := dict_array(doc, doc.trailer, "ID") + first, ok := to_bytes(array_get(doc, id, 0)) + testing.expect(t, ok && len(first) == 1 && first[0] == 0x03, + "the newest /ID did not win the merge") + testing.expectf(t, dict_int(doc, doc.trailer, "Size", 0) == 6, + "merged /Size %v, wanted 6", dict_int(doc, doc.trailer, "Size", 0)) +} + +@(test) +test_xref_free_entry_shadows_older :: proc(t: ^testing.T) { + // An incremental update that deletes an object marks it free. The older + // section still has an in-use entry, and it must not win. + pb := pb_make() + pb_skeleton(&pb) + pb_obj(&pb, 4, "<< /Type /ExtGState >>") + x1 := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 5 /Root 1 0 R >>", x1) + + x2 := pb_xref_begin(&pb) + pb_xref_sub(&pb, 4, 1) + pb_entry(&pb, 0, 1, false) + pb_str(&pb, "trailer\n<< /Size 5 /Root 1 0 R /Prev ") + pb_num(&pb, x1) + pb_str(&pb, " >>\n") + pb_startxref(&pb, x2) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc.xref[4].kind == .Free, "deleted object was resurrected") + testing.expect(t, get_object(doc, Ref{4, 0}) == nil, "deleted object still resolves") +} + +// --------------------------------------------------------------------------- +// The off-by-one subsection header +// --------------------------------------------------------------------------- + +@(test) +test_xref_subsection_declared_one_starts_at_zero :: proc(t: ^testing.T) { + // "1 4" over entries that really begin with the free-list head. Trusting + // the header shifts every object number by one. + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref_ex(&pb, 1, 0) + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc.catalog != nil, "catalog not found after the shift was repaired") + testing.expect(t, dict_name(doc, get_dict(doc, Ref{1, 0}), "Type") == "Catalog", + "object 1 is not the catalog") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_subsection_declared_zero_starts_at_one :: proc(t: ^testing.T) { + // The mirror image: "0 3" over entries that begin at object 1. Object 0 + // can never be in use, which is what gives it away. + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref_ex(&pb, 0, 1) + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, dict_name(doc, get_dict(doc, Ref{1, 0}), "Type") == "Catalog", + "object 1 is not the catalog") + expect_page(t, doc, 3, 200) +} + +// --------------------------------------------------------------------------- +// Cross-reference streams +// --------------------------------------------------------------------------- + +@(private = "file") +build_xref_stream_file :: proc(predictor: bool) -> Pdf_Builder { + pb := pb_make("%PDF-1.5\n") + pb_skeleton(&pb) + + at := pb_here(&pb) + rows := [5]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(pb.offs[2]), 0}, + {1, u64(pb.offs[3]), 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + + if predictor { + pred := png_up_encode(raw, 7) + defer delete(pred) + payload := zlib_stored(pred) + defer delete(payload) + pb_stream_obj(&pb, 4, + "/Type /XRef /Size 5 /W [1 4 2] /Root 1 0 R /Filter /FlateDecode " + + "/DecodeParms << /Predictor 12 /Columns 7 >>", payload) + } else { + pb_stream_obj(&pb, 4, "/Type /XRef /Size 5 /W [1 4 2] /Root 1 0 R", raw) + } + pb_startxref(&pb, at) + return pb +} + +@(test) +test_xref_stream_plain :: proc(t: ^testing.T) { + pb := build_xref_stream_file(false) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, !doc.is_damaged, "a well-formed xref stream recorded a repair") + testing.expect(t, doc.catalog != nil, "no catalog") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_stream_png_predictor :: proc(t: ^testing.T) { + pb := build_xref_stream_file(true) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, !doc.is_damaged, "a well-formed xref stream recorded a repair") + expect_page(t, doc, 3, 200) + testing.expect(t, doc.xref[4].kind == .Normal, "the xref stream's own entry is missing") +} + +@(test) +test_xref_stream_zero_width_type_field :: proc(t: ^testing.T) { + // "/W [0 2 1]": the type field is not stored and defaults to 1, which is + // how a file with no free and no compressed objects saves a byte a row. + pb := pb_make("%PDF-1.5\n") + pb_skeleton(&pb) + + at := pb_here(&pb) + rows := [5]Xref_Row{ + {0, 0, 0}, + {0, u64(pb.offs[1]), 0}, + {0, u64(pb.offs[2]), 0}, + {0, u64(pb.offs[3]), 0}, + {0, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 0, 2, 1) + defer delete(raw) + pb_stream_obj(&pb, 4, "/Type /XRef /Size 5 /W [0 2 1] /Root 1 0 R /Index [1 4]", raw[3:]) + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, doc.xref[1].kind == .Normal, "type defaulted wrong") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_stream_index_clamped_to_data :: proc(t: ^testing.T) { + // /Index promises far more entries than the stream holds. The extra rows + // must be dropped, not read off the end of the buffer. + pb := pb_make("%PDF-1.5\n") + pb_skeleton(&pb) + + at := pb_here(&pb) + rows := [5]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(pb.offs[2]), 0}, + {1, u64(pb.offs[3]), 0}, + {1, u64(at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 4, "/Type /XRef /Size 5 /W [1 4 2] /Root 1 0 R /Index [0 900000]", raw) + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_page(t, doc, 3, 200) + testing.expect(t, object_count(doc) < 900000, "the table grew to a fabricated /Index count") +} + +@(test) +test_xref_absurd_size_is_clamped :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 2000000000 /Root 1 0 R >>", at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + // The floor in xref_max_objects is 1024 entries, so a tiny file with a + // preposterous /Size costs 24 KiB rather than 48 GB. + testing.expectf(t, object_count(doc) <= 1024, + "table grew to %v for a %v byte file", object_count(doc), len(pb.buf)) + expect_page(t, doc, 3, 200) +} + +// --------------------------------------------------------------------------- +// Hybrid-reference files +// --------------------------------------------------------------------------- + +@(test) +test_xref_hybrid_xrefstm :: proc(t: ^testing.T) { + // The classic table hides object 3 by marking it free; the stream named by + // /XRefStm says it lives inside object stream 4. A reader that lets the + // classic entry win loses the page. + pb := pb_make("%PDF-1.5\n") + pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") + pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") + + bodies := [1]string{"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>"} + nums := [1]u32{3} + payload, first := objstm_payload(nums[:], bodies[:]) + defer delete(payload) + pb_mark(&pb, 3) // object 3 has no header of its own + pb.offs[3] = -1 + pb_mark(&pb, 4) + pb_str(&pb, "4 0 obj\n<< /Type /ObjStm /N 1 /First ") + pb_num(&pb, first) + pb_str(&pb, " /Length ") + pb_num(&pb, len(payload)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, payload) + pb_str(&pb, "\nendstream\nendobj\n") + + stm_at := pb_here(&pb) + rows := [6]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(pb.offs[2]), 0}, + {2, 4, 0}, + {1, u64(pb.offs[4]), 0}, + {1, u64(stm_at), 0}, + } + raw := xref_rows_encode(rows[:], 1, 4, 2) + defer delete(raw) + pb_stream_obj(&pb, 5, "/Type /XRef /Size 6 /W [1 4 2] /Root 1 0 R", raw) + + at := pb_xref_begin(&pb) + pb_xref_sub(&pb, 0, 6) + pb_entry(&pb, 0, 65535, false) + pb_entry(&pb, pb.offs[1], 0, true) + pb_entry(&pb, pb.offs[2], 0, true) + pb_entry(&pb, 0, 0, false) // object 3: hidden from old readers + pb_entry(&pb, pb.offs[4], 0, true) + pb_entry(&pb, pb.offs[5], 0, true) + pb_str(&pb, "trailer\n<< /Size 6 /Root 1 0 R /XRefStm ") + pb_num(&pb, stm_at) + pb_str(&pb, " >>\n") + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc.xref[3].kind == .Compressed, + "the /XRefStm entry did not replace the classic free entry") + expect_page(t, doc, 3, 300) +} + +// --------------------------------------------------------------------------- +// Loops and bad pointers +// --------------------------------------------------------------------------- + +@(test) +test_xref_prev_loop_terminates :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + + xa := pb_xref_begin(&pb) + pb_xref_sub(&pb, 0, 4) + pb_entry(&pb, 0, 65535, false) + pb_entry(&pb, pb.offs[1], 0, true) + pb_entry(&pb, pb.offs[2], 0, true) + pb_entry(&pb, pb.offs[3], 0, true) + pb_str(&pb, "trailer\n<< /Size 4 /Root 1 0 R /Prev ") + hole := pb_placeholder(&pb) + pb_str(&pb, " >>\n") + + xb := pb_xref_begin(&pb) + pb_xref_sub(&pb, 0, 1) + pb_entry(&pb, 0, 65535, false) + pb_str(&pb, "trailer\n<< /Size 4 /Root 1 0 R /Prev ") + pb_num(&pb, xa) + pb_str(&pb, " >>\n") + pb_startxref(&pb, xb) + pb_patch(&pb, hole, xb) // A points back at B: a two-node loop + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_startxref_past_eof_rebuilds :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref(&pb) + _ = at + pb_trailer(&pb, "<< /Size 4 /Root 1 0 R >>", 99_000_000) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + testing.expect(t, doc.catalog != nil, "rebuild did not find the catalog") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_startxref_into_junk_rebuilds :: proc(t: ^testing.T) { + // startxref points into the middle of a stream's payload, which is what a + // file that was concatenated or partly re-saved looks like. + pb := pb_make() + pb_skeleton(&pb) + junk := make([]byte, 512) + defer delete(junk) + for i in 0 ..< len(junk) { + junk[i] = byte('a' + i % 26) + } + pb_stream_obj(&pb, 4, "/Type /Junk", junk) + at := pb_classic_xref(&pb) + _ = at + pb_trailer(&pb, "<< /Size 5 /Root 1 0 R >>", pb.offs[4] + 200) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_missing_entirely_rebuilds :: proc(t: ^testing.T) { + pb := pb_make() + pb_skeleton(&pb) + pb_str(&pb, "trailer\n<< /Size 4 /Root 1 0 R >>\n%%EOF\n") + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + testing.expect(t, doc.catalog != nil, "rebuild did not find the catalog") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_rebuild_without_any_trailer :: proc(t: ^testing.T) { + // No xref, no trailer: the catalog has to be found by looking for + // /Type /Catalog. + pb := pb_make() + pb_skeleton(&pb) + pb_str(&pb, "%%EOF\n") + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + testing.expect(t, doc.catalog != nil, "catalog was not found by scanning") + testing.expect(t, dict_name(doc, doc.catalog, "Type") == "Catalog", "wrong catalog") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_rebuild_prefers_later_definition :: proc(t: ^testing.T) { + // Two definitions of object 3 and no usable table. The one further down + // the file is the newer one. + pb := pb_make() + pb_skeleton(&pb) + pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] >>") + pb_str(&pb, "trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n7\n%%EOF\n") + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_page(t, doc, 3, 500) +} + +@(test) +test_xref_rebuild_skips_headers_inside_streams :: proc(t: ^testing.T) { + // A stream payload that spells out an object header must not be mistaken + // for one: doing so would point object 3 at the middle of a stream. + pb := pb_make() + pb_skeleton(&pb) + decoy := "3 0 obj << /Type /Page /MediaBox [0 0 999 999] >> endobj" + pb_stream_obj(&pb, 4, "/Type /Junk", transmute([]byte)decoy) + pb_str(&pb, "trailer\n<< /Size 5 /Root 1 0 R >>\n%%EOF\n") + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_page(t, doc, 3, 200) +} + +// --------------------------------------------------------------------------- +// Files with junk before %PDF- +// --------------------------------------------------------------------------- + +@(test) +test_xref_header_offset :: proc(t: ^testing.T) { + // A mail header, an HTTP response or a self-extracting wrapper in front of + // the file. Every offset in it is measured from "%PDF-", not from zero. + inner := pb_sample_classic() + defer pb_destroy(&inner) + + junk := "Received: by 10.0.0.1 with SMTP id x1234567890abcdefghijklmnop;\r\n" + + "Content-Type: application/pdf\r\n\r\n" + + pb := pb_make(junk) + defer pb_destroy(&pb) + pb_raw(&pb, inner.buf[:]) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, doc.header_offset == i64(len(junk)), + "header_offset %v, wanted %v", doc.header_offset, len(junk)) + testing.expect(t, doc.catalog != nil, "no catalog") + expect_page(t, doc, 3, 200) +} + +// --------------------------------------------------------------------------- +// Malformed input must not crash or hang +// --------------------------------------------------------------------------- + +@(test) +test_xref_truncation_survives :: proc(t: ^testing.T) { + pb := build_xref_stream_file(true) + defer pb_destroy(&pb) + + for cut := 1; cut <= len(pb.buf); cut += 7 { + doc, _ := open_bytes(pb.buf[:cut], Open_Options{skip_page_tree = true}) + if doc != nil { + // Touching every object is what a caller would do next. + for num in 1 ..< object_count(doc) { + _ = get_object(doc, Ref{u32(num), 0}) + } + close(doc) + } + } + testing.expect(t, true, "truncation must never crash or hang") +} + +@(test) +test_xref_byte_flips_survive :: proc(t: ^testing.T) { + base := pb_sample_classic() + defer pb_destroy(&base) + + scratch := make([]byte, len(base.buf)) + defer delete(scratch) + + for at := 0; at < len(base.buf); at += 5 { + copy(scratch, base.buf[:]) + scratch[at] ~= 0xff + doc, _ := open_bytes(scratch, Open_Options{skip_page_tree = true}) + if doc != nil { + for num in 1 ..< object_count(doc) { + _ = get_object(doc, Ref{u32(num), 0}) + } + close(doc) + } + } + testing.expect(t, true, "a flipped byte must never crash or hang") +} + +@(test) +test_xref_empty_and_tiny_inputs :: proc(t: ^testing.T) { + cases := [?]string{ + "", + "%", + "%PDF-1.7\n", + "%PDF-1.7\nstartxref\n", + "%PDF-1.7\nstartxref\n0\n%%EOF\n", + "%PDF-1.7\nxref\ntrailer\n", + "%PDF-1.7\n1 0 obj\nendobj\n", + "startxref\n4\n%%EOF", + "xref\n0 4294967295\n", + } + for src in cases { + doc, err := open_bytes(transmute([]byte)src, Open_Options{skip_page_tree = true}) + if doc != nil { + close(doc) + } else { + testing.expect(t, err != .None, "a nil document must come with an error") + } + } +} + +// --------------------------------------------------------------------------- +// Unit tests for the scanning primitives +// --------------------------------------------------------------------------- + +@(test) +test_xref_find_token_is_whole_word :: proc(t: ^testing.T) { + data := transmute([]byte)string("endobj obj objx 1 0 obj") + // "obj" inside "endobj" and inside "objx" must not match. + at := xref_find_token(data, 0, "obj") + testing.expectf(t, at == 7, "found obj at %v, wanted 7", at) + at2 := xref_find_token(data, 8, "obj") + testing.expectf(t, at2 == 20, "found obj at %v, wanted 20", at2) + testing.expect(t, xref_find_token(data, 0, "stream") == -1, "matched a keyword that is absent") + + stream := transmute([]byte)string("endstream stream") + testing.expectf(t, xref_find_token(stream, 0, "stream") == 10, + "\"stream\" matched inside \"endstream\"") +} + +@(test) +test_xref_header_before :: proc(t: ^testing.T) { + Case :: struct { + src: string, + num: u32, + gen: u16, + ok: bool, + } + cases := [?]Case{ + {"12 0 obj", 12, 0, true}, + {" 7 65535 obj", 7, 65535, true}, + {"3\t2\nobj", 3, 2, true}, + {"x12 0 obj", 0, 0, false}, // glued to a regular character + {"0 0 obj", 0, 0, false}, // object 0 cannot be defined + {"obj", 0, 0, false}, + {"12 obj", 0, 0, false}, // handled elsewhere, not here + {"12 0 999999 obj", 0, 0, false}, // generation out of range + } + for c in cases { + data := transmute([]byte)c.src + at := xref_find_token(data, 0, "obj") + if at < 0 { + testing.expect(t, !c.ok, "no obj keyword in a case that expected one") + continue + } + num, gen, _, ok := xref_header_before(data, at) + testing.expectf(t, ok == c.ok, "%q: ok %v, wanted %v", c.src, ok, c.ok) + if ok && c.ok { + testing.expectf(t, num == c.num && gen == c.gen, + "%q: got %v %v, wanted %v %v", c.src, num, gen, c.num, c.gen) + } + } +} + +@(test) +test_xref_read_field_big_endian :: proc(t: ^testing.T) { + data := [?]byte{0x01, 0x02, 0x03, 0x04, 0x05} + testing.expect(t, xref_read_field(data[:], 0, 0) == 0, "a zero-width field is zero") + testing.expect(t, xref_read_field(data[:], 0, 1) == 0x01, "one byte") + testing.expect(t, xref_read_field(data[:], 1, 2) == 0x0203, "two bytes") + testing.expect(t, xref_read_field(data[:], 1, 4) == 0x02030405, "four bytes") + // Reading past the end stops rather than wandering off the buffer. + testing.expect(t, xref_read_field(data[:], 3, 8) == 0x0405, "clamped at the end") +} + +@(test) +test_xref_digits :: proc(t: ^testing.T) { + v, ok := xref_digits(transmute([]byte)string("00123")) + testing.expect(t, ok && v == 123, "leading zeros") + _, long_ok := xref_digits(transmute([]byte)string("1234567890123456")) + testing.expect(t, !long_ok, "an absurdly long run is not an object number") + _, empty_ok := xref_digits(nil) + testing.expect(t, !empty_ok, "an empty run is not a number") +} + +@(test) +test_xref_previous_startxref_rescues :: proc(t: ^testing.T) { + // A tool that crashed part way through an incremental update leaves a + // startxref pointing at nothing, with the previous one still intact. That + // is a far better table than anything a rebuild can reconstruct. + pb := pb_sample_classic() + defer pb_destroy(&pb) + pb_str(&pb, "startxref\n99000000\n%%EOF\n") + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc_has_repair(doc, .Bad_Startxref), "the dead startxref was not noticed") + testing.expect(t, !doc_has_repair(doc, .Rebuilt_Xref), + "the older startxref should have been used instead of a rebuild") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_trailer_keyword_missing :: proc(t: ^testing.T) { + // Some producers write the dictionary straight after the table. + pb := pb_make() + pb_skeleton(&pb) + at := pb_classic_xref(&pb) + pb_str(&pb, "<< /Size 4 /Root 1 0 R >>\n") + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expect(t, doc.catalog != nil, "no catalog") + testing.expect(t, !doc_has_repair(doc, .Rebuilt_Xref), "should not have needed a rebuild") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_subsection_count_absurd :: proc(t: ^testing.T) { + // "0 900000000" over four entries. The count has to be clamped to what the + // file can hold, and the entries after the table must not be eaten. + pb := pb_make() + pb_skeleton(&pb) + at := pb_xref_begin(&pb) + pb_xref_sub(&pb, 0, 900_000_000) + pb_entry(&pb, 0, 65535, false) + pb_entry(&pb, pb.offs[1], 0, true) + pb_entry(&pb, pb.offs[2], 0, true) + pb_entry(&pb, pb.offs[3], 0, true) + pb_str(&pb, "trailer\n<< /Size 4 /Root 1 0 R >>\n") + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + expect_page(t, doc, 3, 200) + testing.expectf(t, object_count(doc) <= 1024, "table grew to %v", object_count(doc)) +} + +@(test) +test_xref_stream_without_w_falls_back :: proc(t: ^testing.T) { + // No /W at all: the rows cannot be decoded, so the whole section is + // unusable and the file has to be rebuilt rather than half-read. + pb := pb_make("%PDF-1.5\n") + pb_skeleton(&pb) + at := pb_here(&pb) + payload := [?]byte{0, 0, 0, 0, 0, 0, 0} + pb_stream_obj(&pb, 4, "/Type /XRef /Size 5 /Root 1 0 R", payload[:]) + pb_startxref(&pb, at) + defer pb_destroy(&pb) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "a rebuild should have opened this file") + if doc == nil { return } + testing.expect(t, doc_has_repair(doc, .Rebuilt_Xref), "no rebuild was recorded") + expect_page(t, doc, 3, 200) +} + +@(test) +test_xref_many_objects :: proc(t: ^testing.T) { + // Enough objects that the table is regrown many times over. + N :: 1500 + pb := pb_make() + defer pb_destroy(&pb) + pb_skeleton(&pb) + for i in 4 ..= u32(N) { + pb_obj(&pb, i, "<< /Type /Junk >>") + } + at := pb_classic_xref(&pb) + pb_trailer(&pb, "<< /Size 1501 /Root 1 0 R >>", at) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + testing.expectf(t, object_count(doc) == N + 1, "object_count %v, wanted %v", + object_count(doc), N + 1) + n_found := 0 + for i in 4 ..= u32(N) { + if dict_name(doc, get_dict(doc, Ref{i, 0}), "Type") == "Junk" { + n_found += 1 + } + } + testing.expectf(t, n_found == N - 3, "resolved %v of %v objects", n_found, N - 3) +} + +@(test) +test_xref_object_read_during_load_is_not_cached :: proc(t: ^testing.T) { + // An older section's cross-reference stream gives its /Length indirectly. + // Resolving that reference while the table is still half built reads the + // object as null -- correctly, there is nothing there yet -- and caching + // that answer would make it null for the life of the Document, long after + // the section that defines it has been read. + pb := pb_make("%PDF-1.5\n") + defer pb_destroy(&pb) + pb_skeleton(&pb) + + // The section below is one row wide, so this is also the honest /Length. + pb_obj(&pb, 7, "7") + + // The older section, covering object 7 alone. + xb := pb_here(&pb) + row_b := [1]Xref_Row{{1, u64(pb.offs[7]), 0}} + raw_b := xref_rows_encode(row_b[:], 1, 4, 2) + defer delete(raw_b) + pb_mark(&pb, 6) + pb_str(&pb, "6 0 obj\n<< /Type /XRef /Size 10 /W [1 4 2] /Index [7 1] /Root 1 0 R /Length 7 0 R >>\nstream\n") + pb_raw(&pb, raw_b) + pb_str(&pb, "\nendstream\nendobj\n") + + pb_obj(&pb, 9, "<< /Type /Junk >>") + + // The newest section covers 0..6 and 8..9, deliberately skipping object 7 + // so that the older section is the only place it is defined. + xa := pb_here(&pb) + rows_a := [9]Xref_Row{ + {0, 0, 65535}, + {1, u64(pb.offs[1]), 0}, + {1, u64(pb.offs[2]), 0}, + {1, u64(pb.offs[3]), 0}, + {0, 0, 0}, + {0, 0, 0}, + {1, u64(xb), 0}, + {1, u64(pb.offs[9]), 0}, + {1, u64(xa), 0}, + } + raw_a := xref_rows_encode(rows_a[:], 1, 4, 2) + defer delete(raw_a) + pb_mark(&pb, 8) + pb_str(&pb, "8 0 obj\n<< /Type /XRef /Size 10 /W [1 4 2] /Index [0 7 8 2] /Root 1 0 R /Prev ") + pb_num(&pb, xb) + pb_str(&pb, " /Length ") + pb_num(&pb, len(raw_a)) + pb_str(&pb, " >>\nstream\n") + pb_raw(&pb, raw_a) + pb_str(&pb, "\nendstream\nendobj\n") + pb_startxref(&pb, xa) + + doc, err := pb_open(&pb) + defer close(doc) + testing.expect(t, err == .None, "open failed") + if doc == nil { return } + + testing.expect(t, doc.xref[7].kind == .Normal, + "object 7 should come from the older section") + v, ok := to_int(get_object(doc, Ref{7, 0})) + testing.expectf(t, ok && v == i64(len(raw_b)), + "object 7 read as %v (ok %v), wanted %v", v, ok, len(raw_b)) + expect_page(t, doc, 3, 200) +}