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 +4 -4
- data/Gemfile.lock +4 -4
- data/TODO.pdf/75-pdf-metadata-from-xmp.md +52 -90
- data/TODO.pdf/76-tagged-pdf-structure.md +63 -87
- data/TODO.pdf/77-pdfa-xmp-output-intent.md +51 -59
- data/TODO.pdf/78-hyperlinks.md +79 -0
- data/TODO.pdf/79-bookmarks-outline.md +72 -0
- data/lib/idml/parts/xmp.rb +305 -0
- data/lib/idml/parts.rb +4 -0
- data/lib/idml/render/image_collector.rb +107 -0
- data/lib/idml/render/page_item_renderer.rb +18 -1
- data/lib/idml/render/pdfa_packet.rb +135 -0
- data/lib/idml/render/pdfrb_writer.rb +2 -1
- data/lib/idml/render/pipeline.rb +69 -90
- data/lib/idml/render/render_context.rb +2 -0
- data/lib/idml/render/spread_renderer.rb +6 -2
- data/lib/idml/render/structure_mapper.rb +54 -0
- data/lib/idml/render/structure_tracker.rb +55 -0
- data/lib/idml/render.rb +4 -0
- data/lib/idml/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ae5922e2f513147b0e73fb9ba724c7b3a075cfc75da73cac26172029dc1bc8f
|
|
4
|
+
data.tar.gz: 689c47f46262b6b3df737771fd2e9b4e9e5ab4596e272a60e37c902844f2bd78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
3
|
+
## Status: DONE
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
- [
|
|
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:
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
- [
|
|
78
|
-
whose `/StructTreeRoot` has
|
|
79
|
-
- [
|
|
80
|
-
right type (Figure
|
|
81
|
-
- [
|
|
82
|
-
- [
|
|
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
|
|
1
|
+
# TODO PDF 77: PDF/A XMP packet and Catalog /Metadata stream
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: PARTIAL — XMP packet emitted; ICC output intent deferred
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What was implemented
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
## What remains deferred
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
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
|
-
##
|
|
56
|
+
## Acceptance criteria
|
|
70
57
|
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
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.
|