Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fc0ce64
Add generate-office-goldens.sh for print-from-Office golden PNGs
claude Jul 13, 2026
2e8850f
Honour Word's min-content-width guarantee for autofit table columns
claude Jul 13, 2026
87e96ef
Wire the ACID + Iris Blueprint DOCX into the visual golden pipeline
claude Jul 13, 2026
086ee31
Render variable fonts at the shaped instance (fixes bold Arial)
claude Jul 13, 2026
3749f71
Fix list/tab overshoot: exclude the tab char from shaped text
claude Jul 13, 2026
fe7c428
Instance variable fonts in PDF export (bold Arial now exports bold)
claude Jul 13, 2026
8eb8c93
Lay out empty paragraphs with a line of height and their border
claude Jul 13, 2026
eb4a603
Span paragraph borders/shading across the content column, not the ink
claude Jul 14, 2026
ed32a9f
Anchor exact-line clip on the body run's descent, not the line max
claude Jul 14, 2026
0ac87d5
Render wrapNone floats as overlaps, matching Word
claude Jul 14, 2026
95c17e4
Add ACID 2 combined-feature fixture and fix the layout bugs it surfaced
claude Jul 14, 2026
0564432
Add DOCX Word-compatibility repair; fix acid2 fixture and the export …
claude Jul 19, 2026
d669881
loki-text: offer to repair a Word-incompatible DOCX on open
claude Jul 19, 2026
c19e59b
fix(acid2): repair the real Word-reject cause — undeclared mc:Ignorab…
claude Jul 19, 2026
2a01727
fix(opc): type the core-properties part so Word can open the package
claude Jul 19, 2026
8fbe95b
fix(acid2): drop spurious endnotePr so Word reports no Endnotes error
claude Jul 19, 2026
5a223ac
feat(ooxml-repair): detect + fix dangling note-separator refs (3rd axis)
claude Jul 19, 2026
2a1e8ee
fix(layout): map "Calibri Light" to Carlito (heading/title font parity)
claude Jul 19, 2026
cecde51
feat(tables): render table-style borders (Table Grid draws its grid)
claude Jul 19, 2026
1c4fdd7
feat(layout): render page borders (w:pgBorders)
claude Jul 19, 2026
09224e0
fix(layout): keep decimal/right/centre tab columns within narrow cells
claude Jul 19, 2026
8bdee6f
loki-ooxml: flush an open complex field's cached result at paragraph end
claude Jul 19, 2026
4c64240
loki-layout: draw an underlined tab run's rule (signature lines)
claude Jul 19, 2026
13d9842
Render margin line numbering (w:lnNumType)
claude Jul 19, 2026
9ee36a0
loki-ooxml: map w:noBreakHyphen / w:softHyphen to their Unicode hyphens
claude Jul 19, 2026
cade347
loki-ooxml: approximate line/cross w:shd texture patterns as a solid …
claude Jul 19, 2026
b36a831
Render w:bdr character borders (run box)
claude Jul 19, 2026
144911d
loki-layout: render footnotes per-page; stop dropping keep-with-next …
claude Jul 19, 2026
7db1314
loki-layout: render emboss/imprint/shadow run effects as 3-D relief
claude Jul 19, 2026
1ca9b9b
loki-ooxml/loki-layout: render floating wps text boxes with wrap
claude Jul 19, 2026
840c4f7
loki-ooxml: DOCX export for char border, emboss/imprint, and text boxes
claude Jul 19, 2026
c3bb237
loki-odf: ODT round-trip for emboss/imprint and character border
claude Jul 20, 2026
e3fc809
loki-doc-model: Loro-bridge round-trip for emboss/imprint/char border
claude Jul 20, 2026
51e9a58
loki-odf: ODT text-box export + import (round-trips DOCX <-> ODT)
claude Jul 20, 2026
a5af033
Render w:shd line/cross hatch textures as actual hatch lines
claude Jul 20, 2026
89241af
Non-destructive tracked-change display modes (All Markup / Final / Or…
claude Jul 20, 2026
5f79bf3
Reserve footnote-band space so body content stops above it
claude Jul 20, 2026
4fc9831
loki-text: add emboss/imprint/character_border to StyleSpan test lite…
claude Jul 20, 2026
365ebef
ooxml: lower table nesting cap to 50 so the depth guard fires before …
claude Jul 20, 2026
2dd9658
ci(lint): baseline deliberate `let _ =` / `#[allow]` debt for the new…
claude Jul 20, 2026
83c26cb
loki-headless(repair): bind --out with let-else instead of expect()
claude Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions appthere-conformance/examples/rasterize_pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// Copyright 2026 AppThere Loki contributors

//! CLI wrapper over the pinned PDF→PNG rasterizer (Spec 02 D3), used by
//! `scripts/generate-odf-goldens.sh` so golden generation goes through the
//! exact same stage as every other rasterization.
//! `scripts/generate-odf-goldens.sh` (scripted LibreOffice path) and
//! `scripts/generate-office-goldens.sh` (ingest a PDF printed from
//! Word/Excel/LibreOffice) so golden generation goes through the exact same
//! stage as every other rasterization.
//!
//! Usage: `cargo run -p appthere-conformance --example rasterize_pdf -- <pdf> <out_dir> <stem>`

Expand Down
Binary file added appthere-conformance/fixtures/docx/acid-docx.docx
Binary file not shown.
Binary file not shown.
Binary file not shown.
27 changes: 27 additions & 0 deletions appthere-conformance/goldens/docx/acid-docx/PENDING.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Golden capture PENDING — no reference render committed yet.

The visual golden for this DOCX fixture is Microsoft Word's own render,
which cannot be produced headlessly on Linux/CI. To capture it, on a
machine with Microsoft Office:

1. Open appthere-conformance/fixtures/docx/acid-docx.docx in Word.
2. Export/Print to PDF at default ("Standard") settings.
3. Run, from a checkout with poppler-utils installed:

scripts/generate-office-goldens.sh \
--format docx --stem acid-docx \
--pdf <the-printed>.pdf \
--reference "Microsoft 365 Word (<exact build>)" \
--fixture appthere-conformance/fixtures/docx/acid-docx.docx

This writes page-N.png + GENERATION.txt here, replacing this file. The
DOCX visual-golden test (loki-render-cpu/tests/visual_golden_docx.rs)
then compares Loki's render against them; until then it is a no-op.

NOTE (acid-docx): this stress fixture contains Japanese text and Loki
currently lacks the ICU4X `ja` line-segmentation model ("No segmentation
model for language: ja" at layout time). Until that is bundled, the CJK
line-breaking is not stable across builds, so a committed golden for this
fixture may compare unequal even when the layout is otherwise correct.
Bundle the `ja` segmenter before capturing this golden. The CJK-free
`iris-blueprint` fixture has no such caveat.
37 changes: 37 additions & 0 deletions appthere-conformance/goldens/docx/acid2-docx/PENDING.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Golden capture PENDING — no reference render committed yet.

The visual golden for this DOCX fixture is Microsoft Word's own render,
which cannot be produced headlessly on Linux/CI. To capture it, on a
machine with Microsoft Office:

1. Open appthere-conformance/fixtures/docx/acid2-docx.docx in Word.
2. Export/Print to PDF at default ("Standard") settings.
3. Run, from a checkout with poppler-utils installed:

scripts/generate-office-goldens.sh \
--format docx --stem acid2-docx \
--pdf <the-printed>.pdf \
--reference "Microsoft 365 Word (<exact build>)" \
--fixture appthere-conformance/fixtures/docx/acid2-docx.docx

This writes page-N.png + GENERATION.txt here, replacing this file. The
DOCX visual-golden test (loki-render-cpu/tests/visual_golden_docx.rs)
then compares Loki's render against them; until then it is a no-op.

NOTE (acid2-docx): this fixture is *hand-authored OOXML*, not a
Word-authored document — it is packaged from reviewable XML parts by
`cargo run -p loki-acid --example gen_acid2_docx`. Regenerate the fixture
(and this golden's provenance) from that generator if the parts change.
Unlike acid-docx it is deliberately CJK-free, so it has no `ja`
line-segmentation caveat. It combines features into four realistic
archetypes (business report, résumé, newsletter, invoice/contract) plus a
feature-matrix appendix, so a failing page maps to an archetype rather
than a single isolated construct.

Known Loki gaps this fixture currently surfaces (candidate golden-diff
regions once the reference lands): table-style (`w:tblStyle`) borders,
decimal tab stops *inside* table cells, floating text boxes (`wps`
shapes), page borders (`w:pgBorders`), line numbering (`w:lnNumType`),
pattern/gradient cell shading, run text effects (emboss/imprint/shadow),
character borders (`w:bdr`), and `w:noBreakHyphen`/`w:softHyphen` glyphs.
See docs/fidelity-status.md.
19 changes: 19 additions & 0 deletions appthere-conformance/goldens/docx/iris-blueprint/PENDING.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Golden capture PENDING — no reference render committed yet.

The visual golden for this DOCX fixture is Microsoft Word's own render,
which cannot be produced headlessly on Linux/CI. To capture it, on a
machine with Microsoft Office:

1. Open appthere-conformance/fixtures/docx/iris-blueprint.docx in Word.
2. Export/Print to PDF at default ("Standard") settings.
3. Run, from a checkout with poppler-utils installed:

scripts/generate-office-goldens.sh \
--format docx --stem iris-blueprint \
--pdf <the-printed>.pdf \
--reference "Microsoft 365 Word (<exact build>)" \
--fixture appthere-conformance/fixtures/docx/iris-blueprint.docx

This writes page-N.png + GENERATION.txt here, replacing this file. The
DOCX visual-golden test (loki-render-cpu/tests/visual_golden_docx.rs)
then compares Loki's render against them; until then it is a no-op.
50 changes: 50 additions & 0 deletions appthere-conformance/src/corpus/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,23 @@ pub const LIBREOFFICE_24_2: Reference = Reference {
version: "24.2",
};

/// The Microsoft Word reference for the OOXML/DOCX visual goldens. The exact
/// build is recorded per golden in its `GENERATION.txt` (Word can't be
/// automated headlessly, so its goldens are captured manually via
/// `scripts/generate-office-goldens.sh`); this names the authority.
pub const MICROSOFT_365: Reference = Reference {
app: "Microsoft 365 Word",
version: "manual capture (see GENERATION.txt)",
};

/// All three axes apply (the common case for the visual baseline fixtures).
const ALL_AXES: &[Axis] = &[Axis::Schema, Axis::RoundTrip, Axis::Visual];

/// The visual axis only — for fixtures whose reference is Microsoft Word (the
/// visual authority for OOXML) and whose round-trip/schema axes have not yet
/// been vetted against these specific documents.
const VISUAL_ONLY: &[Axis] = &[Axis::Visual];

/// The committed on-disk corpus. Every entry's document must exist under
/// [`fixtures_root`]; entries carrying [`Axis::Visual`] also have a golden
/// directory under [`goldens_root`].
Expand Down Expand Up @@ -58,6 +72,42 @@ pub const MANIFEST: &[FixtureMeta] = &[
reference: Some(LIBREOFFICE_24_2),
tolerance_override: None,
},
// OOXML/DOCX visual fixtures. References come from Microsoft Word (the
// OOXML authority) and are captured manually — the golden dirs currently
// hold a PENDING.txt (see it for the capture steps), so the DOCX
// visual-golden test is a documented no-op until the page PNGs land.
FixtureMeta {
id: "acid-docx",
format: Format::Docx,
feature: "ACID rendering stress suite (line spacing, tables, lists, tabs, fields)",
severity: Severity::P0,
axes: VISUAL_ONLY,
reference: Some(MICROSOFT_365),
tolerance_override: None,
},
FixtureMeta {
id: "iris-blueprint",
format: Format::Docx,
feature: "Real-world structured document (headings, bullet lists, shaded/bordered callout tables)",
severity: Severity::P1,
axes: VISUAL_ONLY,
reference: Some(MICROSOFT_365),
tolerance_override: None,
},
// ACID 2 — realistic archetypes (report, résumé, newsletter, invoice) that
// combine features densely, plus an isolated feature-matrix appendix. Unlike
// acid-docx it is hand-authored OOXML (see `loki-acid`'s `gen_acid2_docx`),
// so it can exercise constructs Loki cannot yet emit. Golden is Word's render
// of this same file; visual test is a no-op until the page PNGs land.
FixtureMeta {
id: "acid2-docx",
format: Format::Docx,
feature: "ACID 2 combined-feature archetypes (report/résumé/newsletter/invoice) + feature-matrix appendix",
severity: Severity::P0,
axes: VISUAL_ONLY,
reference: Some(MICROSOFT_365),
tolerance_override: None,
},
];

/// The corpus root: `appthere-conformance/fixtures/`.
Expand Down
12 changes: 12 additions & 0 deletions docs/audit-2026-06.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ plus a few robustness papercuts.
calling `parse_table`, and a nested-table field on `DocxTableCell`). **If** that
recursion is added, it reintroduces the depth concern from S-1, so pair it with
a `MAX_NESTING_DEPTH` guard at that point.
- **Resolution (2026-07-20):** Nested tables are parsed (the `tbl` arm calls
`parse_table` recursively) and guarded by `MAX_NESTING_DEPTH` in
`docx/reader/document_table.rs`. The guard rejects on depth *count*, but reaching
the rejection still recurses that many table levels first, and one table level
costs three stack frames (`parse_table` → `parse_table_row` → `parse_table_cell`).
At the original limit of 100 that is ~300 frames — enough to overflow a 2-MiB
worker-thread stack *before* the guard fires (the
`excessive_table_nesting_is_rejected_not_stack_overflow` regression test
reproduced this on the default 2-MiB test stack). The limit was lowered to **50**
so the recursion stays comfortably within a 2-MiB stack while remaining ~10× the
deepest real documents. The `w:sdt` content-control parser (1 frame/level) shares
the same budget.

### S-2 — Unsanitized dimension values from file → unit conversion *(Verified · Low · P2)*

Expand Down
2 changes: 1 addition & 1 deletion docs/deferred-features-plan-2026-07-04.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ BM-3** (render-cost proxy) and the ACID headless-raster registry item.
| 3.2 | B-6 | ✅ **Done 2026-07-05** — ISO 29500-4:2016 Transitional + mce, ECMA-376 Part-2 OPC, OASIS ODF 1.3 RNG + MathML3 vendored with PROVENANCE (sha256); real DOCX/ODT exports schema-validated incl. malformed-part negative tests. Tails: Strict XSDs, Dublin Core imports for core.xml (documented in `schemas/README.md`). | M |
| 3.3 | B-1 | ✅ **Done 2026-07-05** — new `loki-render-cpu` crate: `vello_cpu` (=0.0.9) rasterizes the same `PositionedItem` stream the GPU path paints, headless, byte-deterministic (M5 acceptance smoke tests). TODO(conformance-render): decode image items (grey placeholder today). | M |
| 3.4 | B-5 | ✅ **Done 2026-07-05** — `appthere_conformance::raster::PdfRasterizer` (pdftoppm pinned flags @ `CONFORMANCE_DPI` 144, version captured, byte-determinism tested). | S |
| 3.5 | B-2, B-3, B-4 | ✅ **Done 2026-07-05** — SSIM+CIEDE2000 worst-region differ with heatmaps (`golden/diff.rs`, Sharma reference pairs verified); 3 ODF goldens committed with GENERATION metadata (`scripts/generate-odf-goldens.sh`); calibration record `goldens/CALIBRATION.md` → `Tolerance::calibrated()` {0.60, 10.0}. **The calibration pass found and quantified fidelity gap #23 (kerning)** — pinned as the `para-carlito` expected-failure canary. OOXML goldens remain the manual Windows/Word COM procedure (§7.2). | M |
| 3.5 | B-2, B-3, B-4 | ✅ **Done 2026-07-05** — SSIM+CIEDE2000 worst-region differ with heatmaps (`golden/diff.rs`, Sharma reference pairs verified); 3 ODF goldens committed with GENERATION metadata (`scripts/generate-odf-goldens.sh`); calibration record `goldens/CALIBRATION.md` → `Tolerance::calibrated()` {0.60, 10.0}. **The calibration pass found and quantified fidelity gap #23 (kerning)** — pinned as the `para-carlito` expected-failure canary. OOXML goldens follow the manual print-from-Office procedure (§7.2), now scripted by `scripts/generate-office-goldens.sh` (ingest a Word/Excel-printed PDF → same pinned rasterizer → `goldens/<format>/<stem>/`); it also serves LibreOffice manual prints for ODF. | M |
| 3.6 | B-11 | ✅ **Done 2026-07-05** — all three axes run as cargo tests in the existing `build-and-test` job (xmllint + pdftoppm installed); schema + round-trip are hard gates, visual is advisory-by-construction (known divergence pinned; hardens when kerning lands + recalibration). | S |
| 3.7 | B-8, B-9 | **Done ✅ 2026-07-12** — the corpus layer is promoted into `appthere-conformance::corpus` (B-8): the `TC-*` catalog (the four per-format tables moved wholesale; grouping helpers add the *feature* dimension — `by_feature`/`cases_with_severity`), the `Format`/`Severity` vocabulary, and the new `Fixture`/`Consumer` traits (a consumer supplies bytes+metadata and an import/export pair; the crate stays free of Text-specific assumptions). Golden/candidate discovery generalised into `golden::discovery` (root-parameterized). `loki-acid` is the **first consumer**: its `catalog`/`severity` modules are re-export shims, the acid `Fixture` enum implements the shared trait (axes derived from importer availability), `AcidConsumer` wraps the import dispatch (export honestly reports the corpus is import-only), and its golden discovery delegates. B-9: the on-disk corpus is `fixtures/<format>/<id>.<ext>` + `goldens/<format>/<id>/` with a code-as-manifest (`corpus::manifest::MANIFEST`) recording per fixture the feature, applicable axes, reference app+version (LibreOffice 24.2 per `CALIBRATION.md`), and justified tolerance overrides (none needed post-kerning-fix); manifest tests assert on-disk existence + visual entries carry references. *Corrected en route:* the spec's "141 TC cases" headline miscounts — the per-format tables (the machine-readable truth) sum to **139**, now asserted. ODP/ODG/PPTX importers stay gated on the unbuilt ACID PPTX generator (§5.1, ratified). | M |

Expand Down
Loading
Loading