idml 0.2.6 → 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.
@@ -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
 
@@ -10,6 +10,10 @@ module Idml
10
10
  attribute :stroke_color, :string
11
11
  attribute :stroke_weight, :float
12
12
  attribute :stroke_tint, :float
13
+ attribute :end_cap, :string
14
+ attribute :end_join, :string
15
+ attribute :miter_limit, :float
16
+ attribute :stroke_dash_and_gap, :string
13
17
  attribute :visible, :boolean
14
18
  attribute :item_layer, :string
15
19
  attribute :properties, Idml::Elements::Properties, collection: true
@@ -22,6 +26,10 @@ module Idml
22
26
  map_attribute "StrokeColor", to: :stroke_color
23
27
  map_attribute "StrokeWeight", to: :stroke_weight
24
28
  map_attribute "StrokeTint", to: :stroke_tint
29
+ map_attribute "EndCap", to: :end_cap
30
+ map_attribute "EndJoin", to: :end_join
31
+ map_attribute "MiterLimit", to: :miter_limit
32
+ map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
25
33
  map_attribute "Visible", to: :visible
26
34
  map_attribute "ItemLayer", to: :item_layer
27
35
  map_element "Properties", to: :properties
@@ -12,6 +12,10 @@ module Idml
12
12
  attribute :fill_tint, :float
13
13
  attribute :stroke_color, :string
14
14
  attribute :stroke_weight, :float
15
+ attribute :end_cap, :string
16
+ attribute :end_join, :string
17
+ attribute :miter_limit, :float
18
+ attribute :stroke_dash_and_gap, :string
15
19
  attribute :visible, :boolean
16
20
  attribute :item_layer, :string
17
21
  attribute :properties, Idml::Elements::Properties, collection: true
@@ -27,6 +31,10 @@ module Idml
27
31
  map_attribute "FillTint", to: :fill_tint
28
32
  map_attribute "StrokeColor", to: :stroke_color
29
33
  map_attribute "StrokeWeight", to: :stroke_weight
34
+ map_attribute "EndCap", to: :end_cap
35
+ map_attribute "EndJoin", to: :end_join
36
+ map_attribute "MiterLimit", to: :miter_limit
37
+ map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
30
38
  map_attribute "Visible", to: :visible
31
39
  map_attribute "ItemLayer", to: :item_layer
32
40
  map_element "Properties", to: :properties
@@ -15,6 +15,10 @@ module Idml
15
15
  attribute :stroke_color, :string
16
16
  attribute :stroke_weight, :float
17
17
  attribute :stroke_tint, :float
18
+ attribute :end_cap, :string
19
+ attribute :end_join, :string
20
+ attribute :miter_limit, :float
21
+ attribute :stroke_dash_and_gap, :string
18
22
  attribute :visible, :boolean
19
23
  attribute :name, :string
20
24
  attribute :item_layer, :string
@@ -33,6 +37,10 @@ module Idml
33
37
  map_attribute "StrokeColor", to: :stroke_color
34
38
  map_attribute "StrokeWeight", to: :stroke_weight
35
39
  map_attribute "StrokeTint", to: :stroke_tint
40
+ map_attribute "EndCap", to: :end_cap
41
+ map_attribute "EndJoin", to: :end_join
42
+ map_attribute "MiterLimit", to: :miter_limit
43
+ map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
36
44
  map_attribute "Visible", to: :visible
37
45
  map_attribute "Name", to: :name
38
46
  map_attribute "ItemLayer", to: :item_layer
@@ -2,111 +2,19 @@
2
2
 
3
3
  module Idml
4
4
  module Render
5
- # Resolves IDML gradient references to discrete PDF gradient fills.
6
- # A gradient is rendered as N thin colored rectangles that
7
- # approximate the smooth gradient transition. This is not a true
8
- # PDF shading pattern (which requires Function dictionaries), but
9
- # produces a visually correct gradient without complex PDF structure.
10
- class GradientResolver
11
- SEGMENTS = 32
12
-
13
- def self.build(graphic)
14
- table = {}
15
- return new(table) unless graphic&.gradient
16
-
17
- graphic.gradient.each do |gradient|
18
- table[gradient.self_attr] = gradient
19
- end
20
- new(table)
21
- end
22
-
23
- def initialize(table)
24
- @table = table
25
- @color_resolver = nil
26
- end
27
-
28
- # Detect if a color reference is a gradient.
5
+ # Identifies IDML gradient color references and exposes the
6
+ # gradient catalogue from `Resources/Graphic.xml`.
7
+ #
8
+ # The discrete-rectangle approximation that this module used to
9
+ # carry was removed once pdfrb 0.4.0 introduced real PDF shadings
10
+ # (TODOs 49, 66, 68). Today the renderers go through pdfrb's
11
+ # `Shadings#add_axial` / `add_radial` directly; this module's
12
+ # only remaining responsibility is the predicate that distinguishes
13
+ # gradient colour references from solid ones.
14
+ module GradientResolver
29
15
  def self.gradient?(name)
30
16
  name.is_a?(String) && name.start_with?("Gradient/")
31
17
  end
32
-
33
- # Render a gradient fill as PDF operators. Returns nil if the
34
- # color is not a gradient. The returned operators emit colored
35
- # rectangles from y2 down to y1 (matching PDF Y-up coords).
36
- def render_gradient(name, x:, y:, width:, height:, color_resolver:)
37
- @color_resolver = color_resolver
38
- gradient = @table[name]
39
- return nil unless gradient
40
-
41
- stops = gradient.gradient_stop
42
- return nil if stops.length < 2
43
-
44
- segment_height = height.to_f / SEGMENTS
45
- ops = []
46
- SEGMENTS.downto(1) do |i|
47
- offset = (i - 1) / SEGMENTS.to_f
48
- color = interpolate(stops, offset)
49
- next unless color
50
-
51
- ops << "q"
52
- ops << Idml::Render::ColorHelper.to_canvas(color)
53
- ops << format("%<x>.2f %<y>.2f %<w>.2f %<h>.2f re",
54
- x: x,
55
- y: y + ((SEGMENTS - i) * segment_height),
56
- w: width,
57
- h: segment_height + 0.1)
58
- ops << "f"
59
- ops << "Q"
60
- end
61
- ops.join("\n")
62
- end
63
-
64
- def interpolate(stops, offset)
65
- next_stop = stops.find { |s| s.location >= offset } || stops.last
66
- prev_stop = stops.reverse.find { |s| s.location <= offset } || stops.first
67
- return resolve_color(next_stop.stop_color) if next_stop == prev_stop
68
-
69
- blend_between(prev_stop, next_stop, offset)
70
- end
71
-
72
- def blend_between(prev_stop, next_stop, offset)
73
- range = (next_stop.location - prev_stop.location)
74
- range = 1.0 if range.zero?
75
- local = (offset - prev_stop.location) / range
76
-
77
- prev_color = resolve_color(prev_stop.stop_color)
78
- next_color = resolve_color(next_stop.stop_color)
79
- return prev_color unless prev_color && next_color
80
- return next_color if local >= 1.0
81
-
82
- blend(prev_color, next_color, local)
83
- end
84
-
85
- def resolve_color(name)
86
- return nil unless @color_resolver && name
87
-
88
- @color_resolver.resolve(name)
89
- end
90
-
91
- def blend(color_a, color_b, t)
92
- case color_a[:model]
93
- when :rgb
94
- {
95
- model: :rgb,
96
- r: color_a[:r] + ((color_b[:r] - color_a[:r]) * t),
97
- g: color_a[:g] + ((color_b[:g] - color_a[:g]) * t),
98
- b: color_a[:b] + ((color_b[:b] - color_a[:b]) * t),
99
- }
100
- when :cmyk
101
- {
102
- model: :cmyk,
103
- c: color_a[:c] + ((color_b[:c] - color_a[:c]) * t),
104
- m: color_a[:m] + ((color_b[:m] - color_a[:m]) * t),
105
- y: color_a[:y] + ((color_b[:y] - color_a[:y]) * t),
106
- k: color_a[:k] + ((color_b[:k] - color_a[:k]) * t),
107
- }
108
- end
109
- end
110
18
  end
111
19
  end
112
20
  end
@@ -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,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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Resolves a page item's geometric placement on the PDF page.
6
+ # Encapsulates the read of `geometric_bounds` and `item_transform`
7
+ # from any page-item model and the subsequent transform into a
8
+ # PDF-coordinate `{ x:, y:, width:, height: }` rect via `Geometry`.
9
+ #
10
+ # This is the single source of truth for "where does this item
11
+ # draw" — every shape renderer goes through here instead of each
12
+ # one re-implementing the bounds/transform dance.
13
+ module Placement
14
+ FALLBACK = { x: 72.0, y: 72.0, width: 400.0, height: 600.0 }.freeze
15
+
16
+ # Returns the PDF-placement rect for the given page item, or
17
+ # `nil` when the item has no geometric bounds. Pass
18
+ # `fallback: true` to receive a default rect instead of nil
19
+ # (used by text frames that need *somewhere* to render even
20
+ # when geometry is absent).
21
+ def self.box(item, page_height, fallback: false)
22
+ bounds = item.geometric_bounds
23
+ if bounds
24
+ Geometry.placement_rect(bounds, item.item_transform, page_height)
25
+ elsif fallback
26
+ FALLBACK.dup.merge(y: page_height - FALLBACK[:height] - 72.0)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -6,20 +6,22 @@ module Idml
6
6
  class GraphicLineRenderer
7
7
  def self.render(canvas, context)
8
8
  line = context.item
9
- return unless RectangleRenderer.strokeable?(line)
9
+ return unless StrokeStyle.strokeable?(line)
10
10
 
11
11
  color = context.color_resolver&.resolve(line.stroke_color)
12
12
  return unless color
13
13
 
14
- box = RectangleRenderer.placement_box(line, context.page_height)
14
+ box = Placement.box(line, context.page_height)
15
15
  return unless box
16
16
 
17
17
  Blending.wrap(canvas, line.transparency_setting) do
18
- canvas.stroke_color(ColorHelper.to_canvas(color))
19
- canvas.line_width = line.stroke_weight
20
- canvas.move_to(box[:x], box[:y] + box[:height])
21
- canvas.line_to(box[:x] + box[:width], box[:y])
22
- canvas.stroke
18
+ StrokeStyle.apply(canvas, line) do
19
+ canvas.stroke_color(ColorHelper.to_canvas(color))
20
+ canvas.line_width = line.stroke_weight
21
+ canvas.move_to(box[:x], box[:y] + box[:height])
22
+ canvas.line_to(box[:x] + box[:width], box[:y])
23
+ canvas.stroke
24
+ end
23
25
  end
24
26
  end
25
27
  end
@@ -8,7 +8,7 @@ module Idml
8
8
  poly = context.item
9
9
  return if poly.visible == false
10
10
 
11
- box = RectangleRenderer.placement_box(poly, context.page_height)
11
+ box = Placement.box(poly, context.page_height)
12
12
  return unless box
13
13
 
14
14
  Blending.wrap(canvas, poly.transparency_setting) do
@@ -30,15 +30,17 @@ module Idml
30
30
  private_class_method :render_fill
31
31
 
32
32
  def self.render_stroke(canvas, poly, context, box)
33
- return unless RectangleRenderer.strokeable?(poly)
33
+ return unless StrokeStyle.strokeable?(poly)
34
34
 
35
35
  color = context.color_resolver&.resolve(poly.stroke_color)
36
36
  return unless color
37
37
 
38
- canvas.stroke_color(ColorHelper.to_canvas(color))
39
- canvas.line_width = poly.stroke_weight
40
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
41
- canvas.stroke
38
+ StrokeStyle.apply(canvas, poly) do
39
+ canvas.stroke_color(ColorHelper.to_canvas(color))
40
+ canvas.line_width = poly.stroke_weight
41
+ canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
42
+ canvas.stroke
43
+ end
42
44
  end
43
45
  private_class_method :render_stroke
44
46
  end