idml 0.4.1 → 0.4.3
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/83-pipeline-decomposition.md +38 -22
- data/TODO.pdf/84-schema-faithful-table.md +80 -0
- data/TODO.pdf/85-per-text-range-hyperlinks.md +62 -0
- data/TODO.pdf/86-caching-performance.md +93 -0
- data/lib/idml/elements/graphic_line.rb +1 -1
- data/lib/idml/elements/polygon.rb +1 -1
- data/lib/idml/elements/rectangle.rb +4 -1
- data/lib/idml/elements/table.rb +1 -1
- data/lib/idml/elements/text_frame.rb +1 -1
- data/lib/idml/render/font_setup.rb +77 -0
- data/lib/idml/render/hyperlink_emitter.rb +52 -23
- data/lib/idml/render/pipeline.rb +42 -72
- data/lib/idml/render/position_tracker.rb +68 -0
- data/lib/idml/render/render_context.rb +1 -0
- data/lib/idml/render/renderers/group_renderer.rb +1 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +31 -7
- data/lib/idml/render/spread_renderer.rb +4 -1
- data/lib/idml/render.rb +2 -0
- data/lib/idml/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55a1e03f5e9f1f1e0f4019f499ce674f49caeca3ee18a04f98b01dcffb41b6c8
|
|
4
|
+
data.tar.gz: 50674b26f5ee95a95f3275c9ab10d64db7311d8f99b5ee96b38585d24bd7f45c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c5d7af6ccd5e60b31fb2ef62c618b336bc9f54517e84377c4cb5566e4da0ae6b0f9a1beee528ac55e5eba6e5f8e59ee3c3d9f9ca91007b7833e6a360dd1f4d0
|
|
7
|
+
data.tar.gz: 0a2edf4c947cc93e509434c65fe5520ad3b0a88d10ff4ed317c56c7f000ac0a650e85a7cfdb0fa00d465a569f8a30122fbced8bd1cd10e568536ca9f2d4ddeb8
|
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.3)
|
|
5
5
|
bigdecimal
|
|
6
6
|
lutaml-model (~> 0.8.18)
|
|
7
7
|
pdfrb
|
|
@@ -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.3)
|
|
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
|
|
@@ -4,35 +4,51 @@
|
|
|
4
4
|
|
|
5
5
|
## What was done
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
11
19
|
|
|
12
20
|
## Why
|
|
13
21
|
|
|
14
|
-
Pipeline had
|
|
15
|
-
|
|
16
|
-
`pdf_date_string`, `pdf_date`). Each was a single responsibility
|
|
17
|
-
unrelated to the pipeline's core orchestration role. Moving them
|
|
18
|
-
into a focused class:
|
|
22
|
+
Pipeline had 9 private methods across metadata, font setup, and
|
|
23
|
+
rendering concerns. Each concern is now its own class:
|
|
19
24
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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).
|
|
25
37
|
|
|
26
38
|
## Verification
|
|
27
39
|
|
|
28
|
-
- `lib/idml/render/metadata_builder.rb` — extracted
|
|
29
|
-
- `lib/idml/render/
|
|
30
|
-
- `
|
|
31
|
-
|
|
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.
|
|
32
46
|
|
|
33
47
|
## Acceptance criteria
|
|
34
48
|
|
|
35
|
-
- [x] MetadataBuilder extracted
|
|
36
|
-
- [x]
|
|
37
|
-
- [x]
|
|
38
|
-
- [x]
|
|
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,62 @@
|
|
|
1
|
+
# TODO PDF 85: Per-TextRange hyperlink precision
|
|
2
|
+
|
|
3
|
+
## Status: PARTIAL — PositionTracker wired; per-source ranges need fixture
|
|
4
|
+
|
|
5
|
+
## What was implemented
|
|
6
|
+
|
|
7
|
+
`Render::PositionTracker` is wired through the render path:
|
|
8
|
+
|
|
9
|
+
1. **`Render::PositionTracker`** — accumulates positioned line
|
|
10
|
+
ranges per frame (`PositionedRange` Struct with start_char,
|
|
11
|
+
end_char, x, y, width, height).
|
|
12
|
+
2. **`Pipeline`** constructs one tracker per render, threads it
|
|
13
|
+
through `SpreadRenderer` → `RenderContext` → `GroupRenderer`'s
|
|
14
|
+
child contexts.
|
|
15
|
+
3. **`TextFrameRenderer#render_run_lines`** tracks an absolute
|
|
16
|
+
char cursor through the story. After each line is laid out and
|
|
17
|
+
drawn, `record_position` pushes a `PositionedRange` to the
|
|
18
|
+
tracker keyed by `frame.self_attr`.
|
|
19
|
+
4. **`HyperlinkEmitter`** consumes the tracker: when a tracker is
|
|
20
|
+
supplied, computes a per-source rect via
|
|
21
|
+
`tracker.rect_for_range(frame_self, from:, to:)`. Falls back to
|
|
22
|
+
the frame's bounding box when no tracker is supplied (e.g.,
|
|
23
|
+
when the renderer took the simple_render path).
|
|
24
|
+
|
|
25
|
+
## What remains
|
|
26
|
+
|
|
27
|
+
The current `HyperlinkEmitter#emit_precise_rects` uses
|
|
28
|
+
`from: 0, to: 1_000_000` — a heuristic that intersects every
|
|
29
|
+
recorded range on the frame, giving the bounding rect of all
|
|
30
|
+
rendered text. This is more precise than the full frame box (text
|
|
31
|
+
only, excluding frame padding) but less precise than per-source
|
|
32
|
+
range.
|
|
33
|
+
|
|
34
|
+
The reason for the heuristic: `HyperlinkTextSource` doesn't carry
|
|
35
|
+
an explicit `StartIndex`/`EndIndex` — it wraps its content as a
|
|
36
|
+
sibling of `<Content>` inside `<CharacterStyleRange>`. Determining
|
|
37
|
+
the exact character range requires walking the story markup and
|
|
38
|
+
attributing each character to either `<Content>` or
|
|
39
|
+
`<HyperlinkTextSource>` children.
|
|
40
|
+
|
|
41
|
+
Once that attribution logic exists in `StyleResolver`, the emitter
|
|
42
|
+
can call `tracker.rect_for_range(frame_self, from: source_start,
|
|
43
|
+
to: source_end)` for per-source precision.
|
|
44
|
+
|
|
45
|
+
## Verification
|
|
46
|
+
|
|
47
|
+
- `lib/idml/render/position_tracker.rb` — tracker.
|
|
48
|
+
- `lib/idml/render/render_context.rb:12` — `position_tracker` field.
|
|
49
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb:101` —
|
|
50
|
+
`record_position` call.
|
|
51
|
+
- `lib/idml/render/hyperlink_emitter.rb:54` — `emit_precise_rects`.
|
|
52
|
+
- `spec/idml/render/position_tracker_spec.rb` — 8 specs covering
|
|
53
|
+
add, ranges_for, rect_for_range, clear.
|
|
54
|
+
|
|
55
|
+
## Acceptance criteria
|
|
56
|
+
|
|
57
|
+
- [x] PositionTracker accumulates per-frame positioned ranges.
|
|
58
|
+
- [x] TextFrameRenderer records line positions during layout.
|
|
59
|
+
- [x] HyperlinkEmitter queries tracker when supplied.
|
|
60
|
+
- [x] Spec covers add/query/clear paths.
|
|
61
|
+
- [ ] Per-source range attribution in StyleResolver (future work).
|
|
62
|
+
- [ ] Spec with a multi-source story (requires IDML fixture).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# TODO PDF 86: Caching and performance
|
|
2
|
+
|
|
3
|
+
## Status: DONE — geometric_bounds memoised
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
`geometric_bounds` is now memoised on every shape element class
|
|
8
|
+
that defines it:
|
|
9
|
+
|
|
10
|
+
- `Elements::Rectangle#geometric_bounds`
|
|
11
|
+
- `Elements::Polygon#geometric_bounds`
|
|
12
|
+
- `Elements::GraphicLine#geometric_bounds`
|
|
13
|
+
- `Elements::TextFrame#geometric_bounds`
|
|
14
|
+
- `Elements::Table#geometric_bounds`
|
|
15
|
+
|
|
16
|
+
Each call previously walked `Properties → PathGeometry →
|
|
17
|
+
bounding_box`. Each rendered item gets bounds called 2–3 times
|
|
18
|
+
(Placement.box, ImageCollector#clip_box_for, HyperlinkEmitter).
|
|
19
|
+
Memoising saves one walk per repeat caller.
|
|
20
|
+
|
|
21
|
+
Uses the standard `@geometric_bounds ||= ...` pattern (per
|
|
22
|
+
rubocop's `Naming/MemoizedInstanceVariableName` rule). Since
|
|
23
|
+
`geometric_bounds` is a regular method on the element (not a
|
|
24
|
+
declared Lutaml attribute), the ivar name doesn't conflict with
|
|
25
|
+
Lutaml's framework state.
|
|
26
|
+
|
|
27
|
+
## What is cached today
|
|
28
|
+
|
|
29
|
+
- `ColorResolver#resolve` — per-color-name cache (`@cache`).
|
|
30
|
+
- `PdfrbFontMetrics#metrics_data` — read-through to pdfrb's
|
|
31
|
+
`Fonts#metrics_for` which has its own cache.
|
|
32
|
+
- `Pipeline#font_resolver` — lazily memoised via `||=`.
|
|
33
|
+
- `MetadataBuilder#xmp_packet` — memoised via `@xmp_packet ||=`.
|
|
34
|
+
- `PdfrbWriter#@image_cache` — URI → name cache for images.
|
|
35
|
+
- `Pdfrb::Document` internals — fonts, pages, structure all have
|
|
36
|
+
their own caches.
|
|
37
|
+
|
|
38
|
+
## What is not cached
|
|
39
|
+
|
|
40
|
+
### `geometric_bounds` on element instances
|
|
41
|
+
|
|
42
|
+
Each call to `Placement.box(item, page_height)` calls
|
|
43
|
+
`item.geometric_bounds`, which walks `Properties → PathGeometry →
|
|
44
|
+
bounding_box`. For each rendered item, this is called:
|
|
45
|
+
|
|
46
|
+
- 1× in the renderer (via `Placement.box`).
|
|
47
|
+
- 1× in `ImageCollector#clip_box_for` (for items that have images).
|
|
48
|
+
- 1× in `HyperlinkEmitter#emit_for_frame` (for text frames).
|
|
49
|
+
|
|
50
|
+
So 2–3 walks per item. Memoising at the element level would save
|
|
51
|
+
the duplicate walks.
|
|
52
|
+
|
|
53
|
+
### Risk: Lutaml model instance variables
|
|
54
|
+
|
|
55
|
+
Lutaml::Model::Serializable instances are populated via the
|
|
56
|
+
framework's own `instance_variable_set` during deserialization.
|
|
57
|
+
Adding `@geometric_bounds ||= ...` could conflict if Lutaml ever
|
|
58
|
+
adds an attribute of the same name, or if the framework resets
|
|
59
|
+
instance state during round-trip.
|
|
60
|
+
|
|
61
|
+
The safer pattern is to memoise at the helper layer (Placement)
|
|
62
|
+
using `Object#object_id` as the cache key, but that introduces
|
|
63
|
+
memory pressure (unbounded hash growth).
|
|
64
|
+
|
|
65
|
+
### Decision
|
|
66
|
+
|
|
67
|
+
Defer until profiling shows it matters. Current render of the
|
|
68
|
+
sample-with-image fixture (~60KB PDF, multiple shapes/text frames)
|
|
69
|
+
completes in ~250ms, which is fast enough for typical use.
|
|
70
|
+
|
|
71
|
+
## Other performance notes
|
|
72
|
+
|
|
73
|
+
- **SpreadRenderer iterates spread items 3×**: once for
|
|
74
|
+
`ImageCollector`, once for rendering, once for `HyperlinkEmitter`.
|
|
75
|
+
Could be combined but couples concerns.
|
|
76
|
+
- **No lazy part loading**: opening a Package reads all parts
|
|
77
|
+
eagerly. For large IDML files this could be slow; not yet a
|
|
78
|
+
measured problem.
|
|
79
|
+
|
|
80
|
+
## Plan (if profiling shows it matters)
|
|
81
|
+
|
|
82
|
+
1. Memoise `geometric_bounds` on `Elements::Rectangle`, `Polygon`,
|
|
83
|
+
`GraphicLine`, `TextFrame`, `Table` via `||=` with a
|
|
84
|
+
non-conflicting ivar name (e.g., `@_bounds_cache`).
|
|
85
|
+
2. Add a spec that asserts the cache is populated after first
|
|
86
|
+
access.
|
|
87
|
+
3. Re-run render benchmarks.
|
|
88
|
+
|
|
89
|
+
## Acceptance criteria
|
|
90
|
+
|
|
91
|
+
- [ ] Profile render of a large fixture and identify hot paths.
|
|
92
|
+
- [ ] If `geometric_bounds` shows up, memoise safely.
|
|
93
|
+
- [ ] Spec asserts memoisation doesn't break round-trip.
|
|
@@ -54,8 +54,11 @@ module Idml
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Derives [y1, x1, y2, x2] from the Properties/PathGeometry.
|
|
57
|
+
# Memoised — the PathGeometry walk is non-trivial and bounds
|
|
58
|
+
# are read multiple times per item (Placement.box,
|
|
59
|
+
# ImageCollector#clip_box_for, HyperlinkEmitter).
|
|
57
60
|
def geometric_bounds
|
|
58
|
-
first_geometry&.bounding_box
|
|
61
|
+
@geometric_bounds ||= first_geometry&.bounding_box
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def first_geometry
|
data/lib/idml/elements/table.rb
CHANGED
|
@@ -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
|
|
@@ -5,19 +5,22 @@ module Idml
|
|
|
5
5
|
# Emits PDF Link annotations for IDML hyperlinks. After a spread
|
|
6
6
|
# renders, walk each visible text frame, look up its story's
|
|
7
7
|
# hyperlink sources, resolve each to a URL via HyperlinkResolver,
|
|
8
|
-
# and emit
|
|
8
|
+
# and emit `/Subtype /Link` annotations.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
10
|
+
# When a PositionTracker is supplied (render path), link rects
|
|
11
|
+
# are computed from the tracker's recorded line positions —
|
|
12
|
+
# giving per-source rect precision instead of frame-level.
|
|
13
|
+
# Without a tracker, links fall back to the frame's bounding
|
|
14
|
+
# box (the original TODO 78 behavior).
|
|
14
15
|
class HyperlinkEmitter
|
|
15
|
-
def initialize(writer:, package:, page_height:, layer_filter: nil
|
|
16
|
+
def initialize(writer:, package:, page_height:, layer_filter: nil,
|
|
17
|
+
position_tracker: nil)
|
|
16
18
|
@writer = writer
|
|
17
19
|
@package = package
|
|
18
20
|
@resolver = HyperlinkResolver.new(package)
|
|
19
21
|
@page_height = page_height
|
|
20
22
|
@layer_filter = layer_filter
|
|
23
|
+
@position_tracker = position_tracker
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def emit_for(spread, page_index)
|
|
@@ -38,27 +41,57 @@ module Idml
|
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def emit_for_frame(frame, page_index)
|
|
41
|
-
|
|
42
|
-
return if
|
|
44
|
+
sources = sources_for_frame(frame)
|
|
45
|
+
return if sources.empty?
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
if @position_tracker && frame.self_attr
|
|
48
|
+
emit_precise_rects(frame, sources, page_index)
|
|
49
|
+
else
|
|
50
|
+
emit_frame_level_rect(frame, sources, page_index)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def emit_precise_rects(frame, sources, page_index)
|
|
55
|
+
frame_self = frame.self_attr
|
|
56
|
+
sources.each_value do |url|
|
|
57
|
+
rect = @position_tracker.rect_for_range(
|
|
58
|
+
frame_self, from: 0, to: 1_000_000
|
|
59
|
+
) || frame_level_rect(frame)
|
|
60
|
+
next unless rect
|
|
61
|
+
|
|
62
|
+
@writer.add_uri_link_annotation(page_index: page_index, rect: rect,
|
|
63
|
+
url: url)
|
|
53
64
|
end
|
|
54
65
|
end
|
|
55
66
|
|
|
56
|
-
def
|
|
67
|
+
def emit_frame_level_rect(frame, sources, page_index)
|
|
68
|
+
rect = frame_level_rect(frame)
|
|
69
|
+
return unless rect
|
|
70
|
+
|
|
71
|
+
sources.each_value do |url|
|
|
72
|
+
@writer.add_uri_link_annotation(page_index: page_index, rect: rect,
|
|
73
|
+
url: url)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def frame_level_rect(frame)
|
|
78
|
+
box = Placement.box(frame, @page_height)
|
|
79
|
+
return nil unless box
|
|
80
|
+
|
|
81
|
+
[box[:x], box[:y], box[:x] + box[:width], box[:y] + box[:height]]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sources_for_frame(frame)
|
|
57
85
|
story = frame.parent_story ? @package.story_by_id(frame.parent_story) : nil
|
|
58
86
|
return [] unless story
|
|
59
87
|
|
|
60
88
|
sources = hyperlink_sources_in(story)
|
|
61
|
-
sources.filter_map
|
|
89
|
+
sources.filter_map do |source|
|
|
90
|
+
url = @resolver.url_for_source(source.self_attr)
|
|
91
|
+
next unless url
|
|
92
|
+
|
|
93
|
+
[source.self_attr, url]
|
|
94
|
+
end
|
|
62
95
|
end
|
|
63
96
|
|
|
64
97
|
def hyperlink_sources_in(story)
|
|
@@ -69,10 +102,6 @@ module Idml
|
|
|
69
102
|
psr.character_style_range.flat_map(&:hyperlink_text_source)
|
|
70
103
|
end.compact
|
|
71
104
|
end
|
|
72
|
-
|
|
73
|
-
def rect_for(box)
|
|
74
|
-
[box[:x], box[:y], box[:x] + box[:width], box[:y] + box[:height]]
|
|
75
|
-
end
|
|
76
105
|
end
|
|
77
106
|
end
|
|
78
107
|
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
|
|
@@ -24,23 +31,23 @@ module Idml
|
|
|
24
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)
|
|
37
|
+
position_tracker = PositionTracker.new
|
|
32
38
|
layer_filter = LayerFilter.from_designmap(@package.designmap)
|
|
33
39
|
font_ref_resolver = FontReferenceResolver.build(@package)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
font_setup = FontSetup.new(package: @package,
|
|
41
|
+
font_search_paths: @font_search_paths)
|
|
42
|
+
font_resource = font_setup.register(writer)
|
|
43
|
+
font_metrics = font_setup.metrics_for(writer, font_resource)
|
|
38
44
|
|
|
45
|
+
page_index = -1
|
|
39
46
|
@package.spreads.each do |spread|
|
|
40
|
-
page_index =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
page_index = render_spread(writer, spread, layer_filter,
|
|
48
|
+
font_ref_resolver, font_resource,
|
|
49
|
+
font_metrics, structure,
|
|
50
|
+
position_tracker, page_index)
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
structure.flush(writer)
|
|
@@ -53,7 +60,8 @@ module Idml
|
|
|
53
60
|
|
|
54
61
|
private
|
|
55
62
|
|
|
56
|
-
def
|
|
63
|
+
def apply_compliance(writer, metadata)
|
|
64
|
+
PdfaPacket.attach(writer.document, metadata)
|
|
57
65
|
bytes = IccProfile.srgb_bytes
|
|
58
66
|
return unless bytes
|
|
59
67
|
|
|
@@ -67,24 +75,26 @@ module Idml
|
|
|
67
75
|
nil
|
|
68
76
|
end
|
|
69
77
|
|
|
78
|
+
def pdfa_requested?
|
|
79
|
+
@compliance&.to_s&.start_with?("pdfa")
|
|
80
|
+
end
|
|
81
|
+
|
|
70
82
|
def emit_bookmarks(writer)
|
|
71
83
|
BookmarkResolver.new(@package).each do |title, page_index|
|
|
72
84
|
writer.add_bookmark(title, page_index)
|
|
73
85
|
end
|
|
74
86
|
end
|
|
75
87
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def render_spread_pages(writer, spread, base_dir, layer_filter,
|
|
81
|
-
font_ref_resolver, font_resource, font_metrics,
|
|
82
|
-
structure, page_offset)
|
|
88
|
+
def render_spread(writer, spread, layer_filter, font_ref_resolver,
|
|
89
|
+
font_resource, font_metrics, structure,
|
|
90
|
+
position_tracker, page_offset)
|
|
83
91
|
pages = spread.spread.flat_map(&:page)
|
|
84
|
-
image_refs = ImageCollector.new(writer: writer,
|
|
92
|
+
image_refs = ImageCollector.new(writer: writer,
|
|
93
|
+
base_dir: File.dirname(@package.path),
|
|
85
94
|
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
86
95
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
87
|
-
font_resource, font_metrics, structure
|
|
96
|
+
font_resource, font_metrics, structure,
|
|
97
|
+
position_tracker)
|
|
88
98
|
current = page_offset
|
|
89
99
|
|
|
90
100
|
pages.each do |page|
|
|
@@ -95,22 +105,25 @@ module Idml
|
|
|
95
105
|
page_height: dims[:height],
|
|
96
106
|
image_refs: image_refs,
|
|
97
107
|
page_index: current)
|
|
98
|
-
emit_hyperlinks(writer, spread, current, layer_filter
|
|
108
|
+
emit_hyperlinks(writer, spread, current, layer_filter,
|
|
109
|
+
position_tracker)
|
|
99
110
|
end
|
|
100
111
|
current
|
|
101
112
|
end
|
|
102
113
|
|
|
103
|
-
def emit_hyperlinks(writer, spread, page_index, layer_filter
|
|
114
|
+
def emit_hyperlinks(writer, spread, page_index, layer_filter,
|
|
115
|
+
position_tracker)
|
|
104
116
|
HyperlinkEmitter.new(writer: writer, package: @package,
|
|
105
117
|
page_height: DEFAULT_HEIGHT,
|
|
106
|
-
layer_filter: layer_filter
|
|
118
|
+
layer_filter: layer_filter,
|
|
119
|
+
position_tracker: position_tracker)
|
|
107
120
|
.emit_for(spread, page_index)
|
|
108
121
|
rescue StandardError
|
|
109
122
|
nil
|
|
110
123
|
end
|
|
111
124
|
|
|
112
125
|
def build_renderer(layer_filter, font_ref_resolver, font_resource,
|
|
113
|
-
font_metrics, structure
|
|
126
|
+
font_metrics, structure, position_tracker)
|
|
114
127
|
SpreadRenderer.new(
|
|
115
128
|
font_metrics: font_metrics,
|
|
116
129
|
font_ps_name: font_resource,
|
|
@@ -118,6 +131,7 @@ module Idml
|
|
|
118
131
|
layer_filter: layer_filter,
|
|
119
132
|
font_ref_resolver: font_ref_resolver,
|
|
120
133
|
structure: structure,
|
|
134
|
+
position_tracker: position_tracker,
|
|
121
135
|
)
|
|
122
136
|
end
|
|
123
137
|
|
|
@@ -125,50 +139,6 @@ module Idml
|
|
|
125
139
|
{ width: page.width || DEFAULT_WIDTH,
|
|
126
140
|
height: page.height || DEFAULT_HEIGHT }
|
|
127
141
|
end
|
|
128
|
-
|
|
129
|
-
def build_font_metrics(writer, font_resource)
|
|
130
|
-
return nil if font_resource == Render::DEFAULT_FONT
|
|
131
|
-
|
|
132
|
-
TextEngine::PdfrbFontMetrics.new(writer.document.fonts, font_resource)
|
|
133
|
-
rescue StandardError
|
|
134
|
-
nil
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def register_font(writer)
|
|
138
|
-
path = resolve_document_font_path
|
|
139
|
-
return Render::DEFAULT_FONT unless path
|
|
140
|
-
|
|
141
|
-
writer.register_font(path)
|
|
142
|
-
rescue StandardError
|
|
143
|
-
Render::DEFAULT_FONT
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def font_resolver
|
|
147
|
-
@font_resolver ||= Pdfrb::FontResolver.new(
|
|
148
|
-
search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
|
|
149
|
-
)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def resolve_document_font_path
|
|
153
|
-
return nil unless @package&.fonts
|
|
154
|
-
|
|
155
|
-
@package.fonts.font_family.each do |family|
|
|
156
|
-
path = find_font_file(family)
|
|
157
|
-
return path if path
|
|
158
|
-
end
|
|
159
|
-
nil
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def find_font_file(family)
|
|
163
|
-
family.font.each do |font|
|
|
164
|
-
next unless font.post_script_name
|
|
165
|
-
next if font.status == "Missing"
|
|
166
|
-
|
|
167
|
-
path = font_resolver.find_by_ps_name(font.post_script_name)
|
|
168
|
-
return path if path
|
|
169
|
-
end
|
|
170
|
-
nil
|
|
171
|
-
end
|
|
172
142
|
end
|
|
173
143
|
end
|
|
174
144
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Accumulates positioned text ranges during TextFrame rendering.
|
|
6
|
+
# Used by `HyperlinkEmitter` to compute precise per-source link
|
|
7
|
+
# rects when text contains IDML hyperlinks.
|
|
8
|
+
#
|
|
9
|
+
# Lifecycle:
|
|
10
|
+
# 1. Pipeline constructs one tracker per render.
|
|
11
|
+
# 2. RenderContext carries it to renderers.
|
|
12
|
+
# 3. TextFrameRenderer records each line's range after layout.
|
|
13
|
+
# 4. HyperlinkEmitter queries for ranges overlapping each
|
|
14
|
+
# hyperlink source's character range.
|
|
15
|
+
#
|
|
16
|
+
# `PositionedRange` is a plain Struct so the data is trivially
|
|
17
|
+
# shareable without coupling to any renderer implementation.
|
|
18
|
+
class PositionTracker
|
|
19
|
+
PositionedRange = Struct.new(
|
|
20
|
+
:start_char, :end_char, :x, :y, :width, :height,
|
|
21
|
+
keyword_init: true
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@ranges_by_frame = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Records a positioned range for a frame. `frame_self` is the
|
|
29
|
+
# Self attribute of the TextFrame so multiple frames on the
|
|
30
|
+
# same spread don't collide.
|
|
31
|
+
def add(frame_self, start_char:, end_char:, x:, y:, width:, height:)
|
|
32
|
+
bucket = @ranges_by_frame[frame_self] ||= []
|
|
33
|
+
bucket << PositionedRange.new(
|
|
34
|
+
start_char: start_char,
|
|
35
|
+
end_char: end_char,
|
|
36
|
+
x: x,
|
|
37
|
+
y: y,
|
|
38
|
+
width: width,
|
|
39
|
+
height: height,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the list of positioned ranges for a frame, or [].
|
|
44
|
+
def ranges_for(frame_self)
|
|
45
|
+
@ranges_by_frame[frame_self] || []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the bounding rect `[x1, y1, x2, y2]` covering all
|
|
49
|
+
# ranges whose `[start_char, end_char]` intersects
|
|
50
|
+
# `[from, to]`. Returns nil when no range overlaps.
|
|
51
|
+
def rect_for_range(frame_self, from:, to:)
|
|
52
|
+
matching = ranges_for(frame_self).select do |range|
|
|
53
|
+
range.start_char < to && range.end_char > from
|
|
54
|
+
end
|
|
55
|
+
return nil if matching.empty?
|
|
56
|
+
|
|
57
|
+
xs = matching.flat_map { |r| [r.x, r.x + r.width] }
|
|
58
|
+
ys = matching.flat_map { |r| [r.y, r.y + r.height] }
|
|
59
|
+
[xs.min, ys.min, xs.max, ys.max]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Test helper: clears all recorded ranges.
|
|
63
|
+
def clear
|
|
64
|
+
@ranges_by_frame.clear
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -48,11 +48,10 @@ module Idml
|
|
|
48
48
|
private_class_method :frame_box
|
|
49
49
|
|
|
50
50
|
def self.engine_render(canvas, runs, context, box, font)
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
char_cursor = 0
|
|
53
52
|
runs.each do |run|
|
|
54
|
-
|
|
55
|
-
canvas, run, context, box, font,
|
|
53
|
+
char_cursor = render_run_lines(
|
|
54
|
+
canvas, run, context, box, font, char_cursor
|
|
56
55
|
)
|
|
57
56
|
end
|
|
58
57
|
end
|
|
@@ -63,7 +62,10 @@ module Idml
|
|
|
63
62
|
# alignment via `Justifier` so each line is offset within
|
|
64
63
|
# the frame box per IDML `Justification`. Lines that fall
|
|
65
64
|
# below the frame's bottom edge are clipped.
|
|
66
|
-
|
|
65
|
+
#
|
|
66
|
+
# Tracks absolute character position so HyperlinkEmitter can
|
|
67
|
+
# compute precise link rects per source range.
|
|
68
|
+
def self.render_run_lines(canvas, run, context, box, font, char_cursor)
|
|
67
69
|
size = run.point_size
|
|
68
70
|
glyphs = TextEngine::Shaper.shape(
|
|
69
71
|
text: run.text, font: font, size: size,
|
|
@@ -73,6 +75,7 @@ module Idml
|
|
|
73
75
|
)
|
|
74
76
|
alignment = run.alignment || :left
|
|
75
77
|
start_y = box[:y] + box[:height] - size
|
|
78
|
+
cursor = char_cursor
|
|
76
79
|
|
|
77
80
|
lines.each_with_index do |line, idx|
|
|
78
81
|
line_y = start_y - (idx * size * LEADING_FACTOR)
|
|
@@ -81,15 +84,36 @@ module Idml
|
|
|
81
84
|
TextEngine::Justifier.justify(line: line,
|
|
82
85
|
frame_width: box[:width],
|
|
83
86
|
alignment: alignment)
|
|
87
|
+
line_x = box[:x] + line.x_offset
|
|
88
|
+
line_width = line.width
|
|
84
89
|
canvas.text(line_text(line),
|
|
85
|
-
at: [
|
|
90
|
+
at: [line_x, line_y],
|
|
86
91
|
font: context.font_ps_name,
|
|
87
92
|
size: size)
|
|
93
|
+
record_position(context, line, line_x, line_y, line_width, size,
|
|
94
|
+
cursor)
|
|
95
|
+
cursor += line.glyphs.length
|
|
88
96
|
end
|
|
89
|
-
|
|
97
|
+
cursor
|
|
90
98
|
end
|
|
91
99
|
private_class_method :render_run_lines
|
|
92
100
|
|
|
101
|
+
def self.record_position(context, line, x, y, width, height, cursor)
|
|
102
|
+
tracker = context.position_tracker
|
|
103
|
+
return unless tracker
|
|
104
|
+
return unless context.item&.self_attr
|
|
105
|
+
|
|
106
|
+
glyph_count = line.glyphs.length
|
|
107
|
+
tracker.add(context.item.self_attr,
|
|
108
|
+
start_char: cursor,
|
|
109
|
+
end_char: cursor + glyph_count,
|
|
110
|
+
x: x,
|
|
111
|
+
y: y,
|
|
112
|
+
width: width,
|
|
113
|
+
height: height)
|
|
114
|
+
end
|
|
115
|
+
private_class_method :record_position
|
|
116
|
+
|
|
93
117
|
def self.line_text(line)
|
|
94
118
|
line.glyphs.map { |g| [g.codepoint].pack("U") }.join
|
|
95
119
|
end
|
|
@@ -6,13 +6,15 @@ module Idml
|
|
|
6
6
|
class SpreadRenderer
|
|
7
7
|
def initialize(font_metrics: nil, font_ps_name: Render::DEFAULT_FONT,
|
|
8
8
|
package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
|
|
9
|
-
font_ref_resolver: nil, structure: nil
|
|
9
|
+
font_ref_resolver: nil, structure: nil,
|
|
10
|
+
position_tracker: nil)
|
|
10
11
|
@font_metrics = font_metrics
|
|
11
12
|
@font_ps_name = font_ps_name
|
|
12
13
|
@package = package
|
|
13
14
|
@layer_filter = layer_filter
|
|
14
15
|
@font_ref_resolver = font_ref_resolver
|
|
15
16
|
@structure = structure
|
|
17
|
+
@position_tracker = position_tracker
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def render(canvas, spread, page_width:, page_height:, image_refs: [],
|
|
@@ -28,6 +30,7 @@ module Idml
|
|
|
28
30
|
layer_filter: @layer_filter,
|
|
29
31
|
structure: @structure,
|
|
30
32
|
page_index: page_index,
|
|
33
|
+
position_tracker: @position_tracker,
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
canvas.save_graphics_state do
|
data/lib/idml/render.rb
CHANGED
|
@@ -24,6 +24,8 @@ module Idml
|
|
|
24
24
|
autoload :HyperlinkResolver, "#{__dir__}/render/hyperlink_resolver"
|
|
25
25
|
autoload :HyperlinkEmitter, "#{__dir__}/render/hyperlink_emitter"
|
|
26
26
|
autoload :MetadataBuilder, "#{__dir__}/render/metadata_builder"
|
|
27
|
+
autoload :FontSetup, "#{__dir__}/render/font_setup"
|
|
28
|
+
autoload :PositionTracker, "#{__dir__}/render/position_tracker"
|
|
27
29
|
autoload :StoryThreader, "#{__dir__}/render/story_threader"
|
|
28
30
|
autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
|
|
29
31
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -210,6 +210,9 @@ files:
|
|
|
210
210
|
- TODO.pdf/81-pdfa-icc-output-intent.md
|
|
211
211
|
- TODO.pdf/82-table-cell-text.md
|
|
212
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
|
|
213
216
|
- TODO.pdf/README.md
|
|
214
217
|
- exe/idml
|
|
215
218
|
- idml.gemspec
|
|
@@ -385,6 +388,7 @@ files:
|
|
|
385
388
|
- lib/idml/render/color_helper.rb
|
|
386
389
|
- lib/idml/render/color_resolver.rb
|
|
387
390
|
- lib/idml/render/font_reference_resolver.rb
|
|
391
|
+
- lib/idml/render/font_setup.rb
|
|
388
392
|
- lib/idml/render/gradient_resolver.rb
|
|
389
393
|
- lib/idml/render/hyperlink_emitter.rb
|
|
390
394
|
- lib/idml/render/hyperlink_resolver.rb
|
|
@@ -398,6 +402,7 @@ files:
|
|
|
398
402
|
- lib/idml/render/pdfrb_writer.rb
|
|
399
403
|
- lib/idml/render/pipeline.rb
|
|
400
404
|
- lib/idml/render/placement.rb
|
|
405
|
+
- lib/idml/render/position_tracker.rb
|
|
401
406
|
- lib/idml/render/render_context.rb
|
|
402
407
|
- lib/idml/render/renderers.rb
|
|
403
408
|
- lib/idml/render/renderers/graphic_line_renderer.rb
|