idml 0.4.2 → 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/85-per-text-range-hyperlinks.md +57 -88
- data/TODO.pdf/86-caching-performance.md +23 -1
- 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/hyperlink_emitter.rb +52 -23
- data/lib/idml/render/pipeline.rb +15 -7
- 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 +1 -0
- data/lib/idml/version.rb +1 -1
- metadata +2 -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
|
|
@@ -1,93 +1,62 @@
|
|
|
1
1
|
# TODO PDF 85: Per-TextRange hyperlink precision
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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.
|
|
87
54
|
|
|
88
55
|
## Acceptance criteria
|
|
89
56
|
|
|
90
|
-
- [
|
|
91
|
-
- [
|
|
92
|
-
- [
|
|
93
|
-
- [
|
|
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).
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# TODO PDF 86: Caching and performance
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
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.
|
|
4
26
|
|
|
5
27
|
## What is cached today
|
|
6
28
|
|
|
@@ -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
|
@@ -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
|
@@ -34,6 +34,7 @@ module Idml
|
|
|
34
34
|
apply_compliance(writer, metadata) if pdfa_requested?
|
|
35
35
|
|
|
36
36
|
structure = StructureTracker.new(enabled: @tagged)
|
|
37
|
+
position_tracker = PositionTracker.new
|
|
37
38
|
layer_filter = LayerFilter.from_designmap(@package.designmap)
|
|
38
39
|
font_ref_resolver = FontReferenceResolver.build(@package)
|
|
39
40
|
font_setup = FontSetup.new(package: @package,
|
|
@@ -45,7 +46,8 @@ module Idml
|
|
|
45
46
|
@package.spreads.each do |spread|
|
|
46
47
|
page_index = render_spread(writer, spread, layer_filter,
|
|
47
48
|
font_ref_resolver, font_resource,
|
|
48
|
-
font_metrics, structure,
|
|
49
|
+
font_metrics, structure,
|
|
50
|
+
position_tracker, page_index)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
structure.flush(writer)
|
|
@@ -84,13 +86,15 @@ module Idml
|
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def render_spread(writer, spread, layer_filter, font_ref_resolver,
|
|
87
|
-
font_resource, font_metrics, structure,
|
|
89
|
+
font_resource, font_metrics, structure,
|
|
90
|
+
position_tracker, page_offset)
|
|
88
91
|
pages = spread.spread.flat_map(&:page)
|
|
89
92
|
image_refs = ImageCollector.new(writer: writer,
|
|
90
93
|
base_dir: File.dirname(@package.path),
|
|
91
94
|
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
92
95
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
93
|
-
font_resource, font_metrics, structure
|
|
96
|
+
font_resource, font_metrics, structure,
|
|
97
|
+
position_tracker)
|
|
94
98
|
current = page_offset
|
|
95
99
|
|
|
96
100
|
pages.each do |page|
|
|
@@ -101,22 +105,25 @@ module Idml
|
|
|
101
105
|
page_height: dims[:height],
|
|
102
106
|
image_refs: image_refs,
|
|
103
107
|
page_index: current)
|
|
104
|
-
emit_hyperlinks(writer, spread, current, layer_filter
|
|
108
|
+
emit_hyperlinks(writer, spread, current, layer_filter,
|
|
109
|
+
position_tracker)
|
|
105
110
|
end
|
|
106
111
|
current
|
|
107
112
|
end
|
|
108
113
|
|
|
109
|
-
def emit_hyperlinks(writer, spread, page_index, layer_filter
|
|
114
|
+
def emit_hyperlinks(writer, spread, page_index, layer_filter,
|
|
115
|
+
position_tracker)
|
|
110
116
|
HyperlinkEmitter.new(writer: writer, package: @package,
|
|
111
117
|
page_height: DEFAULT_HEIGHT,
|
|
112
|
-
layer_filter: layer_filter
|
|
118
|
+
layer_filter: layer_filter,
|
|
119
|
+
position_tracker: position_tracker)
|
|
113
120
|
.emit_for(spread, page_index)
|
|
114
121
|
rescue StandardError
|
|
115
122
|
nil
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
def build_renderer(layer_filter, font_ref_resolver, font_resource,
|
|
119
|
-
font_metrics, structure)
|
|
126
|
+
font_metrics, structure, position_tracker)
|
|
120
127
|
SpreadRenderer.new(
|
|
121
128
|
font_metrics: font_metrics,
|
|
122
129
|
font_ps_name: font_resource,
|
|
@@ -124,6 +131,7 @@ module Idml
|
|
|
124
131
|
layer_filter: layer_filter,
|
|
125
132
|
font_ref_resolver: font_ref_resolver,
|
|
126
133
|
structure: structure,
|
|
134
|
+
position_tracker: position_tracker,
|
|
127
135
|
)
|
|
128
136
|
end
|
|
129
137
|
|
|
@@ -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
|
@@ -25,6 +25,7 @@ module Idml
|
|
|
25
25
|
autoload :HyperlinkEmitter, "#{__dir__}/render/hyperlink_emitter"
|
|
26
26
|
autoload :MetadataBuilder, "#{__dir__}/render/metadata_builder"
|
|
27
27
|
autoload :FontSetup, "#{__dir__}/render/font_setup"
|
|
28
|
+
autoload :PositionTracker, "#{__dir__}/render/position_tracker"
|
|
28
29
|
autoload :StoryThreader, "#{__dir__}/render/story_threader"
|
|
29
30
|
autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
|
|
30
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
|
|
@@ -402,6 +402,7 @@ files:
|
|
|
402
402
|
- lib/idml/render/pdfrb_writer.rb
|
|
403
403
|
- lib/idml/render/pipeline.rb
|
|
404
404
|
- lib/idml/render/placement.rb
|
|
405
|
+
- lib/idml/render/position_tracker.rb
|
|
405
406
|
- lib/idml/render/render_context.rb
|
|
406
407
|
- lib/idml/render/renderers.rb
|
|
407
408
|
- lib/idml/render/renderers/graphic_line_renderer.rb
|