Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,19 @@ all of them against the published ECMA-376 schemas. A document the application
offers to repair cannot answer "does it open the repaired file without a
prompt", which is the M1 application check.

**The PDF fixtures are not corpus documents, and are generated too.** The eight
under `crates/mirsam-pdf/tests/pdfs/` come from `scripts/make-pdf-fixture.py`
and regenerate with `make pdfs` — one file per *generator family*, because what
PDF work hunts is a generator signature rather than an author mistake: a classic
cross-reference table, a cross-reference stream with object streams, a hybrid
file, an incremental update, a stale `startxref`, an AES-256 document, a
distiller's ASCII filter chain, and a page that is a scan. They live beside the
crate rather than in `tests/fixtures/` on purpose: the golden corpus is what the
*binary* does to a document, and `mirsam audit` does not read a `.pdf` yet. When
a `DocumentReader` lands, two of them join the corpus and the exit code `repair`
gives a readable format with no writer is recorded on a real PDF, the way
`quarterly-page.html` records it now.

**The two HTML corpus documents are the exception, and edited by hand.** A
page *is* its own source: there is no package to build, so `make fixtures` has
nothing to generate and `scripts/validate-ooxml.py` has nothing to validate.
Expand Down
48 changes: 46 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,56 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Milestones M1 through M5 have landed since `v0.1.0`: byte-preserving `repair`,
inheritance resolution, the DOCX and XLSX adapters with their writers, the HTML
reader, and shaping and font-coverage checks behind `--fonts`. Next is M6, the
read-only PDF adapter. See [`docs/PLAN.md`](docs/PLAN.md) and
reader, and shaping and font-coverage checks behind `--fonts`. M6, the
read-only PDF adapter, is under way: its object layer has landed and text
extraction is next. See [`docs/PLAN.md`](docs/PLAN.md) and
[`docs/ROADMAP.md`](docs/ROADMAP.md).

### Added

- **`mirsam-pdf`, the PDF object layer.** The package layer of a format that is
not a package: both spellings of the cross-reference table, `/Prev` chains,
hybrid-reference files, object streams, the general-purpose stream filters,
the page tree with the four attributes it inherits, and content streams
decoded and joined. Nothing is wired into the CLI yet — `mirsam audit`
still turns a `.pdf` away — because text extraction is PLAN §6.2, and a
binary that accepted a `.pdf` before it could read one would be making a
claim this milestone has not earned.

**The parser is written here and the decompressor is taken**, which PLAN
§6.1 asked for as an ADR rather than a `Cargo.toml` line. What settled it was
which half of ISO 32000-1 mirsam asks for: the object graph and the content
streams are §7.2 to §7.8, and everything from §8 on is the rendering this
project has said it will never do. The deciding cost was the second one — a
library's object model becomes the adapter's vocabulary, and with it every
answer to *is a dangling reference null, is a wrong `/Length` readable, is a
stale `startxref` fatal*, which are exactly the honesty decisions §6.2 turns
on. `flate2` is taken for `FlateDecode`, pinned to the `zlib-rs` backend
`zip` already resolves, so no dependency entered the tree. See
[ADR 0010](docs/adr/0010-write-the-pdf-object-layer-take-the-decompressor.md).

**An encrypted document is refused by name.** `AES-256 (/Standard security
handler, V5 R6)`, read out of the `/Encrypt` dictionary that §7.6.1 keeps in
the clear — including the permissions-only wrapper with an empty user
password, because a tool that opened those is one nobody could run on a file
they were given. The alternative was extracting nothing and reporting a clean
file, which is the failure the whole milestone is arranged against.

**What could not be read is named, and so is what had to be rebuilt.** The
four image codecs are named rather than decoded — a stream behind `DCTDecode`
is not text this crate failed to read, it is text that is not in the file —
and a scanned page therefore reaches `unread` as `5 0 R — /DCTDecode, a
filter this tool does not decode`. That is ADR 0009 reaching the format that
needs it most. Separately, a file whose `startxref` has gone stale is
recovered from by scanning for `obj`, the way every viewer opens it, and says
so: a document reported unreadable while a reader opens it is the wrong half
of standing rule 4.

Eight fixtures, one per generator family, are written byte by byte by
`scripts/make-pdf-fixture.py` and regenerate with `make pdfs` — a fixture
built by the library under test proves only that the library agrees with
itself.

- **The DOCX writer.** `mirsam repair report.docx fixed.docx` works, and
`repair` stops turning a Word document away as a readable format without a
writer. A repair writes `w:pPr/w:bidi`, `w:pPr/w:jc`, `w:rPr/w:lang@w:bidi`,
Expand Down
8 changes: 8 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/mirsam-core",
"crates/mirsam-ooxml",
"crates/mirsam-html",
"crates/mirsam-pdf",
"crates/mirsam-conformance",
"crates/mirsam-fonts",
"crates/mirsam-cli",
Expand All @@ -24,6 +25,7 @@ categories = ["command-line-utilities", "text-processing", "internationalization
mirsam-core = { path = "crates/mirsam-core", version = "0.1.0" }
mirsam-ooxml = { path = "crates/mirsam-ooxml", version = "0.1.0" }
mirsam-html = { path = "crates/mirsam-html", version = "0.1.0" }
mirsam-pdf = { path = "crates/mirsam-pdf", version = "0.1.0" }
mirsam-fonts = { path = "crates/mirsam-fonts", version = "0.1.0" }
unicode-bidi = "0.3.18"
unicode-normalization = "0.1.25"
Expand All @@ -36,6 +38,10 @@ quick-xml = "0.42"
# than taking `markup5ever_rcdom`, which names itself unsupported.
html5ever = "0.39"
zip = { version = "8.6", default-features = false, features = ["deflate"] }
# FlateDecode. Held to the backend `zip` already pulls in: two inflate
# implementations in one binary would be two answers to "what do these bytes
# decompress to".
flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ fixtures: ## Regenerate the hand-built corpus documents, then their reports
@python3 scripts/make-torture-fixture.py
@python3 scripts/make-word-fixture.py
@python3 scripts/make-excel-fixture.py
@$(MAKE) --no-print-directory pdfs
@$(MAKE) --no-print-directory golden

pdfs: ## Regenerate the PDF fixture corpus (one file per generator family)
@python3 scripts/make-pdf-fixture.py

fonts: ## Regenerate the hand-built fonts the shaping tests run against
@python3 scripts/make-shaping-fixture.py

Expand Down Expand Up @@ -113,5 +117,5 @@ install: ## Install mirsam into ~/.cargo/bin
@cargo install --path crates/mirsam-cli

.PHONY: help build test fmt fmt-check lint doc clean version check-version \
codename release-name msrv audit-deps golden fixtures fonts names \
codename release-name msrv audit-deps golden fixtures pdfs fonts names \
validate-fixtures corpus verify pre-push hooks-install install
16 changes: 16 additions & 0 deletions crates/mirsam-pdf/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "mirsam-pdf"
description = "PDF object layer for mirsam: cross-reference tables and streams, object streams, stream filters and the page tree. Reads; never writes."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true

[dependencies]
mirsam-core.workspace = true
# The one thing not written here. See docs/adr/0010. Pinned to the backend the
# workspace already resolves through `zip`, so the binary carries one inflate
# implementation rather than two.
flate2 = { workspace = true }
Loading
Loading