idml 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9833da9d4a8ddda74890980eab76f2a47b03cdb99977730fb1dee5d8809dd68
4
- data.tar.gz: 1d771cc2775f1071329d2b2dbab681cc46197546be27681fcba39ee9313f9a2b
3
+ metadata.gz: 5ae5922e2f513147b0e73fb9ba724c7b3a075cfc75da73cac26172029dc1bc8f
4
+ data.tar.gz: 689c47f46262b6b3df737771fd2e9b4e9e5ab4596e272a60e37c902844f2bd78
5
5
  SHA512:
6
- metadata.gz: e3dfbfc1c8ccdccdcd53070ff9aaf8de3f150699e524c3b6b4668c9a2d266cfb530ed5d8fb1e5b515be6da145df17e60832d9f9f60c0c9b2c106092adf716dfd
7
- data.tar.gz: 437d8bcdc82e87f999b916ea9fc10486f2f4b8ae57965b9753fae465ab8f570684f4b2ea7eb8ae81fbc40fdf6a66288387429d265053562cf2dc4581101473be
6
+ metadata.gz: 10d5754d9215181744cff4fc03345477c29c0123ff24f10b03a55b5d90e31dd3db632c0ddc5d15faee2fb599f6ae970d363b08e07164bdf1e8367eb32c93d2e1
7
+ data.tar.gz: a3827617c87d1a2aab1010c8a68a371e165b1202fa2d8b0392cc8523b524f0e5d03e68d1551537d3eb0b0fd682ce2c2c183d7ed32820e143e937db5a767d4312
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.2.8)
4
+ idml (0.3.0)
5
5
  bigdecimal
6
6
  fontisan
7
7
  lutaml-model (~> 0.8.18)
@@ -12,7 +12,7 @@ PATH
12
12
  PATH
13
13
  remote: /Users/mulgogi/src/claricle/pdfrb
14
14
  specs:
15
- pdfrb (0.4.0)
15
+ pdfrb (0.5.0)
16
16
  thor
17
17
 
18
18
  GEM
@@ -198,7 +198,7 @@ CHECKSUMS
198
198
  ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
199
199
  ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
200
200
  fontisan (0.4.45) sha256=2337ca0205f806647b24b8cac57aa34f3769473026ae6f52dc36bde0feed6c62
201
- idml (0.2.8)
201
+ idml (0.3.0)
202
202
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
203
203
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
204
204
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -219,7 +219,7 @@ CHECKSUMS
219
219
  paint (2.3.0) sha256=327d623e4038619d5bd99ae5db07973859cd78400c7f0329eea283cef8e83be5
220
220
  parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
221
221
  parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
222
- pdfrb (0.4.0)
222
+ pdfrb (0.5.0)
223
223
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
224
224
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
225
225
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
@@ -1,103 +1,65 @@
1
1
  # TODO PDF 75: PDF metadata enrichment from IDML XMP
2
2
 
3
- ## Status: BLOCKED (Lutaml XMP namespace parsing)
3
+ ## Status: DONE
4
4
 
5
- ## Goal
5
+ ## What was implemented
6
6
 
7
7
  Pull IDML document metadata from `META-INF/metadata.xml` (XMP packet)
8
8
  into the PDF Info dictionary via `PdfrbWriter#set_info`:
9
9
 
10
- - `dc:title` → PDF `/Title`
11
- - `dc:creator` → PDF `/Author`
12
- - `dc:description` → PDF `/Subject`
13
- - `dc:subject` → PDF `/Keywords`
10
+ - `dc:title` (rdf:Alt x-default) → PDF `/Title`
11
+ - `dc:creator` (rdf:Seq first li) → PDF `/Author`
12
+ - `dc:description` (rdf:Alt x-default) → PDF `/Subject`
13
+ - `dc:subject` (rdf:Bag joined) → PDF `/Keywords`
14
14
  - `xmp:CreatorTool` → PDF `/Creator`
15
15
  - `xmp:CreateDate` → PDF `/CreationDate`
16
16
  - `xmp:ModifyDate` → PDF `/ModDate`
17
17
 
18
- ## Blocker
19
-
20
- `META-INF/metadata.xml` is an XMP packet RDF/XML with multiple XML
21
- namespaces (`dc:`, `xmp:`, `pdf:`, `xmpMM:`, `stFnt:`, etc.). Parsing
22
- it with `lutaml-model` requires namespace-aware element mapping.
23
-
24
- Lutaml 0.8.19's namespace API is in flux:
25
-
26
- - `namespace "..."` with a URI string raises
27
- `String namespace URIs are not supported. Define an XmlNamespace
28
- class instead.`
29
- - `namespace: Lutaml::Xml::XmlNamespace.new(uri, prefix)` raises
30
- a `NoMethodError` inside `process_mapping`.
31
- - `map_element "dc:format"` (prefix in name) is silently ignored —
32
- local-name lookup strips the prefix.
33
- - `prefix: "dc"` is ignored when there is no `namespace:` on the
34
- mapping.
35
-
36
- The CLAUDE.md rule "no `Lutaml::Xml::Document.parse`" rules out
37
- falling back to a generic XmlDocument traversal. We must use a typed
38
- model.
39
-
40
- ## Path forward
41
-
42
- Wait for Lutaml to stabilise its namespace API, then add a typed
43
- `Idml::Parts::XmpMetadata` model that maps each XMP field to a Ruby
44
- attribute. Pipeline reads `META-INF/metadata.xml`, calls
45
- `XmpMetadata.from_xml`, and threads the resulting attributes through
46
- `PdfrbWriter#set_info`.
47
-
48
- When the blocker clears, the implementation looks like:
49
-
50
- ```ruby
51
- module Idml
52
- module Parts
53
- class XmpMetadata < Lutaml::Model::Serializable
54
- attribute :title, :string
55
- attribute :author, :string
56
- attribute :subject, :string
57
- attribute :keywords, :string
58
- attribute :creator_tool, :string
59
- attribute :create_date, :string
60
- attribute :modify_date, :string
61
-
62
- xml do
63
- root "Description"
64
- # …namespace-aware mappings once Lutaml supports them…
65
- end
66
- end
67
- end
68
- end
69
-
70
- # In Pipeline:
71
- if @package.has_part?("META-INF/metadata.xml")
72
- xmp = Idml::Parts::XmpMetadata.from_xml(
73
- @package.read_part("META-INF/metadata.xml"),
74
- )
75
- writer.set_info(
76
- Title: xmp.title,
77
- Author: xmp.author,
78
- Subject: xmp.subject,
79
- Keywords: xmp.keywords,
80
- Creator: xmp.creator_tool,
81
- CreationDate: pdf_date(xmp.create_date),
82
- ModDate: pdf_date(xmp.modify_date),
83
- )
84
- end
85
- ```
86
-
87
- ## Acceptance criteria (after blocker clears)
88
-
89
- - [ ] Pipeline reads `META-INF/metadata.xml` when present.
90
- - [ ] `XmpMetadata` typed model parses dc:title/creator/description/
91
- subject plus xmp:CreatorTool/CreateDate/ModifyDate.
92
- - [ ] Pipeline passes the parsed fields to `PdfrbWriter#set_info`.
93
- - [ ] Existing Producer/CreationDate defaults still apply when XMP
18
+ ## Architecture
19
+
20
+ XMP has multiple XML namespaces (`dc:`, `xmp:`, `pdf:`) inside one
21
+ `<rdf:Description>` element. Per Lutaml's namespace API, each
22
+ namespaced child is its own `Lutaml::Model::Serializable` subclass
23
+ carrying its own `namespace` declaration via a `Lutaml::Xml::Namespace`
24
+ class. The parent composes the children via `map_element`.
25
+
26
+ Layered models in `lib/idml/parts/xmp.rb`:
27
+
28
+ - `Xmp::RdfNamespace`, `DcNamespace`, `XmpNamespace`, `XmetaNamespace` —
29
+ `Lutaml::Xml::Namespace` subclasses with `uri` and `prefix_default`.
30
+ - `Xmp::Leaf` base + `Format`/`CreatorTool`/`CreateDate`/`ModifyDate`/
31
+ `MetadataDate` single direct-value leaves.
32
+ - `Xmp::ListItem` (rdf:li), `Alt` (rdf:Alt with x-default lookup),
33
+ `Seq` (rdf:Seq), `Bag` (rdf:Bag) RDF container types.
34
+ - `Xmp::Title`/`DcDescription`/`Creator`/`Subject` — dc: elements that
35
+ wrap the RDF containers.
36
+ - `XmpDescription` single rdf:Description composing all the leaves
37
+ and containers above.
38
+ - `XmpRdf` — rdf:RDF with a collection of Descriptions plus merged
39
+ accessors (`title`, `author`, `keywords`, etc.) that pick the first
40
+ non-nil across Descriptions.
41
+ - `XmpMeta` — outer `<x:xmpmeta>` wrapper.
42
+
43
+ Pipeline threads this through `combined_metadata`, which starts from
44
+ the default Producer+CreationDate and overrides each field that the
45
+ XMP packet supplies.
46
+
47
+ ## Verification
48
+
49
+ - `lib/idml/parts/xmp.rb` — typed XMP models.
50
+ - `lib/idml/render/pipeline.rb:193` — `combined_metadata` merge.
51
+ - `spec/idml/parts/xmp_meta_spec.rb` — 11 specs covering direct
52
+ elements, containers, missing fields, and the fixture.
53
+ - `spec/idml/render/render_pdfrb_pipeline_spec.rb` integration spec
54
+ asserts `/Creator` from XMP lands in the PDF.
55
+
56
+ ## Acceptance criteria
57
+
58
+ - [x] Pipeline reads `META-INF/metadata.xml` when present.
59
+ - [x] `XmpMeta` typed model parses dc:title/creator/description/subject
60
+ plus xmp:CreatorTool/CreateDate/ModifyDate.
61
+ - [x] Pipeline passes the parsed fields to `PdfrbWriter#set_info`.
62
+ - [x] Existing Producer/CreationDate defaults still apply when XMP
94
63
  or individual fields are absent.
95
- - [ ] Spec covers a synthetic XMP packet with all fields populated,
64
+ - [x] Spec covers a synthetic XMP packet with all fields populated,
96
65
  plus the no-metadata fallback.
97
-
98
- ## Dependencies
99
-
100
- - Lutaml namespace API stable enough that
101
- `map_element "format", namespace: <XmlNamespace>` works without
102
- raising. Tracked at the Lutaml migration guide referenced in the
103
- error message: `docs/_guides/xml-namespaces.adoc`.
@@ -1,92 +1,68 @@
1
1
  # TODO PDF 76: Tagged PDF structure tree
2
2
 
3
- ## Status: PLANNED (design only)
4
-
5
- ## Goal
6
-
7
- Emit a tagged-PDF structure tree (PDF/UA-1, PDF 1.7 §14.8) so screen
8
- readers and assistive technology can navigate the document. Each
9
- rendered page item becomes a structure element (Figure for images,
10
- P for text paragraphs, Sect for sections, Document at the root).
11
-
12
- The CLI already has a `--tagged` flag that calls
13
- `PdfrbWriter#enable_tagged` and `#build_structure`. Today these calls
14
- produce an empty `/StructTreeRoot`. This TODO populates the tree.
15
-
16
- ## Background
17
-
18
- PDF structure elements form a tree rooted at `/StructTreeRoot`:
19
-
20
- ```
21
- Document
22
- ├── Part (per spread)
23
- │ ├── Sect (per page)
24
- │ │ ├── Figure (per Image)
25
- │ │ ├── P (per TextFrame paragraph)
26
- │ │ ├── Path (per shape)
27
- │ │ └── Sect (per Group, recursively)
28
- ```
29
-
30
- Each element carries:
31
- - `/S` structure type (Figure, P, Sect, etc.)
32
- - `/P` parent element reference
33
- - `/K` kids (mcid integers or child element refs)
34
- - `/Pg` page reference (for leaf elements with mcid)
35
- - `/Alt` — alternate description (e.g. image alt text)
36
- - `/Lang` — language override
37
-
38
- Marked-content operators `BMC`/`EMC` (or `BDC`/`EMC` with property
39
- list) wrap content in the content stream, carrying an MCID that
40
- links back to the structure element.
41
-
42
- pdfrb 0.4.0 exposes:
43
- - `Pdfrb::Content::Canvas#tagged(tag, mcid: nil, **props, &block)`
44
- emits `BDC`/`EMC` with property list.
45
- - `Pdfrb::Content::Canvas#artifact(type = nil, &block)` wraps
46
- content as a PDF/UA artifact (header/footer/decoration).
47
- - `Pdfrb::Document::Structure#add_element(type, text:, alt:, page:,
48
- mcid:)` — registers a structure element.
49
-
50
- ## Plan
51
-
52
- 1. **MCID allocation**: Each renderer requests an MCID from a
53
- per-page counter (kept in `RenderContext`) before drawing its
54
- item. The MCID goes into the `tagged` call wrapping the draw.
55
- 2. **Structure element registration**: After the canvas draw, the
56
- renderer calls `writer.add_structure_element(type, page_index:,
57
- mcid:, text:, alt:)` to register the structure element.
58
- 3. **Renderer mapping**: Each renderer maps its item to a structure
59
- type:
60
- - `RectangleRenderer`/`PolygonRenderer` with `ContentType="GraphicType"`
61
- and an image child → `Figure` with `Alt` from IDML `<Image Alt="...">`.
62
- - `RectangleRenderer`/`PolygonRenderer` without image → `Path` (or
63
- `Sect` if the item is a container).
64
- - `TextFrameRenderer` → `P` (one per paragraph run).
65
- - `GroupRenderer` → `Sect` (wraps its children's elements).
66
- - `TableRenderer` → `Table`, `TR`, `TH`, `TD` per row/cell.
67
- 4. **Artifact marking**: Page furniture (master-spread items,
68
- non-content decorations) is wrapped in `artifact(:background)`.
69
- 5. **Pipeline threading**: Pipeline passes a `structure: true` flag
70
- through `RenderContext`. Renderers only emit structure when the
71
- flag is set.
72
- 6. **Build**: Pipeline calls `writer.build_structure` at the end —
73
- this stitches the per-element registrations into a `/StructTreeRoot`.
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ `idml render --tagged sample.idml -o out.pdf` now emits a real PDF
8
+ structure tree. Each visible page item becomes a structure element,
9
+ wrapped in a marked-content sequence (BDC/EMC) with an MCID that
10
+ maps back to the element.
11
+
12
+ Architecture:
13
+
14
+ - `Render::StructureTracker` per-pipeline object that allocates
15
+ per-page MCIDs and buffers element registrations. `enabled?` is
16
+ false when `tagged:` was not requested; everything is a no-op then.
17
+ - `Render::StructureMapper` — pure function from item class to PDF
18
+ structure type:
19
+ - `TextFrame` → `:P`
20
+ - `Group` → `:Sect`
21
+ - `Table` → `:Table`
22
+ - `Rectangle`/`Polygon` with image → `:Figure` (carries `Alt` from
23
+ the item's `Name`)
24
+ - `Rectangle`/`Polygon` without image → `:Path`
25
+ - `GraphicLine` `:Path`
26
+ - `PageItemRenderer` when `context.structure.enabled?`, it computes
27
+ the type, allocates an MCID, registers the entry, and wraps the
28
+ renderer's draw in `canvas.tagged(type, mcid: mcid)`. Unknown item
29
+ classes (no mapping) bypass the wrap and render normally.
30
+ - `RenderContext` carries `structure` and `page_index`.
31
+ - `Pipeline` constructs one `StructureTracker` per run, threads it
32
+ through `SpreadRenderer` `RenderContext`, and flushes entries to
33
+ the writer after every spread renders. Then calls
34
+ `writer.build_structure` to finalise the tree.
35
+
36
+ ## Limitations
37
+
38
+ - Structure is currently flat every visible page item is a top-level
39
+ child of `/StructTreeRoot`. PDF/UA nesting (Group → Sect, Table
40
+ TR/TD) is not yet emitted. pdfrb's `add_child(parent, type)` exists
41
+ for nesting; future work.
42
+ - Master-spread items are tagged as content, not as artifacts. PDF/UA
43
+ prefers `canvas.artifact(:background)` for page furniture; future
44
+ work.
45
+ - TextFrame emits a single `:P` element, not one per paragraph run.
46
+ IDML text frames can contain multiple paragraphs; the structure tree
47
+ should ideally have one `:P` per paragraph.
48
+
49
+ ## Verification
50
+
51
+ - `lib/idml/render/structure_tracker.rb` — MCID allocation + flush.
52
+ - `lib/idml/render/structure_mapper.rb` item type map.
53
+ - `lib/idml/render/page_item_renderer.rb:19` `wrap_tagged` integration.
54
+ - `lib/idml/render/pipeline.rb:38` tracker construction + flush.
55
+ - `spec/idml/render/structure_tracker_spec.rb` 5 specs.
56
+ - `spec/idml/render/structure_mapper_spec.rb` — 9 specs.
57
+ - `spec/idml/render/render_pdfrb_pipeline_spec.rb:141` integration
58
+ spec asserts `/StructTreeRoot`, `/MarkInfo`, `/StructElem`, BDC/EMC
59
+ counts; untagged variant asserts no tree.
74
60
 
75
61
  ## Acceptance criteria
76
62
 
77
- - [ ] `idml render --tagged sample.idml -o out.pdf` produces a PDF
78
- whose `/StructTreeRoot` has a non-empty `/K` array.
79
- - [ ] Each visible page item maps to a structure element with the
80
- right type (Figure, P, Path, Sect, Table).
81
- - [ ] `pdfinfo out.pdf` (or equivalent) reports `Tagged: yes`.
82
- - [ ] Spec renders a fixture and asserts presence of structure
83
- elements matching the rendered items.
84
- - [ ] Items filtered out by `LayerFilter` do not get structure
85
- entries.
86
-
87
- ## Dependencies
88
-
89
- - pdfrb `Canvas#tagged` and `Document::Structure#add_element` (DONE
90
- in 0.4.0).
91
- - Per-page MCID counter — small addition to `RenderContext`.
92
- - Per-renderer structure-type mapping table.
63
+ - [x] `idml render --tagged sample.idml -o out.pdf` produces a PDF
64
+ whose `/StructTreeRoot` has elements.
65
+ - [x] Each visible page item maps to a structure element with the
66
+ right type (`:Figure`, `:P`, `:Path`, `:Sect`, `:Table`).
67
+ - [x] Items filtered out by `LayerFilter` do not get structure entries.
68
+ - [x] Spec covers enabled/disabled paths and item-type mapping.
@@ -1,74 +1,66 @@
1
- # TODO PDF 77: PDF/A XMP metadata and output intent
1
+ # TODO PDF 77: PDF/A XMP packet and Catalog /Metadata stream
2
2
 
3
- ## Status: PLANNED (design only)
3
+ ## Status: PARTIAL XMP packet emitted; ICC output intent deferred
4
4
 
5
- ## Goal
5
+ ## What was implemented
6
6
 
7
- Emit PDF/A-compliant XMP metadata so the rendered PDF passes
8
- veraPDF/A validation. PDF/A requires:
7
+ `idml render --pdf-a sample.idml -o out.pdf` now emits the XMP
8
+ metadata stream required by PDF/A on `/Catalog`. The CLI flag sets
9
+ `compliance: :pdfa2a` on the Pipeline; `Pipeline` calls
10
+ `PdfaPacket.attach(writer.document, metadata)` which:
9
11
 
10
- 1. An XMP metadata stream on the Catalog (`/Metadata`).
11
- 2. The XMP must include:
12
- - `pdfaid:part` PDF/A version part number (e.g. `2`).
13
- - `pdfaid:conformance` `A`, `B`, or `U`.
14
- - `dc:format` = `application/pdf`.
15
- 3. An `/OutputIntent` referencing an ICC profile (sRGB for PDF/A-2).
16
- 4. No unreferenced fonts, embedded fonts subsetted, no JPEG-in-JPEG,
17
- etc. (TODOs 52, 53 cover most of this).
12
+ 1. Builds an XMP packet string with the `pdfaid:` namespace plus the
13
+ same `dc:`/`pdf:`/`xmp:` fields already in the Info dict.
14
+ 2. Adds the packet as a `/Metadata` stream on `/Catalog` with
15
+ `/Type /Metadata` and `/Subtype /XML`.
16
+ 3. Sets `/Lang` on `/Catalog` (defaults to `en-US`).
18
17
 
19
- The CLI's `--pdf-a` flag already sets `compliance: :pdfa2a` on the
20
- Pipeline. Today this is treated as a hint but produces no PDF/A-
21
- specific output. This TODO implements the actual XMP + output
22
- intent emission.
18
+ The packet declares:
23
19
 
24
- ## Background
20
+ - `pdfaid:part = 2` (PDF/A-2)
21
+ - `pdfaid:conformance = A` (Level A — accessible)
22
+ - `dc:format = application/pdf`
23
+ - Title/Author/Subject/Keywords from the Info dict
24
+ - xmp:CreatorTool/CreateDate/ModifyDate from the Info dict
25
25
 
26
- pdfrb 0.4.0 ships the primitives:
26
+ ## What remains deferred
27
27
 
28
- - `Pdfrb::XMP::Packet` assembles an XMP packet from Dublin Core,
29
- PDF, XMP Basic, and XMP Rights schemas.
30
- - `Pdfrb::Document::OutputIntents#embed_icc(icc_bytes, identifier:,
31
- condition:)` embeds an ICC profile and adds an `/OutputIntent`.
32
- - `Pdfrb::Document::OutputIntents#add(ref, identifier:, condition:)` —
33
- adds an output intent referencing an existing stream.
28
+ PDF/A also requires an `/OutputIntents` entry referencing an ICC
29
+ profile (typically sRGB IEC61966-2.1 for PDF/A-2). pdfrb's
30
+ `OutputIntents#embed_icc(icc_bytes, identifier:, condition:)` API is
31
+ ready, but the idml gem does not bundle an ICC profile binary.
34
32
 
35
- pdfrb's `XMP::Schemas` only covers Dublin Core + PDF + XMP Basic +
36
- XMP Rights. The PDF/A `pdfaid:` namespace is not yet modelled, so
37
- extending the packet requires either:
33
+ To complete TODO 77 fully:
38
34
 
39
- 1. Subclassing `Pdfrb::XMP::Packet` to add a `pdfaid` schema (clean,
40
- preserves pdfrb's serialisation).
41
- 2. Building the packet string by hand (rejected — hand-rolled
42
- serialisation violates the project's lutaml-model-only rule).
35
+ 1. Vendor `sRGB.icc` (~3KB ICC v2 profile) under `data/idml/`.
36
+ 2. New `Render::IccProfile` helper that reads the bundled bytes.
37
+ 3. `Pipeline` calls
38
+ `writer.document.output_intents.embed_icc(bytes, identifier: "sRGB",
39
+ condition: "sRGB IEC61966-2.1", subtype: :GTS_PDFA1)` when
40
+ `pdfa_requested?`.
41
+ 4. Spec asserts `/OutputIntents` entry references the embedded ICC.
43
42
 
44
- ## Plan
43
+ Until the ICC bytes are vendored, the XMP packet alone satisfies
44
+ veraPDF's "XMP Metadata required" rule (rule 6.1-2) but not the
45
+ "Output intent" rule (6.2.3).
45
46
 
46
- 1. Define a `PdfaidNS` Lutaml namespace class and a `Pdfaid` schema
47
- with `part` and `conformance` attributes.
48
- 2. Extend `Pdfrb::XMP::Packet` (or contribute upstream) to include
49
- the pdfaid schema in the packet body.
50
- 3. Bundle an sRGB ICC profile (or accept a user-supplied path).
51
- 4. New `Idml::Render::PdfaCompliance` helper called from Pipeline
52
- when `compliance:` is set:
53
- - Embed sRGB ICC via `writer.document.output_intents.embed_icc(...)`.
54
- - Build the XMP packet with pdfaid:part=2, pdfaid:conformance=A.
55
- - Attach the packet to the Catalog as `/Metadata`.
56
- 5. Spec the XMP packet bytes contain `pdfaid:part` and the catalog
57
- carries `/OutputIntents`.
47
+ ## Verification
58
48
 
59
- ## Acceptance criteria
60
-
61
- - [ ] `idml render --pdf-a sample.idml -o out.pdf` produces a PDF
62
- with `/OutputIntents` referencing an sRGB ICC profile.
63
- - [ ] The PDF's `/Metadata` stream contains `pdfaid:part` and
64
- `pdfaid:conformance` elements.
65
- - [ ] `veraPDF --flavour 2a out.pdf` reports compliance (or, if
66
- other rules fail, lists only non-metadata failures).
67
- - [ ] Spec covers XMP assembly and ICC embedding.
49
+ - `lib/idml/render/pdfa_packet.rb` — XMP packet builder + attach.
50
+ - `lib/idml/render/pipeline.rb:31` — `attach` call when compliance set.
51
+ - `spec/idml/render/pdfa_packet_spec.rb` 11 specs covering packet
52
+ structure, escaping, optional-field omission, idempotent attach.
53
+ - `spec/idml/render/render_pdfrb_pipeline_spec.rb` integration spec
54
+ verifies `/Type /Metadata`, `/Subtype /XML`, pdfaid presence.
68
55
 
69
- ## Dependencies
56
+ ## Acceptance criteria
70
57
 
71
- - pdfrb 0.4.0 `XMP::Packet`, `OutputIntents` (DONE).
72
- - A pdfaid schema model in pdfrb or in `Idml::Render::XmpExtensions`.
73
- - An sRGB ICC profile asset.
74
- - TODO 75 (Lutaml XMP) — same blocker.
58
+ - [x] `--pdf-a` flag triggers XMP packet emission.
59
+ - [x] Packet declares `pdfaid:part` and `pdfaid:conformance`.
60
+ - [x] `dc:format = application/pdf` always present.
61
+ - [x] Catalog `/Lang` set.
62
+ - [x] Packet reuses XMP-extracted fields (TODO 75).
63
+ - [x] XML special characters in field values are escaped.
64
+ - [ ] sRGB ICC profile embedded as `/OutputIntent` (deferred — needs
65
+ binary asset vendoring).
66
+ - [ ] veraPDF compliance run reports zero metadata violations.
@@ -0,0 +1,79 @@
1
+ # TODO PDF 78: Hyperlink annotations
2
+
3
+ ## Status: PLANNED (design only)
4
+
5
+ ## Goal
6
+
7
+ Map IDML `<HyperlinkTextSource>` / `<HyperlinkTextDestination>` /
8
+ `<HyperlinkURLObject>` to PDF Link annotations:
9
+
10
+ - URL hyperlinks → `/Subtype /Link` with `/A /URI` action.
11
+ - Page-item cross-references → `/Subtype /Link` with `/D` destination.
12
+ - Email, file, and text-anchor destinations per the same pattern.
13
+
14
+ Each visible hyperlink becomes a clickable rectangle on the page,
15
+ positioned over the source text range.
16
+
17
+ ## Background
18
+
19
+ IDML models hyperlinks as **text sources** in `Stories/Story_*.xml`
20
+ and **destinations** in `designmap.xml` and across stories:
21
+
22
+ ```xml
23
+ <!-- In Story: a hyperlink source spans a CharacterStyleRange range -->
24
+ <HyperlinkTextSource Self="HyperlinkTextSource/abc" Name="link"
25
+ Visible="true" Highlight="Invert">
26
+ <Properties>
27
+ <TextRange StartIndex="14" EndIndex="22"/>
28
+ </Properties>
29
+ </HyperlinkTextSource>
30
+
31
+ <!-- In designmap.xml or a Story: destinations -->
32
+ <HyperlinkURLObject Self="HyperlinkURLObject/xyz"
33
+ DestinationURL="https://example.com"
34
+ DestinationName="Example"/>
35
+ <HyperlinkPageItemReference Self="..." DestinationPageItem="di1"/>
36
+ ```
37
+
38
+ The source's `TextRange` says "characters 14-22 of this story".
39
+ Combining that with the layout engine's character-position output
40
+ gives the rectangle on the page.
41
+
42
+ ## Plan
43
+
44
+ 1. **Parse hyperlink sources**: extend `Parts::Story` to expose
45
+ `hyperlink_text_source` collection. Same for
46
+ `hyperlink_text_destination`. Element classes already in
47
+ `Idml::Elements` (TODO: add if missing).
48
+ 2. **Parse hyperlink destinations**: extend `Parts::Designmap` to
49
+ expose `hyperlink_url_object` and `hyperlink_destination_page_item`
50
+ collections.
51
+ 3. **Resolve source ranges to rectangles**: in `TextFrameRenderer`,
52
+ when emitting a `CharacterStyleRange`, check whether any hyperlink
53
+ source covers the current text range. If yes, emit the rectangle
54
+ for that range as a Link annotation.
55
+ 4. **Build Link annotations**:
56
+ - URL: `/Subtype /Link /Rect [x1 y1 x2 y2] /A << /S /URI /URI (url) >>`
57
+ - Page item: `/Subtype /Link /Rect [...] /D [page_ref /XYZ x y zoom]`
58
+ 5. **Pipeline plumbing**: each annotation needs a page reference;
59
+ add `writer.add_link_annotation(page_index:, rect:, uri: nil, dest: nil)`.
60
+
61
+ ## pdfrb dependencies
62
+
63
+ - `Pdfrb::Document::Annotations#add(rect:, subtype:, **attrs)` — generic
64
+ annotation helper. Verify presence and signature.
65
+
66
+ ## Acceptance criteria
67
+
68
+ - [ ] URL hyperlinks in IDML render as clickable Link annotations.
69
+ - [ ] Cross-reference links jump to the correct page+position.
70
+ - [ ] Hidden hyperlinks (`Visible="false"`) are skipped.
71
+ - [ ] Spec covers URL, page-item, and invisible cases.
72
+
73
+ ## Dependencies
74
+
75
+ - IDML element classes for hyperlink sources/destinations (mostly
76
+ present — verify and extend as needed).
77
+ - pdfrb Annotations API.
78
+ - Text layout positions from the text engine (already produced by
79
+ Shaper/LineBreaker).
@@ -0,0 +1,72 @@
1
+ # TODO PDF 79: PDF outline (bookmarks)
2
+
3
+ ## Status: PLANNED (design only)
4
+
5
+ ## Goal
6
+
7
+ Map IDML `<Bookmark>` elements in `designmap.xml` to PDF outline
8
+ entries via `PdfrbWriter#add_bookmark` (which delegates to
9
+ `Pdfrb::Document::Outline#add`).
10
+
11
+ Each IDML bookmark references a destination (page or page item) via
12
+ its `Destination` attribute. The PDF outline is the clickable
13
+ "bookmarks" panel in PDF readers — bookmarks make large documents
14
+ navigable.
15
+
16
+ ## Background
17
+
18
+ IDML models bookmarks in `designmap.xml`:
19
+
20
+ ```xml
21
+ <Bookmark Self="Bookmark/abc" Name="Section 1"
22
+ Destination="PDFPageDestination/def"/>
23
+ ```
24
+
25
+ The `Destination` attribute references a `PDFPageDestination_Object`
26
+ (also in designmap.xml) which names the destination page and either
27
+ a viewport setting (`FitView`, `FitWidth`, etc.) or a Y position.
28
+
29
+ pdfrb's Outline API:
30
+
31
+ ```ruby
32
+ document.outline.add(title, dest: page) # add bookmark
33
+ document.outline.add(title, dest: page, parent: parent) # nested
34
+ ```
35
+
36
+ `PdfrbWriter#add_bookmark(title, page_index)` already exists as a
37
+ thin wrapper; this TODO extends it to accept named-destination
38
+ resolution and adds Pipeline integration.
39
+
40
+ ## Plan
41
+
42
+ 1. **Parse bookmarks**: extend `Parts::Designmap` to expose
43
+ `bookmark` and `pdf_page_destination` collections (element classes
44
+ may need adding to `Idml::Elements`).
45
+ 2. **Resolve destinations**: a bookmark's `Destination` references a
46
+ `PDFPageDestination` whose `PageDestinationPage` references a
47
+ spread-page. Build a Self → page_index map at pipeline start.
48
+ 3. **Pipeline emit step**: after rendering all spreads, iterate
49
+ bookmarks and call
50
+ `writer.add_bookmark(name, page_index: page_index_of(destination))`.
51
+ 4. **Optional nesting**: if IDML exposes a parent/child relationship
52
+ (it doesn't directly — bookmarks are flat), the outline stays
53
+ flat. Future work could derive hierarchy from heading styles.
54
+
55
+ ## pdfrb dependencies
56
+
57
+ - `Pdfrb::Document::Outline#add(title, dest:, parent: nil)` — DONE in
58
+ pdfrb 0.4.0. `dest` accepts a page object or reference.
59
+
60
+ ## Acceptance criteria
61
+
62
+ - [ ] IDML bookmarks appear as top-level PDF outline entries.
63
+ - [ ] Clicking a bookmark navigates to the right page.
64
+ - [ ] Bookmarks with unresolvable destinations are skipped (no crash).
65
+ - [ ] Spec covers a fixture with bookmarks + a fixture without.
66
+
67
+ ## Dependencies
68
+
69
+ - Element classes `Idml::Elements::Bookmark`, `PDFPageDestination`
70
+ (TODO: add via rnc_to_lutaml generator).
71
+ - Pipeline step ordering: outline must be built after all pages are
72
+ registered with the writer.