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.
- 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/70-stroke-styling.md +23 -51
- data/TODO.pdf/71-shared-placement-module.md +41 -0
- data/TODO.pdf/72-stroke-styling.md +55 -0
- data/TODO.pdf/73-gradient-resolver-cleanup.md +48 -0
- data/TODO.pdf/74-renderer-spec-coverage.md +53 -0
- 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/elements/graphic_line.rb +8 -0
- data/lib/idml/elements/polygon.rb +8 -0
- data/lib/idml/elements/rectangle.rb +8 -0
- data/lib/idml/render/gradient_resolver.rb +10 -102
- data/lib/idml/render/pdfrb_writer.rb +22 -16
- data/lib/idml/render/pipeline.rb +3 -1
- data/lib/idml/render/placement.rb +31 -0
- data/lib/idml/render/renderers/graphic_line_renderer.rb +9 -7
- data/lib/idml/render/renderers/polygon_renderer.rb +8 -6
- data/lib/idml/render/renderers/rectangle_renderer.rb +8 -19
- data/lib/idml/render/renderers/table_renderer.rb +1 -1
- data/lib/idml/render/renderers/text_frame_renderer.rb +1 -9
- data/lib/idml/render/stroke_style.rb +97 -0
- data/lib/idml/render.rb +10 -3
- data/lib/idml/version.rb +1 -1
- metadata +10 -1
|
@@ -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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|