idml 0.2.8 → 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: e9833da9d4a8ddda74890980eab76f2a47b03cdb99977730fb1dee5d8809dd68
4
- data.tar.gz: 1d771cc2775f1071329d2b2dbab681cc46197546be27681fcba39ee9313f9a2b
3
+ metadata.gz: 192799dde8a7c36a34473f5903934fa17a08c8ea05ae444797099ada2b03ff7c
4
+ data.tar.gz: 9b7659bb74cda7f6d8f9f47df07e2123605baa9df584138d3ef382934f999f60
5
5
  SHA512:
6
- metadata.gz: e3dfbfc1c8ccdccdcd53070ff9aaf8de3f150699e524c3b6b4668c9a2d266cfb530ed5d8fb1e5b515be6da145df17e60832d9f9f60c0c9b2c106092adf716dfd
7
- data.tar.gz: 437d8bcdc82e87f999b916ea9fc10486f2f4b8ae57965b9753fae465ab8f570684f4b2ea7eb8ae81fbc40fdf6a66288387429d265053562cf2dc4581101473be
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.8)
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.8)
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,103 +1,65 @@
1
1
  # TODO PDF 75: PDF metadata enrichment from IDML XMP
2
2
 
3
- ## Status: BLOCKED (Lutaml XMP namespace parsing)
3
+ ## Status: DONE
4
4
 
5
- ## Goal
5
+ ## What was implemented
6
6
 
7
7
  Pull IDML document metadata from `META-INF/metadata.xml` (XMP packet)
8
8
  into the PDF Info dictionary via `PdfrbWriter#set_info`:
9
9
 
10
- - `dc:title` → PDF `/Title`
11
- - `dc:creator` → PDF `/Author`
12
- - `dc:description` → PDF `/Subject`
13
- - `dc:subject` → PDF `/Keywords`
10
+ - `dc:title` (rdf:Alt x-default) → PDF `/Title`
11
+ - `dc:creator` (rdf:Seq first li) → PDF `/Author`
12
+ - `dc:description` (rdf:Alt x-default) → PDF `/Subject`
13
+ - `dc:subject` (rdf:Bag joined) → PDF `/Keywords`
14
14
  - `xmp:CreatorTool` → PDF `/Creator`
15
15
  - `xmp:CreateDate` → PDF `/CreationDate`
16
16
  - `xmp:ModifyDate` → PDF `/ModDate`
17
17
 
18
- ## Blocker
19
-
20
- `META-INF/metadata.xml` is an XMP packet RDF/XML with multiple XML
21
- namespaces (`dc:`, `xmp:`, `pdf:`, `xmpMM:`, `stFnt:`, etc.). Parsing
22
- it with `lutaml-model` requires namespace-aware element mapping.
23
-
24
- Lutaml 0.8.19's namespace API is in flux:
25
-
26
- - `namespace "..."` with a URI string raises
27
- `String namespace URIs are not supported. Define an XmlNamespace
28
- class instead.`
29
- - `namespace: Lutaml::Xml::XmlNamespace.new(uri, prefix)` raises
30
- a `NoMethodError` inside `process_mapping`.
31
- - `map_element "dc:format"` (prefix in name) is silently ignored —
32
- local-name lookup strips the prefix.
33
- - `prefix: "dc"` is ignored when there is no `namespace:` on the
34
- mapping.
35
-
36
- The CLAUDE.md rule "no `Lutaml::Xml::Document.parse`" rules out
37
- falling back to a generic XmlDocument traversal. We must use a typed
38
- model.
39
-
40
- ## Path forward
41
-
42
- Wait for Lutaml to stabilise its namespace API, then add a typed
43
- `Idml::Parts::XmpMetadata` model that maps each XMP field to a Ruby
44
- attribute. Pipeline reads `META-INF/metadata.xml`, calls
45
- `XmpMetadata.from_xml`, and threads the resulting attributes through
46
- `PdfrbWriter#set_info`.
47
-
48
- When the blocker clears, the implementation looks like:
49
-
50
- ```ruby
51
- module Idml
52
- module Parts
53
- class XmpMetadata < Lutaml::Model::Serializable
54
- attribute :title, :string
55
- attribute :author, :string
56
- attribute :subject, :string
57
- attribute :keywords, :string
58
- attribute :creator_tool, :string
59
- attribute :create_date, :string
60
- attribute :modify_date, :string
61
-
62
- xml do
63
- root "Description"
64
- # …namespace-aware mappings once Lutaml supports them…
65
- end
66
- end
67
- end
68
- end
69
-
70
- # In Pipeline:
71
- if @package.has_part?("META-INF/metadata.xml")
72
- xmp = Idml::Parts::XmpMetadata.from_xml(
73
- @package.read_part("META-INF/metadata.xml"),
74
- )
75
- writer.set_info(
76
- Title: xmp.title,
77
- Author: xmp.author,
78
- Subject: xmp.subject,
79
- Keywords: xmp.keywords,
80
- Creator: xmp.creator_tool,
81
- CreationDate: pdf_date(xmp.create_date),
82
- ModDate: pdf_date(xmp.modify_date),
83
- )
84
- end
85
- ```
86
-
87
- ## Acceptance criteria (after blocker clears)
88
-
89
- - [ ] Pipeline reads `META-INF/metadata.xml` when present.
90
- - [ ] `XmpMetadata` typed model parses dc:title/creator/description/
91
- subject plus xmp:CreatorTool/CreateDate/ModifyDate.
92
- - [ ] Pipeline passes the parsed fields to `PdfrbWriter#set_info`.
93
- - [ ] Existing Producer/CreationDate defaults still apply when XMP
18
+ ## Architecture
19
+
20
+ XMP has multiple XML namespaces (`dc:`, `xmp:`, `pdf:`) inside one
21
+ `<rdf:Description>` element. Per Lutaml's namespace API, each
22
+ namespaced child is its own `Lutaml::Model::Serializable` subclass
23
+ carrying its own `namespace` declaration via a `Lutaml::Xml::Namespace`
24
+ class. The parent composes the children via `map_element`.
25
+
26
+ Layered models in `lib/idml/parts/xmp.rb`:
27
+
28
+ - `Xmp::RdfNamespace`, `DcNamespace`, `XmpNamespace`, `XmetaNamespace` —
29
+ `Lutaml::Xml::Namespace` subclasses with `uri` and `prefix_default`.
30
+ - `Xmp::Leaf` base + `Format`/`CreatorTool`/`CreateDate`/`ModifyDate`/
31
+ `MetadataDate` single direct-value leaves.
32
+ - `Xmp::ListItem` (rdf:li), `Alt` (rdf:Alt with x-default lookup),
33
+ `Seq` (rdf:Seq), `Bag` (rdf:Bag) RDF container types.
34
+ - `Xmp::Title`/`DcDescription`/`Creator`/`Subject` — dc: elements that
35
+ wrap the RDF containers.
36
+ - `XmpDescription` single rdf:Description composing all the leaves
37
+ and containers above.
38
+ - `XmpRdf` — rdf:RDF with a collection of Descriptions plus merged
39
+ accessors (`title`, `author`, `keywords`, etc.) that pick the first
40
+ non-nil across Descriptions.
41
+ - `XmpMeta` — outer `<x:xmpmeta>` wrapper.
42
+
43
+ Pipeline threads this through `combined_metadata`, which starts from
44
+ the default Producer+CreationDate and overrides each field that the
45
+ XMP packet supplies.
46
+
47
+ ## Verification
48
+
49
+ - `lib/idml/parts/xmp.rb` — typed XMP models.
50
+ - `lib/idml/render/pipeline.rb:193` — `combined_metadata` merge.
51
+ - `spec/idml/parts/xmp_meta_spec.rb` — 11 specs covering direct
52
+ elements, containers, missing fields, and the fixture.
53
+ - `spec/idml/render/render_pdfrb_pipeline_spec.rb` integration spec
54
+ asserts `/Creator` from XMP lands in the PDF.
55
+
56
+ ## Acceptance criteria
57
+
58
+ - [x] Pipeline reads `META-INF/metadata.xml` when present.
59
+ - [x] `XmpMeta` typed model parses dc:title/creator/description/subject
60
+ plus xmp:CreatorTool/CreateDate/ModifyDate.
61
+ - [x] Pipeline passes the parsed fields to `PdfrbWriter#set_info`.
62
+ - [x] Existing Producer/CreationDate defaults still apply when XMP
94
63
  or individual fields are absent.
95
- - [ ] Spec covers a synthetic XMP packet with all fields populated,
64
+ - [x] Spec covers a synthetic XMP packet with all fields populated,
96
65
  plus the no-metadata fallback.
97
-
98
- ## Dependencies
99
-
100
- - Lutaml namespace API stable enough that
101
- `map_element "format", namespace: <XmlNamespace>` works without
102
- raising. Tracked at the Lutaml migration guide referenced in the
103
- error message: `docs/_guides/xml-namespaces.adoc`.
@@ -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
 
@@ -21,7 +21,7 @@ module Idml
21
21
 
22
22
  def call
23
23
  writer = PdfrbWriter.new
24
- writer.set_info(default_metadata)
24
+ writer.set_info(combined_metadata)
25
25
  writer.enable_tagged if @tagged
26
26
  layer_filter = LayerFilter.from_designmap(@package.designmap)
27
27
  font_ref_resolver = FontReferenceResolver.build(@package)
@@ -190,6 +190,46 @@ module Idml
190
190
  nil
191
191
  end
192
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
+
193
233
  def default_metadata
194
234
  {
195
235
  Producer: "idml gem v#{Idml::VERSION}",
@@ -200,6 +240,9 @@ module Idml
200
240
  def pdf_date(time)
201
241
  time.strftime("D:%Y%m%d%H%M%S+00'00'")
202
242
  end
243
+
244
+ XMP_PATH = "META-INF/metadata.xml"
245
+ private_constant :XMP_PATH
203
246
  end
204
247
  end
205
248
  end
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.8"
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.8
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -381,6 +381,7 @@ files:
381
381
  - lib/idml/parts/style.rb
382
382
  - lib/idml/parts/style_mapping.rb
383
383
  - lib/idml/parts/tags.rb
384
+ - lib/idml/parts/xmp.rb
384
385
  - lib/idml/render.rb
385
386
  - lib/idml/render/blending.rb
386
387
  - lib/idml/render/color_helper.rb