idml 0.4.3 → 0.4.4
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 +42 -34
- data/lib/idml/elements/character_style_range.rb +34 -0
- data/lib/idml/elements/hyperlink_text_source.rb +18 -5
- data/lib/idml/render/hyperlink_emitter.rb +46 -4
- data/lib/idml/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e54e07729eb2840c505e903d5aeaf6a69372776cc4b4f1e552d523362b0e861c
|
|
4
|
+
data.tar.gz: d424a2a1092c636c7d757a390350ea5cecdb4d36cc458aaea4ee2497aec9e418
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b9b79e3aebb407be02d20a75b451bcdcb8ed8eb94813470b7f0dd31d3443850e194d3deb813b317ef1536706dbcbca9473d6f8666f12cbefcb83e2247d25c33
|
|
7
|
+
data.tar.gz: 06cbf92ecebe9363a343f146b2903cf50bae58f27943e6ea4c80d87f80eaf9e718b923a31fd193fa54cc28075ff20fe696248a00773aeee6f477117814515a1b
|
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.4)
|
|
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.4)
|
|
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,10 +1,14 @@
|
|
|
1
1
|
# TODO PDF 85: Per-TextRange hyperlink precision
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE (architecture); fixture validation deferred
|
|
4
4
|
|
|
5
5
|
## What was implemented
|
|
6
6
|
|
|
7
|
-
`Render::PositionTracker` is wired through the render path
|
|
7
|
+
`Render::PositionTracker` is wired through the render path AND
|
|
8
|
+
per-source character ranges are computed via CSR's
|
|
9
|
+
`attributed_text` method.
|
|
10
|
+
|
|
11
|
+
### Position tracker (Pipeline-level)
|
|
8
12
|
|
|
9
13
|
1. **`Render::PositionTracker`** — accumulates positioned line
|
|
10
14
|
ranges per frame (`PositionedRange` Struct with start_char,
|
|
@@ -16,31 +20,29 @@
|
|
|
16
20
|
char cursor through the story. After each line is laid out and
|
|
17
21
|
drawn, `record_position` pushes a `PositionedRange` to the
|
|
18
22
|
tracker keyed by `frame.self_attr`.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
can call `tracker.rect_for_range(frame_self, from: source_start,
|
|
43
|
-
to: source_end)` for per-source precision.
|
|
23
|
+
|
|
24
|
+
### Per-source attribution (CSR-level)
|
|
25
|
+
|
|
26
|
+
4. **`Elements::CharacterStyleRange#attributed_text`** returns an
|
|
27
|
+
array of `{char:, source_self:}` tuples. Plain chars have no
|
|
28
|
+
`source_self`; chars inside a `HyperlinkTextSource` carry the
|
|
29
|
+
source's Self attribute. The method walks Content,
|
|
30
|
+
HyperlinkTextSource, and nested CSR children.
|
|
31
|
+
5. **`Elements::HyperlinkTextSource`** now captures inline content
|
|
32
|
+
(`content`, `character_style_range`, `hyperlink_text_source`
|
|
33
|
+
collections) and exposes `text_content` that walks them all.
|
|
34
|
+
6. **`HyperlinkEmitter#source_char_ranges_for_frame`** walks the
|
|
35
|
+
story's CSRs via `attributed_text`, building a
|
|
36
|
+
`source_self → [start_char, end_char]` map.
|
|
37
|
+
7. **`HyperlinkEmitter#emit_precise_rects`** queries
|
|
38
|
+
`PositionTracker#rect_for_range(frame_self, from:, to:)` for
|
|
39
|
+
each source's range, emitting one Link annotation per source.
|
|
40
|
+
|
|
41
|
+
### Fallback
|
|
42
|
+
|
|
43
|
+
When no tracker is supplied (e.g., the renderer took the
|
|
44
|
+
simple_render path), HyperlinkEmitter falls back to the frame's
|
|
45
|
+
bounding box (the original TODO 78 behavior).
|
|
44
46
|
|
|
45
47
|
## Verification
|
|
46
48
|
|
|
@@ -48,15 +50,21 @@ to: source_end)` for per-source precision.
|
|
|
48
50
|
- `lib/idml/render/render_context.rb:12` — `position_tracker` field.
|
|
49
51
|
- `lib/idml/render/renderers/text_frame_renderer.rb:101` —
|
|
50
52
|
`record_position` call.
|
|
51
|
-
- `lib/idml/render/hyperlink_emitter.rb:
|
|
52
|
-
- `
|
|
53
|
-
|
|
53
|
+
- `lib/idml/render/hyperlink_emitter.rb:79` — `source_char_ranges_for_frame`.
|
|
54
|
+
- `lib/idml/elements/character_style_range.rb:669` — `attributed_text`.
|
|
55
|
+
- `lib/idml/elements/hyperlink_text_source.rb` — content collections.
|
|
56
|
+
- `spec/idml/render/position_tracker_spec.rb` — 8 specs.
|
|
57
|
+
- `spec/idml/elements/character_style_range_spec.rb` — 4 specs
|
|
58
|
+
covering text_content (Content join, HyperlinkTextSource wrap)
|
|
59
|
+
and attributed_text (plain vs linked char tagging).
|
|
54
60
|
|
|
55
61
|
## Acceptance criteria
|
|
56
62
|
|
|
57
63
|
- [x] PositionTracker accumulates per-frame positioned ranges.
|
|
58
64
|
- [x] TextFrameRenderer records line positions during layout.
|
|
59
|
-
- [x]
|
|
60
|
-
- [x]
|
|
61
|
-
|
|
62
|
-
- [
|
|
65
|
+
- [x] CSR#attributed_text tags characters per source Self.
|
|
66
|
+
- [x] HyperlinkEmitter computes per-source link rects via
|
|
67
|
+
tracker queries against per-source character ranges.
|
|
68
|
+
- [x] Falls back to frame-level rect when no tracker is supplied.
|
|
69
|
+
- [x] Spec coverage: PositionTracker (8) + CSR attribution (4).
|
|
70
|
+
- [ ] Real-fixture end-to-end test (requires IDML with hyperlinks).
|
|
@@ -657,10 +657,44 @@ module Idml
|
|
|
657
657
|
|
|
658
658
|
def text_content
|
|
659
659
|
parts = content.map(&:text)
|
|
660
|
+
hyperlink_text_source.each { |s| parts << s.text_content }
|
|
660
661
|
character_style_range.each { |c| parts << c.text_content }
|
|
661
662
|
parts.join
|
|
662
663
|
end
|
|
663
664
|
|
|
665
|
+
# Returns an array of {char:, source_self:} tuples. Plain
|
|
666
|
+
# Content chars have source_self == nil; chars inside a
|
|
667
|
+
# HyperlinkTextSource carry the source's Self attribute. Used
|
|
668
|
+
# by HyperlinkEmitter to compute per-source link rects.
|
|
669
|
+
def attributed_text
|
|
670
|
+
result = []
|
|
671
|
+
append_plain_content(result)
|
|
672
|
+
append_hyperlink_content(result)
|
|
673
|
+
append_nested_csr(result)
|
|
674
|
+
result
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def append_plain_content(result)
|
|
678
|
+
content.each do |c|
|
|
679
|
+
c.text.to_s.each_char { |ch| result << { char: ch } }
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
private :append_plain_content
|
|
683
|
+
|
|
684
|
+
def append_hyperlink_content(result)
|
|
685
|
+
hyperlink_text_source.each do |source|
|
|
686
|
+
source.text_content.each_char do |ch|
|
|
687
|
+
result << { char: ch, source_self: source.self_attr }
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
private :append_hyperlink_content
|
|
692
|
+
|
|
693
|
+
def append_nested_csr(result)
|
|
694
|
+
character_style_range.each { |c| result.concat(c.attributed_text) }
|
|
695
|
+
end
|
|
696
|
+
private :append_nested_csr
|
|
697
|
+
|
|
664
698
|
def each_xml_element(&block)
|
|
665
699
|
xml_element.each do |e|
|
|
666
700
|
yield e
|
|
@@ -8,16 +8,19 @@ module Idml
|
|
|
8
8
|
# attribute is referenced by `<Hyperlink Source="...">` in the
|
|
9
9
|
# document designmap.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# the story flow determines the range — there is no explicit
|
|
15
|
-
# `StartIndex`/`EndIndex` on the element itself.
|
|
11
|
+
# The element wraps character content (CSR children + nested
|
|
12
|
+
# HyperlinkTextSource children + Content text). `text_content`
|
|
13
|
+
# walks all those children to produce the wrapped string.
|
|
16
14
|
class HyperlinkTextSource < Lutaml::Model::Serializable
|
|
17
15
|
attribute :self_attr, :string
|
|
18
16
|
attribute :name, :string
|
|
19
17
|
attribute :hidden, :boolean
|
|
20
18
|
attribute :applied_character_style, :string
|
|
19
|
+
attribute :character_style_range, Idml::Elements::CharacterStyleRange,
|
|
20
|
+
collection: true
|
|
21
|
+
attribute :hyperlink_text_source, Idml::Elements::HyperlinkTextSource,
|
|
22
|
+
collection: true
|
|
23
|
+
attribute :content, Idml::Elements::Content, collection: true
|
|
21
24
|
|
|
22
25
|
xml do
|
|
23
26
|
root "HyperlinkTextSource"
|
|
@@ -25,6 +28,16 @@ module Idml
|
|
|
25
28
|
map_attribute "Name", to: :name
|
|
26
29
|
map_attribute "Hidden", to: :hidden
|
|
27
30
|
map_attribute "AppliedCharacterStyle", to: :applied_character_style
|
|
31
|
+
map_element "CharacterStyleRange", to: :character_style_range
|
|
32
|
+
map_element "HyperlinkTextSource", to: :hyperlink_text_source
|
|
33
|
+
map_element "Content", to: :content
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def text_content
|
|
37
|
+
parts = content.map(&:text)
|
|
38
|
+
character_style_range.each { |c| parts << c.text_content }
|
|
39
|
+
hyperlink_text_source.each { |nested| parts << nested.text_content }
|
|
40
|
+
parts.join
|
|
28
41
|
end
|
|
29
42
|
end
|
|
30
43
|
end
|
|
@@ -53,10 +53,19 @@ module Idml
|
|
|
53
53
|
|
|
54
54
|
def emit_precise_rects(frame, sources, page_index)
|
|
55
55
|
frame_self = frame.self_attr
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
source_ranges = source_char_ranges_for_frame(frame)
|
|
57
|
+
|
|
58
|
+
sources.each do |source_self, url|
|
|
59
|
+
range = source_ranges[source_self]
|
|
60
|
+
rect = if range
|
|
61
|
+
@position_tracker.rect_for_range(
|
|
62
|
+
frame_self, from: range[0], to: range[1]
|
|
63
|
+
)
|
|
64
|
+
else
|
|
65
|
+
@position_tracker.rect_for_range(
|
|
66
|
+
frame_self, from: 0, to: 1_000_000
|
|
67
|
+
)
|
|
68
|
+
end || frame_level_rect(frame)
|
|
60
69
|
next unless rect
|
|
61
70
|
|
|
62
71
|
@writer.add_uri_link_annotation(page_index: page_index, rect: rect,
|
|
@@ -64,6 +73,39 @@ module Idml
|
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
76
|
+
# Returns a map of source_self -> [start_char, end_char] across
|
|
77
|
+
# all CSRs in the frame's story. Character positions are
|
|
78
|
+
# absolute within the story text, matching the cursor used by
|
|
79
|
+
# `TextFrameRenderer#record_position`.
|
|
80
|
+
def source_char_ranges_for_frame(frame)
|
|
81
|
+
story = frame.parent_story ? @package.story_by_id(frame.parent_story) : nil
|
|
82
|
+
return {} unless story&.inner
|
|
83
|
+
|
|
84
|
+
cursor = [0]
|
|
85
|
+
ranges = {}
|
|
86
|
+
walk_attribution(story.inner, cursor, ranges)
|
|
87
|
+
ranges
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def walk_attribution(story_inner, cursor, ranges)
|
|
91
|
+
story_inner.paragraph_style_range.each do |psr|
|
|
92
|
+
psr.character_style_range.each do |csr|
|
|
93
|
+
csr.attributed_text.each do |tuple|
|
|
94
|
+
record_tuple(tuple, cursor, ranges)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def record_tuple(tuple, cursor, ranges)
|
|
101
|
+
source = tuple[:source_self]
|
|
102
|
+
if source
|
|
103
|
+
ranges[source] ||= [cursor[0], cursor[0]]
|
|
104
|
+
ranges[source][1] = cursor[0] + 1
|
|
105
|
+
end
|
|
106
|
+
cursor[0] += 1
|
|
107
|
+
end
|
|
108
|
+
|
|
67
109
|
def emit_frame_level_rect(frame, sources, page_index)
|
|
68
110
|
rect = frame_level_rect(frame)
|
|
69
111
|
return unless rect
|
data/lib/idml/version.rb
CHANGED