idml 0.2.7 → 0.2.9

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: 6fcb60002b6973119b04d5aef70ec4ae5541c2da5da1d6fb42fcb19288fec86d
4
- data.tar.gz: 86fb08cdace467cf97fedd0cd82bd65820ca1977da5a83255db40ba239273799
3
+ metadata.gz: 192799dde8a7c36a34473f5903934fa17a08c8ea05ae444797099ada2b03ff7c
4
+ data.tar.gz: 9b7659bb74cda7f6d8f9f47df07e2123605baa9df584138d3ef382934f999f60
5
5
  SHA512:
6
- metadata.gz: 38cab95250785926124189618d16e7d63190ffa63824296530292c8c264bc8913fe6038ed39fbd85ca84925252399cdf609cfda54b64530f20d77bb3ac6c7fed
7
- data.tar.gz: bed359cf5365c4a2658f379c7d2a84213c545688993cbe86406bcbf88609b59a992ee46cc96eb21c77ec9142036e7b57bd2c561356502051d2cb15bd71c94b78
6
+ metadata.gz: c113cb7d7a44f198c4c6746cde61f2301733f7f61f2ed5bfe6a3afd145e4446a2e144790916a5bad8106440e0f83b2901dff52e6ff2adf52dedbbe7fb8fcf655
7
+ data.tar.gz: 7960cfe58d64292369cfb23c87a5bcaeec495a7e20f3bbf8c87592040ec04d57630d61ecf7fe2916b1706c142d53c7561d3229c5563216eaf015119d28148a48
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.2.7)
4
+ idml (0.2.9)
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.7)
201
+ idml (0.2.9)
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: BLOCKED (pdfrb has no subsetting API)
4
-
5
- ## Goal
6
-
7
- Subset embedded TrueType fonts to include only the glyphs actually used
8
- in the document. This dramatically reduces PDF file size (from full font
9
- ~500KB to subset ~5-20KB per font).
10
-
11
- ## Blocker
12
-
13
- pdfrb 0.4.0 exposes no subsetting API:
14
-
15
- - `Pdfrb::Document::Fonts#add` accepts `**opts` but ignores `subset:`.
16
- - No `used_codepoints` collector.
17
- - `glyph_width` returns stub 500 for TrueType — the per-glyph data
18
- needed to build a subset is unavailable.
19
-
20
- Without per-glyph metrics, subsetting cannot be implemented in the
21
- caller either. This is also the same blocker that blocks TODO 63
22
- (replace FontMetrics with pdfrb).
23
-
24
- ## Path forward
25
-
26
- Either:
27
-
28
- 1. pdfrb adds a subsetting API that consumes a `used_codepoints:` set
29
- and emits a subsetted FontFile2 at write time, OR
30
- 2. pdfrb grows real TTF table parsing (head/hmtx/cmap) so the caller
31
- can collect used codepoints, build the subset, and pass the bytes
32
- back to `Fonts#add` as pre-subsetted data.
33
-
34
- Once pdfrb unblocks, the idml side needs to:
35
-
36
- 1. Pipeline collects all Unicode codepoints across all stories.
37
- 2. Pipeline passes `used_codepoints: set` to `Fonts#add(subset: true, ...)`.
38
- 3. Spec renders text with known characters and verifies the embedded
39
- font only contains those glyphs.
40
-
41
- ## Acceptance criteria (after pdfrb unblocks)
42
-
43
- - [ ] Pipeline collects all Unicode codepoints used across all stories.
44
- - [ ] For each embedded font, only the used glyphs are included.
45
- - [ ] FontFile2 contains a subsetted font table (not the full font).
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: BLOCKED (pdfrb measurement APIs are stubs)
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`. Eliminates the Fontisan dependency for text layout.
9
+ and `#metrics_for`.
10
10
 
11
- ## Blocker
11
+ ## What pdfrb 0.4.0 provides
12
12
 
13
- pdfrb 0.4.0 ships the API surface but the implementations are stubs
14
- that return constant values, not real per-glyph widths:
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
- `/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb`:
23
+ ## What still needs pdfrb work
17
24
 
18
- ```ruby
19
- def measure_text(text, font:, size:)
20
- return 0 unless text
21
- # TODO: use font metrics for per-glyph width lookup
22
- text.to_s.length * (size || 0).to_f * 0.5
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
- def glyph_width(_char, _resource)
26
- 500
27
- end
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
- def metrics_for(_resource)
30
- nil
31
- end
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
- The Shaper and LineBreaker depend on accurate per-glyph widths for
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
- Fontisan correctly parses head/hhea/hmtx/cmap tables for any TTF/OTF
39
- and exposes per-glyph advance widths. Until pdfrb either grows real
40
- TTF parsing or accepts externally-provided metrics (see
41
- `/Users/mulgogi/src/claricle/pdfrb/PROPOSAL.external-font-metrics.md`),
42
- Fontisan stays.
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 with externally-provided metrics.
65
- - [ ] Shaper/LineBreaker call pdfrb's measurement API, not FontMetrics.
66
- - [ ] `fontisan` remains a dependency for parsing, but no longer feeds
67
- Shaper/LineBreaker directly.
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 0.4.0's `Fonts#measure_text`/`#glyph_width`/`#metrics_for`
72
- with real implementations OR
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 0.19.0 feature integration
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
- instead of N separate `canvas.text` calls per line. Single batch
9
- call for all lines within a run.
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 stubs in `Fonts#measure_text` (returns
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
- 2. **`text_rich` for multi-run text**: tracked in TODO 67. Cannot
18
- advance between runs without real measurement.
19
- 3. **Replace FontMetrics with pdfrb measurement**: tracked in TODO 63.
20
- 4. **Font subsetting**: tracked in TODO 52. `Fonts#add` accepts `**opts`
21
- but `subset:` has no effect.
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 a stub)
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 returns a
16
- stub `length * 0.5 * size`. Without accurate advance, runs overwrite
17
- each other on the line.
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:65`:
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
- # TODO: use font metrics for per-glyph width lookup
27
- text.to_s.length * (size || 0).to_f * 0.5
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
- `glyph_width` (line 79) is also a stub returning 500, and `metrics_for`
32
- (line 87) returns nil. These need real Fontisan-backed implementations
33
- before text_rich produces correct output.
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.text_lines(..., rich: true)`
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 0.4.0 `Fonts#measure_text` returns real per-glyph widths.
51
- - pdfrb 0.4.0 `Fonts#glyph_width` not returning stub 500.
53
+ - pdfrb `Fonts#measure_text` returns real per-glyph widths for TTF
54
+ fonts (currently stub see TODO 63).
@@ -0,0 +1,65 @@
1
+ # TODO PDF 75: PDF metadata enrichment from IDML XMP
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
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` (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
+ - `xmp:CreatorTool` → PDF `/Creator`
15
+ - `xmp:CreateDate` → PDF `/CreationDate`
16
+ - `xmp:ModifyDate` → PDF `/ModDate`
17
+
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
63
+ or individual fields are absent.
64
+ - [x] Spec covers a synthetic XMP packet with all fields populated,
65
+ plus the no-metadata fallback.
@@ -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
 
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Idml
6
+ module Parts
7
+ # XMP (Extensible Metadata Platform) parser for IDML's
8
+ # `META-INF/metadata.xml`. The packet is RDF/XML with multiple
9
+ # namespaces (`dc:`, `xmp:`, `pdf:`, `xmpMM:`, etc.).
10
+ #
11
+ # Per Lutaml's namespace API, each namespaced element is a
12
+ # separate `Serializable` subclass that carries its own
13
+ # `namespace`. The parent `<rdf:Description>` composes the
14
+ # children via `map_element`, with the namespace binding handled
15
+ # on each child class.
16
+ module Xmp
17
+ class RdfNamespace < Lutaml::Xml::Namespace
18
+ uri "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
19
+ prefix_default "rdf"
20
+ end
21
+
22
+ class DcNamespace < Lutaml::Xml::Namespace
23
+ uri "http://purl.org/dc/elements/1.1/"
24
+ prefix_default "dc"
25
+ end
26
+
27
+ class XmpNamespace < Lutaml::Xml::Namespace
28
+ uri "http://ns.adobe.com/xap/1.0/"
29
+ prefix_default "xmp"
30
+ end
31
+
32
+ class XmetaNamespace < Lutaml::Xml::Namespace
33
+ uri "adobe:ns:meta/"
34
+ prefix_default "x"
35
+ end
36
+
37
+ # Single direct-value element. Subclasses set the element name
38
+ # and namespace.
39
+ class Leaf < Lutaml::Model::Serializable
40
+ attribute :value, :string
41
+ end
42
+
43
+ class Format < Leaf
44
+ xml do
45
+ root "format"
46
+ namespace DcNamespace
47
+ map_content to: :value
48
+ end
49
+ end
50
+
51
+ class CreatorTool < Leaf
52
+ xml do
53
+ root "CreatorTool"
54
+ namespace XmpNamespace
55
+ map_content to: :value
56
+ end
57
+ end
58
+
59
+ class CreateDate < Leaf
60
+ xml do
61
+ root "CreateDate"
62
+ namespace XmpNamespace
63
+ map_content to: :value
64
+ end
65
+ end
66
+
67
+ class ModifyDate < Leaf
68
+ xml do
69
+ root "ModifyDate"
70
+ namespace XmpNamespace
71
+ map_content to: :value
72
+ end
73
+ end
74
+
75
+ class MetadataDate < Leaf
76
+ xml do
77
+ root "MetadataDate"
78
+ namespace XmpNamespace
79
+ map_content to: :value
80
+ end
81
+ end
82
+
83
+ # rdf:li element inside rdf:Alt / rdf:Seq / rdf:Bag containers.
84
+ class ListItem < Lutaml::Model::Serializable
85
+ attribute :lang, :string
86
+ attribute :value, :string
87
+
88
+ xml do
89
+ root "li"
90
+ namespace RdfNamespace
91
+ map_attribute "lang", to: :lang
92
+ map_content to: :value
93
+ end
94
+ end
95
+
96
+ # rdf:Alt — alternative-language values. Used for dc:title and
97
+ # dc:description.
98
+ class Alt < Lutaml::Model::Serializable
99
+ attribute :items, ListItem, collection: true
100
+
101
+ xml do
102
+ root "Alt"
103
+ namespace RdfNamespace
104
+ map_element "li", to: :items
105
+ end
106
+
107
+ def default_value
108
+ x_default = items.find { |i| i.lang == "x-default" }
109
+ (x_default || items.first)&.value
110
+ end
111
+ end
112
+
113
+ # rdf:Seq — ordered sequence. Used for dc:creator.
114
+ class Seq < Lutaml::Model::Serializable
115
+ attribute :items, ListItem, collection: true
116
+
117
+ xml do
118
+ root "Seq"
119
+ namespace RdfNamespace
120
+ map_element "li", to: :items
121
+ end
122
+
123
+ def values
124
+ items.filter_map(&:value)
125
+ end
126
+ end
127
+
128
+ # rdf:Bag — unordered set. Used for dc:subject.
129
+ class Bag < Lutaml::Model::Serializable
130
+ attribute :items, ListItem, collection: true
131
+
132
+ xml do
133
+ root "Bag"
134
+ namespace RdfNamespace
135
+ map_element "li", to: :items
136
+ end
137
+
138
+ def values
139
+ items.filter_map(&:value)
140
+ end
141
+ end
142
+
143
+ # Container-wrapped dc elements.
144
+ class Title < Lutaml::Model::Serializable
145
+ attribute :alt, Alt
146
+
147
+ xml do
148
+ root "title"
149
+ namespace DcNamespace
150
+ map_element "Alt", to: :alt
151
+ end
152
+
153
+ def value
154
+ alt&.default_value
155
+ end
156
+ end
157
+
158
+ class DcDescription < Lutaml::Model::Serializable
159
+ attribute :alt, Alt
160
+
161
+ xml do
162
+ root "description"
163
+ namespace DcNamespace
164
+ map_element "Alt", to: :alt
165
+ end
166
+
167
+ def value
168
+ alt&.default_value
169
+ end
170
+ end
171
+
172
+ class Creator < Lutaml::Model::Serializable
173
+ attribute :seq, Seq
174
+
175
+ xml do
176
+ root "creator"
177
+ namespace DcNamespace
178
+ map_element "Seq", to: :seq
179
+ end
180
+
181
+ def values
182
+ seq&.values || []
183
+ end
184
+ end
185
+
186
+ class Subject < Lutaml::Model::Serializable
187
+ attribute :bag, Bag
188
+
189
+ xml do
190
+ root "subject"
191
+ namespace DcNamespace
192
+ map_element "Bag", to: :bag
193
+ end
194
+
195
+ def values
196
+ bag&.values || []
197
+ end
198
+ end
199
+ end
200
+
201
+ # Single `<rdf:Description>` element from an XMP packet. Combines
202
+ # children from multiple namespaces (`dc:`, `xmp:`) by composing
203
+ # nested `Serializable` models — each child carries its own
204
+ # `namespace` declaration per Lutaml's namespace API.
205
+ class XmpDescription < Lutaml::Model::Serializable
206
+ attribute :format, Xmp::Format
207
+ attribute :title, Xmp::Title
208
+ attribute :creator, Xmp::Creator
209
+ attribute :description, Xmp::DcDescription
210
+ attribute :subject, Xmp::Subject
211
+ attribute :creator_tool, Xmp::CreatorTool
212
+ attribute :create_date, Xmp::CreateDate
213
+ attribute :modify_date, Xmp::ModifyDate
214
+ attribute :metadata_date, Xmp::MetadataDate
215
+
216
+ xml do
217
+ root "Description"
218
+ namespace Xmp::RdfNamespace
219
+ map_element "format", to: :format
220
+ map_element "title", to: :title
221
+ map_element "creator", to: :creator
222
+ map_element "description", to: :description
223
+ map_element "subject", to: :subject
224
+ map_element "CreatorTool", to: :creator_tool
225
+ map_element "CreateDate", to: :create_date
226
+ map_element "ModifyDate", to: :modify_date
227
+ map_element "MetadataDate", to: :metadata_date
228
+ end
229
+
230
+ def author
231
+ creator&.values&.first
232
+ end
233
+
234
+ def keywords
235
+ subject&.values&.join(", ")
236
+ end
237
+ end
238
+
239
+ # `<rdf:RDF>` — collection of `<rdf:Description>` siblings inside
240
+ # an XMP packet. Per IDML's META-INF/metadata.xml, there is
241
+ # typically one Description per schema group.
242
+ class XmpRdf < Lutaml::Model::Serializable
243
+ attribute :descriptions, XmpDescription, collection: true
244
+
245
+ xml do
246
+ root "RDF"
247
+ namespace Xmp::RdfNamespace
248
+ map_element "Description", to: :descriptions
249
+ end
250
+
251
+ # Merged view across all Descriptions. Each attribute returns
252
+ # the first non-nil value across the descriptions.
253
+ def first_description
254
+ descriptions.first
255
+ end
256
+
257
+ def title
258
+ descriptions.map(&:title).find(&:itself)&.value
259
+ end
260
+
261
+ def author
262
+ descriptions.filter_map(&:author).first
263
+ end
264
+
265
+ def subject
266
+ descriptions.filter_map(&:subject).find(&:itself)&.values
267
+ end
268
+
269
+ def keywords
270
+ subject&.join(", ")
271
+ end
272
+
273
+ def description
274
+ descriptions.map(&:description).find(&:itself)&.value
275
+ end
276
+
277
+ def creator_tool
278
+ descriptions.filter_map(&:creator_tool).first&.value
279
+ end
280
+
281
+ def create_date
282
+ descriptions.filter_map(&:create_date).first&.value
283
+ end
284
+
285
+ def modify_date
286
+ descriptions.filter_map(&:modify_date).first&.value
287
+ end
288
+ end
289
+
290
+ # `<x:xmpmeta>` — outer wrapper of an XMP packet.
291
+ class XmpMeta < Lutaml::Model::Serializable
292
+ attribute :rdf, XmpRdf
293
+
294
+ xml do
295
+ root "xmpmeta"
296
+ namespace Xmp::XmetaNamespace
297
+ map_element "RDF", to: :rdf
298
+ end
299
+
300
+ def description
301
+ rdf&.first_description
302
+ end
303
+ end
304
+ end
305
+ end
data/lib/idml/parts.rb CHANGED
@@ -15,6 +15,10 @@ module Idml
15
15
  autoload :Preferences, "idml/parts/preferences"
16
16
  autoload :Tags, "idml/parts/tags"
17
17
  autoload :Mapping, "idml/parts/mapping"
18
+ autoload :Xmp, "idml/parts/xmp"
19
+ autoload :XmpDescription, "idml/parts/xmp"
20
+ autoload :XmpRdf, "idml/parts/xmp"
21
+ autoload :XmpMeta, "idml/parts/xmp"
18
22
 
19
23
  @registry = {}
20
24
 
@@ -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 xref, trailer,
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
- meta.public_send(setter, value.to_s) if setter
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&.key?(uri) ? @image_cache[uri] : nil
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
@@ -10,17 +10,18 @@ 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
22
23
  writer = PdfrbWriter.new
23
- writer.set_info(default_metadata)
24
+ writer.set_info(combined_metadata)
24
25
  writer.enable_tagged if @tagged
25
26
  layer_filter = LayerFilter.from_designmap(@package.designmap)
26
27
  font_ref_resolver = FontReferenceResolver.build(@package)
@@ -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
@@ -188,6 +190,46 @@ module Idml
188
190
  nil
189
191
  end
190
192
 
193
+ def combined_metadata
194
+ defaults = default_metadata
195
+ xmp_metadata.each do |key, value|
196
+ next if value.nil? || value.empty?
197
+
198
+ defaults[key] = value
199
+ end
200
+ defaults
201
+ end
202
+
203
+ def xmp_metadata
204
+ return {} unless @package.has_part?(XMP_PATH)
205
+
206
+ xml = @package.read_part(XMP_PATH)
207
+ meta = Parts::XmpMeta.from_xml(xml)
208
+ rdf = meta.rdf
209
+ return {} unless rdf
210
+
211
+ {
212
+ Title: rdf.title,
213
+ Author: rdf.author,
214
+ Subject: rdf.description,
215
+ Keywords: rdf.keywords,
216
+ Creator: rdf.creator_tool,
217
+ CreationDate: pdf_date_string(rdf.create_date),
218
+ ModDate: pdf_date_string(rdf.modify_date),
219
+ }
220
+ rescue StandardError
221
+ {}
222
+ end
223
+
224
+ def pdf_date_string(iso8601)
225
+ return nil unless iso8601
226
+
227
+ time = Time.iso8601(iso8601)
228
+ pdf_date(time)
229
+ rescue ArgumentError
230
+ nil
231
+ end
232
+
191
233
  def default_metadata
192
234
  {
193
235
  Producer: "idml gem v#{Idml::VERSION}",
@@ -198,6 +240,9 @@ module Idml
198
240
  def pdf_date(time)
199
241
  time.strftime("D:%Y%m%d%H%M%S+00'00'")
200
242
  end
243
+
244
+ XMP_PATH = "META-INF/metadata.xml"
245
+ private_constant :XMP_PATH
201
246
  end
202
247
  end
203
248
  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, compliance: compliance,
31
- tagged: tagged).call
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.9"
5
5
  end
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.7
4
+ version: 0.2.9
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
@@ -378,6 +381,7 @@ files:
378
381
  - lib/idml/parts/style.rb
379
382
  - lib/idml/parts/style_mapping.rb
380
383
  - lib/idml/parts/tags.rb
384
+ - lib/idml/parts/xmp.rb
381
385
  - lib/idml/render.rb
382
386
  - lib/idml/render/blending.rb
383
387
  - lib/idml/render/color_helper.rb