idml 0.4.0 → 0.4.2
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 +4 -4
- data/TODO.pdf/83-pipeline-decomposition.md +54 -0
- data/TODO.pdf/84-schema-faithful-table.md +80 -0
- data/TODO.pdf/85-per-text-range-hyperlinks.md +93 -0
- data/TODO.pdf/86-caching-performance.md +71 -0
- data/lib/idml/elements/character_style_range.rb +3 -0
- data/lib/idml/elements/hyperlink_text_source.rb +31 -0
- data/lib/idml/elements.rb +2 -0
- data/lib/idml/render/font_setup.rb +77 -0
- data/lib/idml/render/hyperlink_emitter.rb +6 -2
- data/lib/idml/render/metadata_builder.rb +73 -0
- data/lib/idml/render/pipeline.rb +37 -127
- data/lib/idml/render.rb +2 -0
- data/lib/idml/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79791db7118dac992a6dbfc780f143bc545cc1d6b7c408fac91fe9eb75de8d32
|
|
4
|
+
data.tar.gz: f98eb3f41100e1b7f62c51511f33aa774a5272e14fb4bcc7012dff50c88336d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34b95aeea5bf217fec6a88f8ae7a8a03c399001c0f4735bc11b0485afe27dbf9012fd481c8e6005f68f17fcc2bf850a63d03a3d23249bd570a57739ab92af1ed
|
|
7
|
+
data.tar.gz: 1f462441786c0a807f3bb0ddb01ff0c1c4a2e9a8e736fbd9e18b7666c2fe1c57c917dae3770b051ec716a25f4202cba5eef86a96bdca9630263ff5f385527367
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
idml (0.4.
|
|
4
|
+
idml (0.4.2)
|
|
5
5
|
bigdecimal
|
|
6
6
|
lutaml-model (~> 0.8.18)
|
|
7
7
|
pdfrb
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
PATH
|
|
12
12
|
remote: /Users/mulgogi/src/claricle/pdfrb
|
|
13
13
|
specs:
|
|
14
|
-
pdfrb (0.
|
|
14
|
+
pdfrb (0.6.0)
|
|
15
15
|
thor
|
|
16
16
|
|
|
17
17
|
GEM
|
|
@@ -183,7 +183,7 @@ CHECKSUMS
|
|
|
183
183
|
ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
|
|
184
184
|
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
|
185
185
|
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
|
186
|
-
idml (0.4.
|
|
186
|
+
idml (0.4.2)
|
|
187
187
|
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
|
|
188
188
|
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
|
|
189
189
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
@@ -203,7 +203,7 @@ CHECKSUMS
|
|
|
203
203
|
paint (2.3.0) sha256=327d623e4038619d5bd99ae5db07973859cd78400c7f0329eea283cef8e83be5
|
|
204
204
|
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
205
205
|
parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
|
|
206
|
-
pdfrb (0.
|
|
206
|
+
pdfrb (0.6.0)
|
|
207
207
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
208
208
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
209
209
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# TODO PDF 83: Pipeline decomposition
|
|
2
|
+
|
|
3
|
+
## Status: DONE
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
Two extractions from `Pipeline` into dedicated classes:
|
|
8
|
+
|
|
9
|
+
1. **`Render::MetadataBuilder`** — metadata assembly (combined
|
|
10
|
+
defaults + XMP-extracted fields, ISO 8601 → PDF date conversion).
|
|
11
|
+
2. **`Render::FontSetup`** — document font resolution via
|
|
12
|
+
`Pdfrb::FontResolver`, registration with pdfrb, and
|
|
13
|
+
`PdfrbFontMetrics` adapter construction.
|
|
14
|
+
|
|
15
|
+
Pipeline now does pure orchestration: build writer → set metadata →
|
|
16
|
+
apply compliance → set up structure → resolve font → render spreads
|
|
17
|
+
→ flush structure → emit bookmarks → subset → write. No parsing,
|
|
18
|
+
no date math, no font-path lookup.
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
Pipeline had 9 private methods across metadata, font setup, and
|
|
23
|
+
rendering concerns. Each concern is now its own class:
|
|
24
|
+
|
|
25
|
+
- `MetadataBuilder` — Info dict assembly.
|
|
26
|
+
- `FontSetup` — font file lookup + registration + metrics adapter.
|
|
27
|
+
- `PdfaPacket` — XMP packet + Catalog /Metadata attachment.
|
|
28
|
+
- `IccProfile` — locate sRGB ICC bytes.
|
|
29
|
+
- `BookmarkResolver` — destination chain + page index.
|
|
30
|
+
- `HyperlinkResolver` — source → URL lookup.
|
|
31
|
+
- `HyperlinkEmitter` — Link annotation emission.
|
|
32
|
+
- `StructureTracker` — MCID allocation + element registration.
|
|
33
|
+
- `StructureMapper` — item → PDF structure type.
|
|
34
|
+
- `ImageCollector` — image registration + placement.
|
|
35
|
+
|
|
36
|
+
Pipeline shrunk from ~225 lines (v0.3.0) to ~135 lines (v0.4.2).
|
|
37
|
+
|
|
38
|
+
## Verification
|
|
39
|
+
|
|
40
|
+
- `lib/idml/render/metadata_builder.rb` — extracted.
|
|
41
|
+
- `lib/idml/render/font_setup.rb` — extracted.
|
|
42
|
+
- `lib/idml/render/pipeline.rb` — orchestration only.
|
|
43
|
+
- `spec/idml/render/metadata_builder_spec.rb` — 5 specs.
|
|
44
|
+
- `spec/idml/render/font_setup_spec.rb` — 4 specs.
|
|
45
|
+
- `spec/idml/render/placement_spec.rb` — added for completeness.
|
|
46
|
+
|
|
47
|
+
## Acceptance criteria
|
|
48
|
+
|
|
49
|
+
- [x] MetadataBuilder extracted.
|
|
50
|
+
- [x] FontSetup extracted.
|
|
51
|
+
- [x] Pipeline no longer contains metadata/font helpers.
|
|
52
|
+
- [x] Each extracted class has dedicated specs.
|
|
53
|
+
- [x] All existing pipeline specs still pass.
|
|
54
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# TODO PDF 84: Schema-faithful Table/Cell/Row structure
|
|
2
|
+
|
|
3
|
+
## Status: DEFERRED (architectural correction; no fixtures to validate)
|
|
4
|
+
|
|
5
|
+
## Current state vs schema
|
|
6
|
+
|
|
7
|
+
The existing `Idml::Elements::Table`, `TableRow`, `TableCell` classes
|
|
8
|
+
do not match the IDML RNC schema. Three structural divergences:
|
|
9
|
+
|
|
10
|
+
### 1. Table lives in Stories, not Spreads
|
|
11
|
+
|
|
12
|
+
Per `reference-docs/schemas/package/Stories/Story.rnc`, `Table_Object`
|
|
13
|
+
appears inside `Story_Object` (as a child of `ParagraphStyleRange` or
|
|
14
|
+
`CharacterStyleRange`). The current `Idml::Elements::SpreadObject`
|
|
15
|
+
incorrectly declares `attribute :table, ... collection: true`, and
|
|
16
|
+
`PageItemRenderer` dispatches spread-level Tables to `TableRenderer`.
|
|
17
|
+
|
|
18
|
+
In real IDML, Tables appear inside Stories. A TextFrame's story
|
|
19
|
+
contains the table; the renderer should walk the story to find
|
|
20
|
+
tables, not the spread.
|
|
21
|
+
|
|
22
|
+
### 2. Element names: `Cell` and `Row`, not `TableCell` and `TableRow`
|
|
23
|
+
|
|
24
|
+
Per RNC, the element names are:
|
|
25
|
+
- `element Cell` (`Cell_Object`)
|
|
26
|
+
- `element Row` (`Row_Object`)
|
|
27
|
+
|
|
28
|
+
The current model uses `TableCell` and `TableRow` as both class names
|
|
29
|
+
and XML element roots — neither matches the schema.
|
|
30
|
+
|
|
31
|
+
### 3. Cell and Row are siblings under Table, not nested
|
|
32
|
+
|
|
33
|
+
Per RNC, `Table_Object` contains `Cell_Object* & Row_Object*` as
|
|
34
|
+
**siblings**, not nested `<Row><Cell>` structure:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Table_Object = element Table {
|
|
38
|
+
...
|
|
39
|
+
Cell_Object*,
|
|
40
|
+
Row_Object*
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The current model uses `Table > TableRow > TableCell` nesting, which
|
|
45
|
+
won't parse real IDML.
|
|
46
|
+
|
|
47
|
+
## Plan
|
|
48
|
+
|
|
49
|
+
1. Add new `Elements::Cell` (matching `Cell_Object` RNC, ~80 attrs).
|
|
50
|
+
2. Add new `Elements::Row` (matching `Row_Object` RNC).
|
|
51
|
+
3. Restructure `Elements::Table` to declare both `cell` and `row`
|
|
52
|
+
sibling collections.
|
|
53
|
+
4. Remove `Elements::TableCell` and `Elements::TableRow` (broken).
|
|
54
|
+
5. Remove `:table` from `Elements::SpreadObject`; Tables come via
|
|
55
|
+
Stories only.
|
|
56
|
+
6. Update `TableRenderer` to:
|
|
57
|
+
- Dispatch from `TextFrameRenderer` when a story contains a Table.
|
|
58
|
+
- Render via `cell` and `row` siblings, using cell `Name` attr
|
|
59
|
+
(e.g., `"0_0"`) to determine column/row position.
|
|
60
|
+
7. Update `PageItemRenderer::RENDERER_MAP` to remove Table (no
|
|
61
|
+
longer a page item).
|
|
62
|
+
8. Update specs.
|
|
63
|
+
|
|
64
|
+
## Why deferred
|
|
65
|
+
|
|
66
|
+
- No fixture in `spec/fixtures/` contains a Table, so the current
|
|
67
|
+
broken model has never been validated against real IDML.
|
|
68
|
+
- The correction touches multiple subsystems (elements, parts,
|
|
69
|
+
renderers, dispatch) and is best done as a focused refactor with
|
|
70
|
+
real-fixture regression tests.
|
|
71
|
+
- Adding fixtures requires either Adobe InDesign (commercial) or a
|
|
72
|
+
hand-crafted IDML ZIP matching the schema.
|
|
73
|
+
|
|
74
|
+
## Acceptance criteria
|
|
75
|
+
|
|
76
|
+
- [ ] `Elements::Cell` matches `Cell_Object` RNC.
|
|
77
|
+
- [ ] `Elements::Row` matches `Row_Object` RNC.
|
|
78
|
+
- [ ] `Elements::Table` has sibling `cell` and `row` collections.
|
|
79
|
+
- [ ] Tables dispatch from `TextFrameRenderer`, not spread iteration.
|
|
80
|
+
- [ ] Real-fixture regression test with a known Table IDML.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# TODO PDF 85: Per-TextRange hyperlink precision
|
|
2
|
+
|
|
3
|
+
## Status: DEFERRED (requires text-engine position tracking)
|
|
4
|
+
|
|
5
|
+
## Current state
|
|
6
|
+
|
|
7
|
+
`HyperlinkEmitter` (TODO 78) emits one Link annotation per text
|
|
8
|
+
frame whose story contains a hyperlink source. The annotation
|
|
9
|
+
covers the entire frame's bounding box, not just the linked
|
|
10
|
+
characters.
|
|
11
|
+
|
|
12
|
+
## What per-TextRange precision requires
|
|
13
|
+
|
|
14
|
+
For each hyperlink source, compute the rect that covers exactly
|
|
15
|
+
the source's characters on the page.
|
|
16
|
+
|
|
17
|
+
### Position tracking
|
|
18
|
+
|
|
19
|
+
The text engine (`Shaper`, `LineBreaker`, `Justifier`) already
|
|
20
|
+
produces positioned lines via `VerticalLayout`, but
|
|
21
|
+
`TextFrameRenderer` does not surface per-character positions
|
|
22
|
+
outside its own scope. To compute per-source rects we need:
|
|
23
|
+
|
|
24
|
+
1. **Absolute character index per CSR** — track cumulative char
|
|
25
|
+
position as CSRs are processed within a story.
|
|
26
|
+
2. **Per-glyph (x, y) after layout** — Shaper gives widths,
|
|
27
|
+
LineBreaker gives line assignments, Justifier gives x_offset,
|
|
28
|
+
but no structured "positioned glyph" output is exposed today.
|
|
29
|
+
3. **Source range lookup** — map `HyperlinkTextSource#Self` to a
|
|
30
|
+
`[start_char, end_char]` range. Today HyperlinkTextSource is
|
|
31
|
+
modelled as a sibling element of `Content` inside CSR; the
|
|
32
|
+
range is implicit (the source wraps its own content).
|
|
33
|
+
|
|
34
|
+
### Emitter change
|
|
35
|
+
|
|
36
|
+
`HyperlinkEmitter` currently does:
|
|
37
|
+
```ruby
|
|
38
|
+
box = Placement.box(frame, @page_height)
|
|
39
|
+
@writer.add_uri_link_annotation(page_index:, rect: rect_for(box), url:)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Per-range version needs the positioned glyph data:
|
|
43
|
+
```ruby
|
|
44
|
+
positions = context.positioned_glyphs_for_frame(frame.self_attr)
|
|
45
|
+
range = source_range_for(hyperlink_source)
|
|
46
|
+
rect = bounding_rect_for_range(positions, range)
|
|
47
|
+
@writer.add_uri_link_annotation(page_index:, rect:, url:)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Threading
|
|
51
|
+
|
|
52
|
+
Positioned glyph data must travel from `TextFrameRenderer` (which
|
|
53
|
+
runs the layout engine) to `HyperlinkEmitter` (which runs after
|
|
54
|
+
the page renders). Options:
|
|
55
|
+
|
|
56
|
+
- **A. Side channel via RenderContext**: add `positioned_glyphs`
|
|
57
|
+
hash to RenderContext, keyed by frame Self. Renderers populate
|
|
58
|
+
it; emitter reads it.
|
|
59
|
+
- **B. Two-pass render**: re-run layout in the emitter to compute
|
|
60
|
+
positions. Wasteful but keeps emitter self-contained.
|
|
61
|
+
- **C. Position tracker object**: similar to StructureTracker —
|
|
62
|
+
Pipeline constructs it, threads through RenderContext, queries
|
|
63
|
+
after rendering.
|
|
64
|
+
|
|
65
|
+
Option C is the cleanest (matches StructureTracker pattern from
|
|
66
|
+
TODO 76).
|
|
67
|
+
|
|
68
|
+
## Plan
|
|
69
|
+
|
|
70
|
+
1. Add `Render::PositionTracker` — accumulates per-frame positioned
|
|
71
|
+
ranges: `[{ frame_self:, start_char:, end_char:, x:, y:, width:,
|
|
72
|
+
height: }, ...]`.
|
|
73
|
+
2. Add `position_tracker` to `RenderContext`.
|
|
74
|
+
3. In `TextFrameRenderer#render_run_lines`, after layout, push each
|
|
75
|
+
line's positioned range to the tracker.
|
|
76
|
+
4. In `HyperlinkEmitter#emit_for_frame`, query the tracker for
|
|
77
|
+
ranges that overlap each hyperlink source's `[start_char,
|
|
78
|
+
end_char]`. Compute the bounding rect from those ranges.
|
|
79
|
+
5. Emit one Link annotation per source.
|
|
80
|
+
|
|
81
|
+
## Why deferred
|
|
82
|
+
|
|
83
|
+
- No fixture has hyperlinks → can't validate per-range precision.
|
|
84
|
+
- Position tracker adds another stateful object to the render path.
|
|
85
|
+
- Frame-level precision is "good enough" for visible link presence;
|
|
86
|
+
per-range is a polish improvement.
|
|
87
|
+
|
|
88
|
+
## Acceptance criteria
|
|
89
|
+
|
|
90
|
+
- [ ] PositionTracker accumulates per-frame positioned ranges.
|
|
91
|
+
- [ ] HyperlinkEmitter queries tracker for source ranges.
|
|
92
|
+
- [ ] Each hyperlink source gets its own Link annotation rect.
|
|
93
|
+
- [ ] Spec covers multi-source story with overlapping ranges.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# TODO PDF 86: Caching and performance
|
|
2
|
+
|
|
3
|
+
## Status: PARTIAL — most hot paths cached; one risk noted
|
|
4
|
+
|
|
5
|
+
## What is cached today
|
|
6
|
+
|
|
7
|
+
- `ColorResolver#resolve` — per-color-name cache (`@cache`).
|
|
8
|
+
- `PdfrbFontMetrics#metrics_data` — read-through to pdfrb's
|
|
9
|
+
`Fonts#metrics_for` which has its own cache.
|
|
10
|
+
- `Pipeline#font_resolver` — lazily memoised via `||=`.
|
|
11
|
+
- `MetadataBuilder#xmp_packet` — memoised via `@xmp_packet ||=`.
|
|
12
|
+
- `PdfrbWriter#@image_cache` — URI → name cache for images.
|
|
13
|
+
- `Pdfrb::Document` internals — fonts, pages, structure all have
|
|
14
|
+
their own caches.
|
|
15
|
+
|
|
16
|
+
## What is not cached
|
|
17
|
+
|
|
18
|
+
### `geometric_bounds` on element instances
|
|
19
|
+
|
|
20
|
+
Each call to `Placement.box(item, page_height)` calls
|
|
21
|
+
`item.geometric_bounds`, which walks `Properties → PathGeometry →
|
|
22
|
+
bounding_box`. For each rendered item, this is called:
|
|
23
|
+
|
|
24
|
+
- 1× in the renderer (via `Placement.box`).
|
|
25
|
+
- 1× in `ImageCollector#clip_box_for` (for items that have images).
|
|
26
|
+
- 1× in `HyperlinkEmitter#emit_for_frame` (for text frames).
|
|
27
|
+
|
|
28
|
+
So 2–3 walks per item. Memoising at the element level would save
|
|
29
|
+
the duplicate walks.
|
|
30
|
+
|
|
31
|
+
### Risk: Lutaml model instance variables
|
|
32
|
+
|
|
33
|
+
Lutaml::Model::Serializable instances are populated via the
|
|
34
|
+
framework's own `instance_variable_set` during deserialization.
|
|
35
|
+
Adding `@geometric_bounds ||= ...` could conflict if Lutaml ever
|
|
36
|
+
adds an attribute of the same name, or if the framework resets
|
|
37
|
+
instance state during round-trip.
|
|
38
|
+
|
|
39
|
+
The safer pattern is to memoise at the helper layer (Placement)
|
|
40
|
+
using `Object#object_id` as the cache key, but that introduces
|
|
41
|
+
memory pressure (unbounded hash growth).
|
|
42
|
+
|
|
43
|
+
### Decision
|
|
44
|
+
|
|
45
|
+
Defer until profiling shows it matters. Current render of the
|
|
46
|
+
sample-with-image fixture (~60KB PDF, multiple shapes/text frames)
|
|
47
|
+
completes in ~250ms, which is fast enough for typical use.
|
|
48
|
+
|
|
49
|
+
## Other performance notes
|
|
50
|
+
|
|
51
|
+
- **SpreadRenderer iterates spread items 3×**: once for
|
|
52
|
+
`ImageCollector`, once for rendering, once for `HyperlinkEmitter`.
|
|
53
|
+
Could be combined but couples concerns.
|
|
54
|
+
- **No lazy part loading**: opening a Package reads all parts
|
|
55
|
+
eagerly. For large IDML files this could be slow; not yet a
|
|
56
|
+
measured problem.
|
|
57
|
+
|
|
58
|
+
## Plan (if profiling shows it matters)
|
|
59
|
+
|
|
60
|
+
1. Memoise `geometric_bounds` on `Elements::Rectangle`, `Polygon`,
|
|
61
|
+
`GraphicLine`, `TextFrame`, `Table` via `||=` with a
|
|
62
|
+
non-conflicting ivar name (e.g., `@_bounds_cache`).
|
|
63
|
+
2. Add a spec that asserts the cache is populated after first
|
|
64
|
+
access.
|
|
65
|
+
3. Re-run render benchmarks.
|
|
66
|
+
|
|
67
|
+
## Acceptance criteria
|
|
68
|
+
|
|
69
|
+
- [ ] Profile render of a large fixture and identify hot paths.
|
|
70
|
+
- [ ] If `geometric_bounds` shows up, memoise safely.
|
|
71
|
+
- [ ] Spec asserts memoisation doesn't break round-trip.
|
|
@@ -296,6 +296,8 @@ module Idml
|
|
|
296
296
|
attribute :character_style_range, Idml::Elements::CharacterStyleRange,
|
|
297
297
|
collection: true
|
|
298
298
|
attribute :content, Idml::Elements::Content, collection: true
|
|
299
|
+
attribute :hyperlink_text_source, Idml::Elements::HyperlinkTextSource,
|
|
300
|
+
collection: true
|
|
299
301
|
attribute :xml_element, Idml::Elements::XmlElement, collection: true
|
|
300
302
|
|
|
301
303
|
xml do
|
|
@@ -649,6 +651,7 @@ module Idml
|
|
|
649
651
|
map_attribute "UpperLevel", to: :upper_level
|
|
650
652
|
map_element "CharacterStyleRange", to: :character_style_range
|
|
651
653
|
map_element "Content", to: :content
|
|
654
|
+
map_element "HyperlinkTextSource", to: :hyperlink_text_source
|
|
652
655
|
map_element "XMLElement", to: :xml_element
|
|
653
656
|
end
|
|
654
657
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Elements
|
|
5
|
+
# `<HyperlinkTextSource>` — source anchor of a hyperlink in a
|
|
6
|
+
# story. Per `HyperlinkTextSource_Object` in
|
|
7
|
+
# `reference-docs/schemas/package/Stories/Story.rnc`. The `Self`
|
|
8
|
+
# attribute is referenced by `<Hyperlink Source="...">` in the
|
|
9
|
+
# document designmap.
|
|
10
|
+
#
|
|
11
|
+
# Note on text range: IDML's HyperlinkTextSource wraps the
|
|
12
|
+
# character content it covers (it is a sibling of `<Content>`
|
|
13
|
+
# inside a `<CharacterStyleRange>`). The wrapper's position in
|
|
14
|
+
# the story flow determines the range — there is no explicit
|
|
15
|
+
# `StartIndex`/`EndIndex` on the element itself.
|
|
16
|
+
class HyperlinkTextSource < Lutaml::Model::Serializable
|
|
17
|
+
attribute :self_attr, :string
|
|
18
|
+
attribute :name, :string
|
|
19
|
+
attribute :hidden, :boolean
|
|
20
|
+
attribute :applied_character_style, :string
|
|
21
|
+
|
|
22
|
+
xml do
|
|
23
|
+
root "HyperlinkTextSource"
|
|
24
|
+
map_attribute "Self", to: :self_attr
|
|
25
|
+
map_attribute "Name", to: :name
|
|
26
|
+
map_attribute "Hidden", to: :hidden
|
|
27
|
+
map_attribute "AppliedCharacterStyle", to: :applied_character_style
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/idml/elements.rb
CHANGED
|
@@ -64,6 +64,8 @@ module Idml
|
|
|
64
64
|
autoload :Hyperlink, "idml/elements/hyperlink"
|
|
65
65
|
autoload :HyperlinkPageDestination,
|
|
66
66
|
"idml/elements/hyperlink_page_destination"
|
|
67
|
+
autoload :HyperlinkTextSource,
|
|
68
|
+
"idml/elements/hyperlink_text_source"
|
|
67
69
|
autoload :HyperlinkURLDestination,
|
|
68
70
|
"idml/elements/hyperlink_url_destination"
|
|
69
71
|
autoload :Image, "idml/elements/image"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Resolves the document font and wraps it in a
|
|
6
|
+
# `TextEngine::PdfrbFontMetrics` adapter. Encapsulates:
|
|
7
|
+
#
|
|
8
|
+
# 1. Finding the IDML document's first non-missing font file
|
|
9
|
+
# via `Pdfrb::FontResolver#find_by_ps_name`.
|
|
10
|
+
# 2. Registering that file with pdfrb's Fonts collection.
|
|
11
|
+
# 3. Wrapping the resulting Symbol resource in
|
|
12
|
+
# `PdfrbFontMetrics` for the text engine.
|
|
13
|
+
#
|
|
14
|
+
# Extracted from `Pipeline` for SRP — Pipeline orchestrates,
|
|
15
|
+
# this object owns the font-setup dance.
|
|
16
|
+
class FontSetup
|
|
17
|
+
DEFAULT_FONT = Render::DEFAULT_FONT
|
|
18
|
+
|
|
19
|
+
def initialize(package:, font_search_paths: nil)
|
|
20
|
+
@package = package
|
|
21
|
+
@font_search_paths = font_search_paths
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Registers the resolved font with `writer` and returns the
|
|
25
|
+
# Symbol resource. Falls back to `Render::DEFAULT_FONT`
|
|
26
|
+
# (`"Helvetica"`) when the document has no resolvable font.
|
|
27
|
+
def register(writer)
|
|
28
|
+
path = resolve_document_font_path
|
|
29
|
+
return DEFAULT_FONT unless path
|
|
30
|
+
|
|
31
|
+
writer.register_font(path)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
DEFAULT_FONT
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Wraps a registered font resource in a PdfrbFontMetrics
|
|
37
|
+
# adapter. Returns nil when the resource is the default
|
|
38
|
+
# fallback (no real metrics available).
|
|
39
|
+
def metrics_for(writer, font_resource)
|
|
40
|
+
return nil if font_resource == DEFAULT_FONT
|
|
41
|
+
|
|
42
|
+
TextEngine::PdfrbFontMetrics.new(writer.document.fonts, font_resource)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def resolve_document_font_path
|
|
50
|
+
return nil unless @package&.fonts
|
|
51
|
+
|
|
52
|
+
@package.fonts.font_family.each do |family|
|
|
53
|
+
path = find_font_file(family)
|
|
54
|
+
return path if path
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_font_file(family)
|
|
60
|
+
family.font.each do |font|
|
|
61
|
+
next unless font.post_script_name
|
|
62
|
+
next if font.status == "Missing"
|
|
63
|
+
|
|
64
|
+
path = resolver.find_by_ps_name(font.post_script_name)
|
|
65
|
+
return path if path
|
|
66
|
+
end
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolver
|
|
71
|
+
@resolver ||= Pdfrb::FontResolver.new(
|
|
72
|
+
search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -12,11 +12,12 @@ module Idml
|
|
|
12
12
|
# the source's TextRange. Precise per-range rects require deeper
|
|
13
13
|
# integration with the text engine — see TODO 78.
|
|
14
14
|
class HyperlinkEmitter
|
|
15
|
-
def initialize(writer:, package:, page_height:)
|
|
15
|
+
def initialize(writer:, package:, page_height:, layer_filter: nil)
|
|
16
16
|
@writer = writer
|
|
17
17
|
@package = package
|
|
18
18
|
@resolver = HyperlinkResolver.new(package)
|
|
19
19
|
@page_height = page_height
|
|
20
|
+
@layer_filter = layer_filter
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def emit_for(spread, page_index)
|
|
@@ -29,7 +30,10 @@ module Idml
|
|
|
29
30
|
|
|
30
31
|
def each_text_frame_on(spread)
|
|
31
32
|
spread.each_page_item do |item|
|
|
32
|
-
|
|
33
|
+
next unless item.is_a?(Idml::Elements::TextFrame)
|
|
34
|
+
next if @layer_filter && !@layer_filter.visible?(item)
|
|
35
|
+
|
|
36
|
+
yield item
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Idml
|
|
6
|
+
module Render
|
|
7
|
+
# Builds the PDF Info-dict metadata hash by merging XMP-extracted
|
|
8
|
+
# values from `META-INF/metadata.xml` over sensible defaults.
|
|
9
|
+
# Extracted from `Pipeline` for SRP — Pipeline orchestrates
|
|
10
|
+
# rendering, this module owns metadata assembly.
|
|
11
|
+
class MetadataBuilder
|
|
12
|
+
XMP_PATH = "META-INF/metadata.xml"
|
|
13
|
+
|
|
14
|
+
def initialize(package)
|
|
15
|
+
@package = package
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns a hash with Producer/CreationDate defaults, overridden
|
|
19
|
+
# by any XMP-supplied fields (Title, Author, Subject, Keywords,
|
|
20
|
+
# Creator, CreationDate, ModDate). XMP dates are converted from
|
|
21
|
+
# ISO 8601 to PDF `D:YYYYMMDDHHmmss` format.
|
|
22
|
+
def build
|
|
23
|
+
defaults.merge(xmp_fields) { |_key, default, xmp| xmp || default }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def defaults
|
|
29
|
+
{
|
|
30
|
+
Producer: "idml gem v#{Idml::VERSION}",
|
|
31
|
+
CreationDate: pdf_date(Time.now.utc),
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def xmp_fields
|
|
36
|
+
return {} unless xmp_packet
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
Title: xmp_packet.title,
|
|
40
|
+
Author: xmp_packet.author,
|
|
41
|
+
Subject: xmp_packet.description,
|
|
42
|
+
Keywords: xmp_packet.keywords,
|
|
43
|
+
Creator: xmp_packet.creator_tool,
|
|
44
|
+
CreationDate: pdf_date_string(xmp_packet.create_date),
|
|
45
|
+
ModDate: pdf_date_string(xmp_packet.modify_date),
|
|
46
|
+
}.compact
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def xmp_packet
|
|
50
|
+
return nil unless @package.has_part?(XMP_PATH)
|
|
51
|
+
|
|
52
|
+
@xmp_packet ||= begin
|
|
53
|
+
xml = @package.read_part(XMP_PATH)
|
|
54
|
+
Parts::XmpMeta.from_xml(xml).rdf
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pdf_date_string(iso8601)
|
|
61
|
+
return nil unless iso8601
|
|
62
|
+
|
|
63
|
+
pdf_date(Time.iso8601(iso8601))
|
|
64
|
+
rescue ArgumentError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pdf_date(time)
|
|
69
|
+
time.strftime("D:%Y%m%d%H%M%S+00'00'")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
module Idml
|
|
4
4
|
module Render
|
|
5
5
|
# Top-level pipeline: IDML Package → PDF file using pdfrb.
|
|
6
|
-
# All PDF assembly is delegated to
|
|
7
|
-
#
|
|
6
|
+
# All PDF assembly is delegated to pdfrb; this class orchestrates
|
|
7
|
+
# the high-level flow:
|
|
8
|
+
#
|
|
9
|
+
# 1. Build writer + set metadata.
|
|
10
|
+
# 2. Apply compliance (PDF/A XMP + ICC) if requested.
|
|
11
|
+
# 3. Set up structure tracker for tagged PDF.
|
|
12
|
+
# 4. Resolve document font.
|
|
13
|
+
# 5. For each spread: collect images, render pages, emit hyperlinks.
|
|
14
|
+
# 6. Flush structure, emit bookmarks, subset fonts, write.
|
|
8
15
|
class Pipeline
|
|
9
16
|
DEFAULT_WIDTH = 612
|
|
10
17
|
DEFAULT_HEIGHT = 792
|
|
@@ -21,26 +28,24 @@ module Idml
|
|
|
21
28
|
|
|
22
29
|
def call
|
|
23
30
|
writer = PdfrbWriter.new
|
|
24
|
-
metadata =
|
|
31
|
+
metadata = MetadataBuilder.new(@package).build
|
|
25
32
|
writer.set_info(metadata)
|
|
26
33
|
writer.enable_tagged if @tagged
|
|
27
|
-
if pdfa_requested?
|
|
28
|
-
|
|
29
|
-
embed_pdfa_output_intent(writer)
|
|
30
|
-
end
|
|
34
|
+
apply_compliance(writer, metadata) if pdfa_requested?
|
|
35
|
+
|
|
31
36
|
structure = StructureTracker.new(enabled: @tagged)
|
|
32
37
|
layer_filter = LayerFilter.from_designmap(@package.designmap)
|
|
33
38
|
font_ref_resolver = FontReferenceResolver.build(@package)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
font_setup = FontSetup.new(package: @package,
|
|
40
|
+
font_search_paths: @font_search_paths)
|
|
41
|
+
font_resource = font_setup.register(writer)
|
|
42
|
+
font_metrics = font_setup.metrics_for(writer, font_resource)
|
|
38
43
|
|
|
44
|
+
page_index = -1
|
|
39
45
|
@package.spreads.each do |spread|
|
|
40
|
-
page_index =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
structure, page_index)
|
|
46
|
+
page_index = render_spread(writer, spread, layer_filter,
|
|
47
|
+
font_ref_resolver, font_resource,
|
|
48
|
+
font_metrics, structure, page_index)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
structure.flush(writer)
|
|
@@ -53,7 +58,8 @@ module Idml
|
|
|
53
58
|
|
|
54
59
|
private
|
|
55
60
|
|
|
56
|
-
def
|
|
61
|
+
def apply_compliance(writer, metadata)
|
|
62
|
+
PdfaPacket.attach(writer.document, metadata)
|
|
57
63
|
bytes = IccProfile.srgb_bytes
|
|
58
64
|
return unless bytes
|
|
59
65
|
|
|
@@ -67,24 +73,24 @@ module Idml
|
|
|
67
73
|
nil
|
|
68
74
|
end
|
|
69
75
|
|
|
76
|
+
def pdfa_requested?
|
|
77
|
+
@compliance&.to_s&.start_with?("pdfa")
|
|
78
|
+
end
|
|
79
|
+
|
|
70
80
|
def emit_bookmarks(writer)
|
|
71
81
|
BookmarkResolver.new(@package).each do |title, page_index|
|
|
72
82
|
writer.add_bookmark(title, page_index)
|
|
73
83
|
end
|
|
74
84
|
end
|
|
75
85
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def render_spread_pages(writer, spread, base_dir, layer_filter,
|
|
81
|
-
font_ref_resolver, font_resource, font_metrics,
|
|
82
|
-
structure, page_offset)
|
|
86
|
+
def render_spread(writer, spread, layer_filter, font_ref_resolver,
|
|
87
|
+
font_resource, font_metrics, structure, page_offset)
|
|
83
88
|
pages = spread.spread.flat_map(&:page)
|
|
84
|
-
image_refs = ImageCollector.new(writer: writer,
|
|
89
|
+
image_refs = ImageCollector.new(writer: writer,
|
|
90
|
+
base_dir: File.dirname(@package.path),
|
|
85
91
|
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
86
92
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
87
|
-
font_resource, font_metrics, structure
|
|
93
|
+
font_resource, font_metrics, structure)
|
|
88
94
|
current = page_offset
|
|
89
95
|
|
|
90
96
|
pages.each do |page|
|
|
@@ -95,20 +101,22 @@ module Idml
|
|
|
95
101
|
page_height: dims[:height],
|
|
96
102
|
image_refs: image_refs,
|
|
97
103
|
page_index: current)
|
|
98
|
-
emit_hyperlinks(writer, spread, current)
|
|
104
|
+
emit_hyperlinks(writer, spread, current, layer_filter)
|
|
99
105
|
end
|
|
100
106
|
current
|
|
101
107
|
end
|
|
102
108
|
|
|
103
|
-
def emit_hyperlinks(writer, spread, page_index)
|
|
109
|
+
def emit_hyperlinks(writer, spread, page_index, layer_filter)
|
|
104
110
|
HyperlinkEmitter.new(writer: writer, package: @package,
|
|
105
|
-
page_height: DEFAULT_HEIGHT
|
|
111
|
+
page_height: DEFAULT_HEIGHT,
|
|
112
|
+
layer_filter: layer_filter)
|
|
113
|
+
.emit_for(spread, page_index)
|
|
106
114
|
rescue StandardError
|
|
107
115
|
nil
|
|
108
116
|
end
|
|
109
117
|
|
|
110
118
|
def build_renderer(layer_filter, font_ref_resolver, font_resource,
|
|
111
|
-
font_metrics, structure
|
|
119
|
+
font_metrics, structure)
|
|
112
120
|
SpreadRenderer.new(
|
|
113
121
|
font_metrics: font_metrics,
|
|
114
122
|
font_ps_name: font_resource,
|
|
@@ -123,104 +131,6 @@ module Idml
|
|
|
123
131
|
{ width: page.width || DEFAULT_WIDTH,
|
|
124
132
|
height: page.height || DEFAULT_HEIGHT }
|
|
125
133
|
end
|
|
126
|
-
|
|
127
|
-
def build_font_metrics(writer, font_resource)
|
|
128
|
-
return nil if font_resource == Render::DEFAULT_FONT
|
|
129
|
-
|
|
130
|
-
TextEngine::PdfrbFontMetrics.new(writer.document.fonts, font_resource)
|
|
131
|
-
rescue StandardError
|
|
132
|
-
nil
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def register_font(writer)
|
|
136
|
-
path = resolve_document_font_path
|
|
137
|
-
return Render::DEFAULT_FONT unless path
|
|
138
|
-
|
|
139
|
-
writer.register_font(path)
|
|
140
|
-
rescue StandardError
|
|
141
|
-
Render::DEFAULT_FONT
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def font_resolver
|
|
145
|
-
@font_resolver ||= Pdfrb::FontResolver.new(
|
|
146
|
-
search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
|
|
147
|
-
)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def resolve_document_font_path
|
|
151
|
-
return nil unless @package&.fonts
|
|
152
|
-
|
|
153
|
-
@package.fonts.font_family.each do |family|
|
|
154
|
-
path = find_font_file(family)
|
|
155
|
-
return path if path
|
|
156
|
-
end
|
|
157
|
-
nil
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def find_font_file(family)
|
|
161
|
-
family.font.each do |font|
|
|
162
|
-
next unless font.post_script_name
|
|
163
|
-
next if font.status == "Missing"
|
|
164
|
-
|
|
165
|
-
path = font_resolver.find_by_ps_name(font.post_script_name)
|
|
166
|
-
return path if path
|
|
167
|
-
end
|
|
168
|
-
nil
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def combined_metadata
|
|
172
|
-
defaults = default_metadata
|
|
173
|
-
xmp_metadata.each do |key, value|
|
|
174
|
-
next if value.nil? || value.empty?
|
|
175
|
-
|
|
176
|
-
defaults[key] = value
|
|
177
|
-
end
|
|
178
|
-
defaults
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def xmp_metadata
|
|
182
|
-
return {} unless @package.has_part?(XMP_PATH)
|
|
183
|
-
|
|
184
|
-
xml = @package.read_part(XMP_PATH)
|
|
185
|
-
meta = Parts::XmpMeta.from_xml(xml)
|
|
186
|
-
rdf = meta.rdf
|
|
187
|
-
return {} unless rdf
|
|
188
|
-
|
|
189
|
-
{
|
|
190
|
-
Title: rdf.title,
|
|
191
|
-
Author: rdf.author,
|
|
192
|
-
Subject: rdf.description,
|
|
193
|
-
Keywords: rdf.keywords,
|
|
194
|
-
Creator: rdf.creator_tool,
|
|
195
|
-
CreationDate: pdf_date_string(rdf.create_date),
|
|
196
|
-
ModDate: pdf_date_string(rdf.modify_date),
|
|
197
|
-
}
|
|
198
|
-
rescue StandardError
|
|
199
|
-
{}
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def pdf_date_string(iso8601)
|
|
203
|
-
return nil unless iso8601
|
|
204
|
-
|
|
205
|
-
time = Time.iso8601(iso8601)
|
|
206
|
-
pdf_date(time)
|
|
207
|
-
rescue ArgumentError
|
|
208
|
-
nil
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def default_metadata
|
|
212
|
-
{
|
|
213
|
-
Producer: "idml gem v#{Idml::VERSION}",
|
|
214
|
-
CreationDate: pdf_date(Time.now.utc),
|
|
215
|
-
}
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def pdf_date(time)
|
|
219
|
-
time.strftime("D:%Y%m%d%H%M%S+00'00'")
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
XMP_PATH = "META-INF/metadata.xml"
|
|
223
|
-
private_constant :XMP_PATH
|
|
224
134
|
end
|
|
225
135
|
end
|
|
226
136
|
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -23,6 +23,8 @@ module Idml
|
|
|
23
23
|
autoload :BookmarkResolver, "#{__dir__}/render/bookmark_resolver"
|
|
24
24
|
autoload :HyperlinkResolver, "#{__dir__}/render/hyperlink_resolver"
|
|
25
25
|
autoload :HyperlinkEmitter, "#{__dir__}/render/hyperlink_emitter"
|
|
26
|
+
autoload :MetadataBuilder, "#{__dir__}/render/metadata_builder"
|
|
27
|
+
autoload :FontSetup, "#{__dir__}/render/font_setup"
|
|
26
28
|
autoload :StoryThreader, "#{__dir__}/render/story_threader"
|
|
27
29
|
autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
|
|
28
30
|
autoload :SpreadRenderer, "#{__dir__}/render/spread_renderer"
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -209,6 +209,10 @@ files:
|
|
|
209
209
|
- TODO.pdf/80-paragraph-alignment.md
|
|
210
210
|
- TODO.pdf/81-pdfa-icc-output-intent.md
|
|
211
211
|
- TODO.pdf/82-table-cell-text.md
|
|
212
|
+
- TODO.pdf/83-pipeline-decomposition.md
|
|
213
|
+
- TODO.pdf/84-schema-faithful-table.md
|
|
214
|
+
- TODO.pdf/85-per-text-range-hyperlinks.md
|
|
215
|
+
- TODO.pdf/86-caching-performance.md
|
|
212
216
|
- TODO.pdf/README.md
|
|
213
217
|
- exe/idml
|
|
214
218
|
- idml.gemspec
|
|
@@ -255,6 +259,7 @@ files:
|
|
|
255
259
|
- lib/idml/elements/group.rb
|
|
256
260
|
- lib/idml/elements/hyperlink.rb
|
|
257
261
|
- lib/idml/elements/hyperlink_page_destination.rb
|
|
262
|
+
- lib/idml/elements/hyperlink_text_source.rb
|
|
258
263
|
- lib/idml/elements/hyperlink_url_destination.rb
|
|
259
264
|
- lib/idml/elements/image.rb
|
|
260
265
|
- lib/idml/elements/ink.rb
|
|
@@ -383,6 +388,7 @@ files:
|
|
|
383
388
|
- lib/idml/render/color_helper.rb
|
|
384
389
|
- lib/idml/render/color_resolver.rb
|
|
385
390
|
- lib/idml/render/font_reference_resolver.rb
|
|
391
|
+
- lib/idml/render/font_setup.rb
|
|
386
392
|
- lib/idml/render/gradient_resolver.rb
|
|
387
393
|
- lib/idml/render/hyperlink_emitter.rb
|
|
388
394
|
- lib/idml/render/hyperlink_resolver.rb
|
|
@@ -390,6 +396,7 @@ files:
|
|
|
390
396
|
- lib/idml/render/image.rb
|
|
391
397
|
- lib/idml/render/image_collector.rb
|
|
392
398
|
- lib/idml/render/layer_filter.rb
|
|
399
|
+
- lib/idml/render/metadata_builder.rb
|
|
393
400
|
- lib/idml/render/page_item_renderer.rb
|
|
394
401
|
- lib/idml/render/pdfa_packet.rb
|
|
395
402
|
- lib/idml/render/pdfrb_writer.rb
|