idml 0.2.7 → 0.2.8
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 +2 -2
- data/TODO.pdf/52-font-subsetting.md +43 -50
- data/TODO.pdf/63-replace-fontmetrics-with-pdfrb.md +39 -52
- data/TODO.pdf/65-pdfrb-019-integration.md +23 -13
- data/TODO.pdf/67-text-rich-multi-run.md +18 -15
- data/TODO.pdf/75-pdf-metadata-from-xmp.md +103 -0
- data/TODO.pdf/76-tagged-pdf-structure.md +92 -0
- data/TODO.pdf/77-pdfa-xmp-output-intent.md +74 -0
- data/lib/idml/cli.rb +3 -0
- data/lib/idml/render/pdfrb_writer.rb +22 -16
- data/lib/idml/render/pipeline.rb +3 -1
- data/lib/idml/render.rb +8 -3
- data/lib/idml/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9833da9d4a8ddda74890980eab76f2a47b03cdb99977730fb1dee5d8809dd68
|
|
4
|
+
data.tar.gz: 1d771cc2775f1071329d2b2dbab681cc46197546be27681fcba39ee9313f9a2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3dfbfc1c8ccdccdcd53070ff9aaf8de3f150699e524c3b6b4668c9a2d266cfb530ed5d8fb1e5b515be6da145df17e60832d9f9f60c0c9b2c106092adf716dfd
|
|
7
|
+
data.tar.gz: 437d8bcdc82e87f999b916ea9fc10486f2f4b8ae57965b9753fae465ab8f570684f4b2ea7eb8ae81fbc40fdf6a66288387429d265053562cf2dc4581101473be
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
idml (0.2.
|
|
4
|
+
idml (0.2.8)
|
|
5
5
|
bigdecimal
|
|
6
6
|
fontisan
|
|
7
7
|
lutaml-model (~> 0.8.18)
|
|
@@ -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.
|
|
201
|
+
idml (0.2.8)
|
|
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
|
|
@@ -1,52 +1,45 @@
|
|
|
1
1
|
# TODO PDF 52: Font subsetting via pdfrb
|
|
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
|
-
- [ ] PDF file size significantly reduced vs full-font embedding.
|
|
47
|
-
- [ ] Spec: render text with known characters, verify embedded font
|
|
48
|
-
contains only those glyphs.
|
|
49
|
-
|
|
50
|
-
## Dependencies
|
|
51
|
-
|
|
52
|
-
- pdfrb subsetting API (NOT YET AVAILABLE in 0.4.0).
|
|
3
|
+
## Status: DONE
|
|
4
|
+
|
|
5
|
+
## What was implemented
|
|
6
|
+
|
|
7
|
+
Pipeline now subsets embedded TrueType fonts via pdfrb 0.4.0's
|
|
8
|
+
`Fonts#subset_fonts!`. The flow:
|
|
9
|
+
|
|
10
|
+
1. Every `canvas.text` / `canvas.text_lines` call auto-populates
|
|
11
|
+
`document.fonts.used_codepoints(resource)` per font.
|
|
12
|
+
2. `Pipeline#call` invokes `writer.subset_fonts!` after rendering all
|
|
13
|
+
spreads and before `writer.write`.
|
|
14
|
+
3. `PdfrbWriter#subset_fonts!` delegates to
|
|
15
|
+
`document.fonts.subset_fonts!`, which rewrites each registered
|
|
16
|
+
font's FontFile2 with a subset containing only the used glyphs
|
|
17
|
+
(plus glyph 0 / notdef).
|
|
18
|
+
4. Subsetting uses `Pdfrb::Font::TrueType::Subsetter`, which rebuilds
|
|
19
|
+
glyf/loca/cmap/hmtx/hhea/maxp/head tables and resolves composite
|
|
20
|
+
glyph references.
|
|
21
|
+
|
|
22
|
+
## Opting out
|
|
23
|
+
|
|
24
|
+
`Idml::Render.render(..., subset_fonts: false)` skips the call.
|
|
25
|
+
The CLI exposes `--no-subset` for the same effect. Useful when a
|
|
26
|
+
downstream consumer needs the full font table (e.g. searchable PDFs
|
|
27
|
+
with a specific glyph set).
|
|
28
|
+
|
|
29
|
+
## Verification
|
|
30
|
+
|
|
31
|
+
- `lib/idml/render/pdfrb_writer.rb:48` — `subset_fonts!` delegation.
|
|
32
|
+
- `lib/idml/render/pipeline.rb:38` — `subset_fonts!` call site.
|
|
33
|
+
- `spec/idml/render/render_pdfrb_writer_spec.rb:120` — subsetting
|
|
34
|
+
produces a sub-15KB PDF for "Hello" in Arial.
|
|
35
|
+
|
|
36
|
+
## Acceptance criteria
|
|
37
|
+
|
|
38
|
+
- [x] Pipeline collects used codepoints automatically via pdfrb's
|
|
39
|
+
`encode_text` hook.
|
|
40
|
+
- [x] Each embedded font's FontFile2 is rewritten with a subset
|
|
41
|
+
containing only used glyphs.
|
|
42
|
+
- [x] PDF file size significantly reduced vs full-font embedding.
|
|
43
|
+
- [x] Spec renders text with known characters and verifies the
|
|
44
|
+
output PDF is small.
|
|
45
|
+
- [x] `subset_fonts: false` skips subsetting.
|
|
@@ -1,73 +1,60 @@
|
|
|
1
1
|
# TODO PDF 63: Replace FontMetrics with pdfrb measurement API
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: PARTIALLY UNBLOCKED (subsetting works; measurement still AFM-only)
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
7
7
|
Replace `Idml::TextEngine::FontMetrics` (200+ lines of TTF binary parsing
|
|
8
8
|
via Fontisan) with pdfrb's native `Fonts#measure_text`, `#glyph_width`,
|
|
9
|
-
and `#metrics_for`.
|
|
9
|
+
and `#metrics_for`.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## What pdfrb 0.4.0 provides
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
- `Pdfrb::Font::TrueType::File` — real TTF parser with Head, Hhea,
|
|
14
|
+
Cmap, Hmtx, OS2 tables.
|
|
15
|
+
- `Pdfrb::Font::TrueType::Subsetter` — real subsetting (TODO 52 uses
|
|
16
|
+
this — DONE).
|
|
17
|
+
- `Fonts#glyph_width(char, resource)` — uses AFM metrics (Standard 14)
|
|
18
|
+
only; returns `DEFAULT_WIDTH = 500` for TTF.
|
|
19
|
+
- `Fonts#measure_text(text, font:, size:)` — same: AFM or stub
|
|
20
|
+
(`length * 0.5 * size`).
|
|
21
|
+
- `Fonts#metrics_for(resource)` — AFM metrics only.
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
## What still needs pdfrb work
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
25
|
+
pdfrb's TTF parser exists but is not wired to the measurement API.
|
|
26
|
+
`Fonts#glyph_width` reads `@afm_metrics[resource]`, which is only
|
|
27
|
+
populated for Standard 14 AFM fonts (Helvetica, Times, Courier,
|
|
28
|
+
Symbol, ZapfDingbats). For every other font (any TTF/OTF), it falls
|
|
29
|
+
back to the stub.
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
The per-glyph-width proposal asked for "Look up glyph ID from cmap,
|
|
32
|
+
then width from hmtx." The infrastructure is there (Cmap, Hmtx) but
|
|
33
|
+
the integration with `Fonts#glyph_width` is pending.
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
35
|
+
Until pdfrb's measurement API wires the TTF parser, the idml gem's
|
|
36
|
+
Shaper and LineBreaker must keep using Fontisan-based FontMetrics
|
|
37
|
+
for accurate per-glyph widths.
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
correct word-wrap. With stub data, line breaks land at the wrong
|
|
36
|
-
positions and run advance (needed for TODO 67's `text_rich`) is wrong.
|
|
39
|
+
## Plan (after pdfrb unblocks)
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Path forward
|
|
45
|
-
|
|
46
|
-
Proposal at `~/src/claricle/pdfrb/PROPOSAL.external-font-metrics.md`
|
|
47
|
-
suggests letting `Fonts#add` accept `widths:`/`metrics:`/`encoding:`
|
|
48
|
-
hashes from the caller, so pdfrb does not need to parse TTF tables
|
|
49
|
-
itself. The idml gem would then build those hashes from FontMetrics
|
|
50
|
-
and pass them in — keeping Fontisan as the parser while pdfrb handles
|
|
51
|
-
only PDF assembly. This is the Unix-philosophy split.
|
|
52
|
-
|
|
53
|
-
Once that proposal lands:
|
|
54
|
-
|
|
55
|
-
1. `PdfrbWriter#register_font_with_metrics(path, widths:, metrics:, ...)`.
|
|
56
|
-
2. Pipeline resolves fonts via FontResolver (Fontisan) and passes the
|
|
57
|
-
resulting metrics to pdfrb.
|
|
58
|
-
3. Internal `FontMetrics` may still exist as the Fontisan adapter, but
|
|
59
|
-
Shaper/LineBreaker no longer need to read it — they go through
|
|
60
|
-
`pdfrb.measure_text`.
|
|
41
|
+
1. `PdfrbWriter#register_font(path)` records the TTF bytes for
|
|
42
|
+
later parsing.
|
|
43
|
+
2. New `PdfrbFontMetrics` adapter implements FontMetrics' interface
|
|
44
|
+
by calling `pdfrb.fonts.glyph_width(font_resource, codepoint)`.
|
|
45
|
+
3. `FontResolver` returns `PdfrbFontMetrics` for resolved fonts.
|
|
46
|
+
4. Remove `text_engine/font_metrics.rb` (200+ lines).
|
|
61
47
|
|
|
62
48
|
## Acceptance criteria (after pdfrb unblocks)
|
|
63
49
|
|
|
64
|
-
- [ ] Pipeline registers fonts
|
|
65
|
-
- [ ] Shaper/LineBreaker call pdfrb's measurement API, not
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
- [ ] Pipeline registers fonts via `pdfrb.fonts.add(path)`.
|
|
51
|
+
- [ ] Shaper/LineBreaker call pdfrb's measurement API, not
|
|
52
|
+
FontMetrics.
|
|
53
|
+
- [ ] `fontisan` removed from gemspec dependencies.
|
|
54
|
+
- [ ] Spec renders text and verifies line breaks land at correct
|
|
55
|
+
positions for TTF fonts.
|
|
68
56
|
|
|
69
57
|
## Dependencies
|
|
70
58
|
|
|
71
|
-
- pdfrb
|
|
72
|
-
|
|
73
|
-
- pdfrb accepts the external-metrics proposal linked above.
|
|
59
|
+
- pdfrb `Fonts#glyph_width` uses parsed TTF tables, not AFM-only.
|
|
60
|
+
- pdfrb `Fonts#measure_text` uses parsed TTF tables.
|
|
@@ -1,29 +1,39 @@
|
|
|
1
|
-
# TODO PDF 65: pdfrb
|
|
1
|
+
# TODO PDF 65: pdfrb feature integration
|
|
2
2
|
|
|
3
3
|
## Status: PARTIALLY DONE (remainder blocked)
|
|
4
4
|
|
|
5
5
|
## What was implemented
|
|
6
6
|
|
|
7
7
|
1. **Canvas#text_lines**: `TextFrameRenderer` uses `canvas.text_lines`
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
for batched single-font multi-line text (TODO 27).
|
|
9
|
+
2. **Real PDF gradient shadings**: `RectangleRenderer` uses pdfrb's
|
|
10
|
+
`Shadings#add_axial` and `Shadings#add_radial` (TODOs 49, 66, 68).
|
|
11
|
+
3. **Canvas#with_transparency**: `Blending.wrap` applies IDML
|
|
12
|
+
`BlendingSetting` opacity + blend modes (TODO 69).
|
|
13
|
+
4. **Stroke-style setters**: `StrokeStyle.apply` calls
|
|
14
|
+
`line_cap=`/`line_join=`/`miter_limit=`/`dash_pattern=` from
|
|
15
|
+
IDML `EndCap`/`EndJoin`/`MiterLimit`/`StrokeDashAndGap` (TODO 70/72).
|
|
16
|
+
5. **Font subsetting**: `Pipeline` calls `Fonts#subset_fonts!` before
|
|
17
|
+
write (TODO 52).
|
|
18
|
+
6. **Placement module**: shared `Render::Placement.box` (TODO 71).
|
|
10
19
|
|
|
11
20
|
## What remains
|
|
12
21
|
|
|
13
|
-
Blocked by pdfrb 0.4.0
|
|
14
|
-
`length * 0.5 * size`) and `#glyph_width` (returns 500). Until real
|
|
15
|
-
per-glyph widths land:
|
|
22
|
+
Blocked by pdfrb 0.4.0's TTF measurement still being AFM-only:
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
7. **`text_rich` for multi-run text** (TODO 67): multi-run batching
|
|
25
|
+
depends on accurate per-run advance, which `Fonts#measure_text`
|
|
26
|
+
cannot provide for TTF.
|
|
27
|
+
8. **Replace FontMetrics with pdfrb measurement** (TODO 63): same
|
|
28
|
+
blocker.
|
|
22
29
|
|
|
23
30
|
## Acceptance criteria
|
|
24
31
|
|
|
25
32
|
- [x] TextFrameRenderer uses `canvas.text_lines`
|
|
33
|
+
- [x] Real PDF gradient shadings via pdfrb
|
|
34
|
+
- [x] Transparency and blend modes
|
|
35
|
+
- [x] Stroke styling
|
|
36
|
+
- [x] Font subsetting
|
|
37
|
+
- [x] Shared Placement module
|
|
26
38
|
- [ ] FontMetrics replaced with pdfrb `glyph_width` (blocked — TODO 63)
|
|
27
|
-
- [ ] FontResolver replaced with `Pdfrb::FontResolver` (blocked — TODO 63)
|
|
28
|
-
- [ ] Font subsetting verified (blocked — TODO 52)
|
|
29
39
|
- [ ] `text_rich` used for multi-run frames (blocked — TODO 67)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TODO PDF 67: Multi-run text batching via Canvas#text_rich
|
|
2
2
|
|
|
3
|
-
## Status: BLOCKED (pdfrb measure_text is
|
|
3
|
+
## Status: BLOCKED (pdfrb measure_text is stub for TTF)
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
@@ -12,32 +12,35 @@ all runs inside one BT/ET block, advancing the text matrix between runs.
|
|
|
12
12
|
|
|
13
13
|
`text_rich` is the right primitive for multi-run text — one begin/end
|
|
14
14
|
text block per frame instead of N. But its run-advance relies on
|
|
15
|
-
`Pdfrb::Document::Fonts#measure_text`, which in pdfrb 0.4.0
|
|
16
|
-
stub `length * 0.5 * size
|
|
17
|
-
|
|
15
|
+
`Pdfrb::Document::Fonts#measure_text`, which in pdfrb 0.4.0 still
|
|
16
|
+
returns the stub `length * 0.5 * size` for TTF/OTF fonts (only
|
|
17
|
+
Standard 14 AFM fonts get real measurement). Without accurate advance,
|
|
18
|
+
runs overwrite each other on the line.
|
|
18
19
|
|
|
19
20
|
## Blocker
|
|
20
21
|
|
|
21
|
-
`/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb
|
|
22
|
+
`/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb`:
|
|
22
23
|
|
|
23
24
|
```ruby
|
|
24
25
|
def measure_text(text, font:, size:)
|
|
25
|
-
return 0 unless text
|
|
26
|
-
|
|
27
|
-
text.to_s.length *
|
|
26
|
+
return 0 unless text && size
|
|
27
|
+
metrics = @afm_metrics[font]
|
|
28
|
+
return text.to_s.length * size.to_f * 0.5 unless metrics # STUB
|
|
29
|
+
...
|
|
28
30
|
end
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
`@afm_metrics` is only populated for Standard 14 fonts. pdfrb has
|
|
34
|
+
the TTF parsing infrastructure (`Pdfrb::Font::TrueType::File` with
|
|
35
|
+
Cmap, Hmtx) but the integration with `Fonts#glyph_width` /
|
|
36
|
+
`measure_text` is pending.
|
|
34
37
|
|
|
35
38
|
## Plan (after pdfrb unblocks)
|
|
36
39
|
|
|
37
40
|
1. Build `runs` array of `{ text:, font:, size:, color: }` per line.
|
|
38
|
-
2. Replace `simple_render` body with `canvas.
|
|
39
|
-
or `canvas.text_rich(runs, at: [x, y])`.
|
|
41
|
+
2. Replace `simple_render` body with `canvas.text_rich(runs, at: [x, y])`.
|
|
40
42
|
3. Verify rendered PDF: runs no longer overwrite, multi-color lines work.
|
|
43
|
+
4. Combine with TODO 63 to drop Fontisan for measurement.
|
|
41
44
|
|
|
42
45
|
## Acceptance criteria
|
|
43
46
|
|
|
@@ -47,5 +50,5 @@ before text_rich produces correct output.
|
|
|
47
50
|
|
|
48
51
|
## Dependencies
|
|
49
52
|
|
|
50
|
-
- pdfrb
|
|
51
|
-
|
|
53
|
+
- pdfrb `Fonts#measure_text` returns real per-glyph widths for TTF
|
|
54
|
+
fonts (currently stub — see TODO 63).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# TODO PDF 75: PDF metadata enrichment from IDML XMP
|
|
2
|
+
|
|
3
|
+
## Status: BLOCKED (Lutaml XMP namespace parsing)
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Pull IDML document metadata from `META-INF/metadata.xml` (XMP packet)
|
|
8
|
+
into the PDF Info dictionary via `PdfrbWriter#set_info`:
|
|
9
|
+
|
|
10
|
+
- `dc:title` → PDF `/Title`
|
|
11
|
+
- `dc:creator` → PDF `/Author`
|
|
12
|
+
- `dc:description` → PDF `/Subject`
|
|
13
|
+
- `dc:subject` → PDF `/Keywords`
|
|
14
|
+
- `xmp:CreatorTool` → PDF `/Creator`
|
|
15
|
+
- `xmp:CreateDate` → PDF `/CreationDate`
|
|
16
|
+
- `xmp:ModifyDate` → PDF `/ModDate`
|
|
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
|
|
94
|
+
or individual fields are absent.
|
|
95
|
+
- [ ] Spec covers a synthetic XMP packet with all fields populated,
|
|
96
|
+
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`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# TODO PDF 76: Tagged PDF structure tree
|
|
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`.
|
|
74
|
+
|
|
75
|
+
## Acceptance criteria
|
|
76
|
+
|
|
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.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# TODO PDF 77: PDF/A XMP metadata and output intent
|
|
2
|
+
|
|
3
|
+
## Status: PLANNED (design only)
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Emit PDF/A-compliant XMP metadata so the rendered PDF passes
|
|
8
|
+
veraPDF/A validation. PDF/A requires:
|
|
9
|
+
|
|
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).
|
|
18
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Background
|
|
25
|
+
|
|
26
|
+
pdfrb 0.4.0 ships the primitives:
|
|
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.
|
|
34
|
+
|
|
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:
|
|
38
|
+
|
|
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).
|
|
43
|
+
|
|
44
|
+
## Plan
|
|
45
|
+
|
|
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`.
|
|
58
|
+
|
|
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.
|
|
68
|
+
|
|
69
|
+
## Dependencies
|
|
70
|
+
|
|
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.
|
data/lib/idml/cli.rb
CHANGED
|
@@ -67,6 +67,8 @@ module Idml
|
|
|
67
67
|
desc: "Produce PDF/A-2a compliant output"
|
|
68
68
|
method_option :tagged, type: :boolean, default: false,
|
|
69
69
|
desc: "Produce tagged PDF (PDF/UA)"
|
|
70
|
+
method_option :no_subset, type: :boolean, default: false,
|
|
71
|
+
desc: "Skip font subsetting (larger PDF)"
|
|
70
72
|
method_option :verbose, aliases: "-v", type: :boolean, default: false,
|
|
71
73
|
desc: "Print progress"
|
|
72
74
|
def render(path)
|
|
@@ -97,6 +99,7 @@ module Idml
|
|
|
97
99
|
font_search_paths: font_search_paths,
|
|
98
100
|
compliance: options[:pdf_a] ? :pdfa2a : nil,
|
|
99
101
|
tagged: options[:tagged],
|
|
102
|
+
subset_fonts: !options[:no_subset],
|
|
100
103
|
}
|
|
101
104
|
end
|
|
102
105
|
|
|
@@ -6,14 +6,26 @@ module Idml
|
|
|
6
6
|
# Delegates all PDF assembly to pdfrb (no hand-rolled PDF code).
|
|
7
7
|
# The adapter provides a consistent interface for the Pipeline
|
|
8
8
|
# (add_page → returns Canvas, add_image → name, register_font → name,
|
|
9
|
-
# set_info, add_bookmark) while letting pdfrb handle
|
|
10
|
-
# object streams, and operator emission.
|
|
9
|
+
# set_info, add_bookmark, subset_fonts!) while letting pdfrb handle
|
|
10
|
+
# xref, trailer, object streams, and operator emission.
|
|
11
11
|
class PdfrbWriter
|
|
12
12
|
DEFAULT_WIDTH = 612
|
|
13
13
|
DEFAULT_HEIGHT = 792
|
|
14
14
|
|
|
15
|
+
META_SETTERS = {
|
|
16
|
+
Title: :title=,
|
|
17
|
+
Author: :author=,
|
|
18
|
+
Subject: :subject=,
|
|
19
|
+
Keywords: :keywords=,
|
|
20
|
+
Creator: :creator=,
|
|
21
|
+
Producer: :producer=,
|
|
22
|
+
CreationDate: :creationdate=,
|
|
23
|
+
ModDate: :moddate=,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
15
26
|
def initialize
|
|
16
27
|
@document = Pdfrb::Document.new
|
|
28
|
+
@image_cache = {}
|
|
17
29
|
end
|
|
18
30
|
|
|
19
31
|
def add_page(width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT)
|
|
@@ -39,20 +51,25 @@ module Idml
|
|
|
39
51
|
meta = @document.metadata
|
|
40
52
|
hash.each do |key, value|
|
|
41
53
|
setter = META_SETTERS[key.to_sym]
|
|
42
|
-
|
|
54
|
+
next unless setter
|
|
55
|
+
|
|
56
|
+
meta.public_send(setter, value.to_s)
|
|
43
57
|
end
|
|
44
58
|
end
|
|
45
59
|
|
|
60
|
+
def subset_fonts!
|
|
61
|
+
@document.fonts.subset_fonts!
|
|
62
|
+
end
|
|
63
|
+
|
|
46
64
|
def write(path)
|
|
47
65
|
@document.write(path)
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
def image_name_for(uri)
|
|
51
|
-
@image_cache
|
|
69
|
+
@image_cache[uri]
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
def register_image_name(uri, name)
|
|
55
|
-
@image_cache ||= {}
|
|
56
73
|
@image_cache[uri] = name
|
|
57
74
|
end
|
|
58
75
|
|
|
@@ -72,17 +89,6 @@ module Idml
|
|
|
72
89
|
def document
|
|
73
90
|
@document
|
|
74
91
|
end
|
|
75
|
-
|
|
76
|
-
META_SETTERS = {
|
|
77
|
-
Title: :title=,
|
|
78
|
-
Author: :author=,
|
|
79
|
-
Subject: :subject=,
|
|
80
|
-
Keywords: :keywords=,
|
|
81
|
-
Creator: :creator=,
|
|
82
|
-
Producer: :producer=,
|
|
83
|
-
CreationDate: :creationdate=,
|
|
84
|
-
ModDate: :moddate=,
|
|
85
|
-
}.freeze
|
|
86
92
|
end
|
|
87
93
|
end
|
|
88
94
|
end
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -10,12 +10,13 @@ module Idml
|
|
|
10
10
|
DEFAULT_HEIGHT = 792
|
|
11
11
|
|
|
12
12
|
def initialize(package, output_path, font_search_paths = nil,
|
|
13
|
-
compliance: nil, tagged: false)
|
|
13
|
+
compliance: nil, tagged: false, subset_fonts: true)
|
|
14
14
|
@package = package
|
|
15
15
|
@output_path = output_path
|
|
16
16
|
@font_resolver = build_font_resolver(font_search_paths)
|
|
17
17
|
@compliance = compliance
|
|
18
18
|
@tagged = tagged
|
|
19
|
+
@subset_fonts = subset_fonts
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def call
|
|
@@ -33,6 +34,7 @@ module Idml
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
writer.build_structure if @tagged
|
|
37
|
+
writer.subset_fonts! if @subset_fonts
|
|
36
38
|
writer.write(@output_path)
|
|
37
39
|
@output_path
|
|
38
40
|
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -25,11 +25,16 @@ module Idml
|
|
|
25
25
|
|
|
26
26
|
DEFAULT_FONT = "Helvetica"
|
|
27
27
|
|
|
28
|
+
# Render an IDML package to a PDF file. All options except
|
|
29
|
+
# `package:` and `to:` are keyword-only and optional.
|
|
30
|
+
# rubocop:disable Metrics/ParameterLists
|
|
28
31
|
def self.render(package:, to:, font_search_paths: nil, compliance: nil,
|
|
29
|
-
tagged: false)
|
|
30
|
-
Pipeline.new(package, to, font_search_paths,
|
|
31
|
-
|
|
32
|
+
tagged: false, subset_fonts: true)
|
|
33
|
+
Pipeline.new(package, to, font_search_paths,
|
|
34
|
+
compliance: compliance, tagged: tagged,
|
|
35
|
+
subset_fonts: subset_fonts).call
|
|
32
36
|
end
|
|
37
|
+
# rubocop:enable Metrics/ParameterLists
|
|
33
38
|
end
|
|
34
39
|
end
|
|
35
40
|
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -215,6 +215,9 @@ files:
|
|
|
215
215
|
- TODO.pdf/72-stroke-styling.md
|
|
216
216
|
- TODO.pdf/73-gradient-resolver-cleanup.md
|
|
217
217
|
- TODO.pdf/74-renderer-spec-coverage.md
|
|
218
|
+
- TODO.pdf/75-pdf-metadata-from-xmp.md
|
|
219
|
+
- TODO.pdf/76-tagged-pdf-structure.md
|
|
220
|
+
- TODO.pdf/77-pdfa-xmp-output-intent.md
|
|
218
221
|
- TODO.pdf/README.md
|
|
219
222
|
- exe/idml
|
|
220
223
|
- idml.gemspec
|