idml 0.4.3 → 0.4.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55a1e03f5e9f1f1e0f4019f499ce674f49caeca3ee18a04f98b01dcffb41b6c8
4
- data.tar.gz: 50674b26f5ee95a95f3275c9ab10d64db7311d8f99b5ee96b38585d24bd7f45c
3
+ metadata.gz: 73b6eace594144002b5d513bb823512ff44a018e90dffb83188167674e839975
4
+ data.tar.gz: bcdebe8d9ba4c525d0b9f0bb0c47e10dcb08edb15d22c145031bb0cdbb796b7d
5
5
  SHA512:
6
- metadata.gz: 7c5d7af6ccd5e60b31fb2ef62c618b336bc9f54517e84377c4cb5566e4da0ae6b0f9a1beee528ac55e5eba6e5f8e59ee3c3d9f9ca91007b7833e6a360dd1f4d0
7
- data.tar.gz: 0a2edf4c947cc93e509434c65fe5520ad3b0a88d10ff4ed317c56c7f000ac0a650e85a7cfdb0fa00d465a569f8a30122fbced8bd1cd10e568536ca9f2d4ddeb8
6
+ metadata.gz: d9637751e91a71db94cc5ab3ede0843eecde0aa35323a128309d21a4e4c3c0c7c88f08e887d7b9d67a081454747e14d85954b43b9e0e3429325f9e39c1bf0b17
7
+ data.tar.gz: d92faf74e08e0f82c9c4265ea0f1532a7d73c25b4195c992a984eedce7625f7cac1d51c367bb35fbcfa7563c77d701e0f7134438aea5bd3d0d2a359a47d1e639
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.4.3)
4
+ idml (0.4.5)
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.3)
186
+ idml (0.4.5)
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,6 +1,35 @@
1
1
  # TODO PDF 84: Schema-faithful Table/Cell/Row structure
2
2
 
3
- ## Status: DEFERRED (architectural correction; no fixtures to validate)
3
+ ## Status: PARTIAL Cell and Row elements added; full restructure deferred
4
+
5
+ ## What was added
6
+
7
+ Schema-faithful element classes alongside the legacy non-standard
8
+ ones, so existing fixtures continue to work:
9
+
10
+ - **`Elements::Cell`** — matches `Cell_Object` RNC. Captures Self,
11
+ Name, RowSpan, ColumnSpan, insets, FillColor, FillTint,
12
+ VerticalJustification, and inline `CharacterStyleRange` children.
13
+ `text_content` walks inline CSRs. `col_row` decodes the
14
+ `"col_row"` Name format.
15
+ - **`Elements::Row`** — matches `Row_Object` RNC. Captures Self,
16
+ Name, insets, FillColor, height constraints.
17
+ - **`Elements::Table`** now declares `cell` and `row` collections
18
+ alongside the legacy `table_row`. Both parse correctly from XML.
19
+
20
+ ## What remains deferred
21
+
22
+ The full schema-faithful refactor — moving Table out of SpreadObject
23
+ (per RNC, Tables live in Stories, not Spreads) and removing the
24
+ legacy `Elements::TableRow`/`TableCell` classes — requires a real
25
+ IDML fixture with tables to validate.
26
+
27
+ Until then:
28
+ - The legacy `Table > TableRow > TableCell` path handles the
29
+ existing synthetic test fixture.
30
+ - The schema-faithful `Table > {Cell, Row}` path is available for
31
+ future fixtures.
32
+ - `TableRenderer` still uses the legacy path.
4
33
 
5
34
  ## Current state vs schema
6
35
 
@@ -1,10 +1,14 @@
1
1
  # TODO PDF 85: Per-TextRange hyperlink precision
2
2
 
3
- ## Status: PARTIAL — PositionTracker wired; per-source ranges need fixture
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
- 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.
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:54` — `emit_precise_rects`.
52
- - `spec/idml/render/position_tracker_spec.rb` — 8 specs covering
53
- add, ranges_for, rect_for_range, clear.
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] 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).
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).
@@ -0,0 +1,97 @@
1
+ # TODO PDF 87: IDML → PDF render — final state summary
2
+
3
+ ## Status: COMPLETE (architecture); fixture-validation work bounded
4
+
5
+ ## What was built
6
+
7
+ A complete IDML → PDF renderer in pure Ruby, using:
8
+ - `lutaml-model` for XML (no Nokogiri/REXML).
9
+ - `pdfrb` for PDF assembly (no hand-rolled PDF operators).
10
+ - `Pdfrb::Font::TrueType` for TTF measurement (no Fontisan).
11
+
12
+ ## Feature coverage
13
+
14
+ | Feature | Status |
15
+ |---------|--------|
16
+ | Text frames with paragraph alignment | DONE (TODO 80) |
17
+ | Word-wrap via Shaper + LineBreaker + Justifier | DONE |
18
+ | Real TTF font measurement via pdfrb | DONE (TODO 63) |
19
+ | Font subsetting | DONE (TODO 52) |
20
+ | Rectangle/Polygon/GraphicLine shapes | DONE |
21
+ | Linear and radial gradient shadings | DONE (TODOs 49, 66, 68) |
22
+ | Transparency and blend modes | DONE (TODO 69) |
23
+ | Stroke styling (cap, join, miter, dash) | DONE (TODO 70/72) |
24
+ | Image embedding with clipping | DONE |
25
+ | XMP metadata extraction | DONE (TODO 75) |
26
+ | Tagged PDF structure tree | DONE (TODO 76) |
27
+ | PDF/A-2a XMP packet | DONE (TODO 77) |
28
+ | sRGB ICC output intent | DONE (TODO 81) |
29
+ | Bookmarks / outline | DONE (TODO 79) |
30
+ | Hyperlinks (per-source rects) | DONE (TODOs 78, 85) |
31
+ | Basic table cell text | DONE (TODO 82) |
32
+ | Layer filtering | DONE |
33
+ | Master spread rendering | DONE |
34
+ | Pipeline decomposition (MetadataBuilder + FontSetup) | DONE (TODO 83) |
35
+ | geometric_bounds memoisation | DONE (TODO 86) |
36
+ | PositionTracker for per-source hyperlink rects | DONE (TODO 85) |
37
+
38
+ ## Architecture
39
+
40
+ Pipeline (orchestrator) → Render helpers (one per concern):
41
+ - `Placement` — item to PDF-rect bridge.
42
+ - `Blending` — transparency/blend mode wrapper.
43
+ - `StrokeStyle` — stroke cap/join/miter/dash.
44
+ - `ColorResolver` / `ColorHelper` — IDML color → PDF.
45
+ - `ImageCollector` — image registration + placement.
46
+ - `MetadataBuilder` — Info dict from XMP.
47
+ - `FontSetup` — pdfrb font registration + metrics adapter.
48
+ - `PdfaPacket` / `IccProfile` — PDF/A compliance.
49
+ - `StructureTracker` / `StructureMapper` — tagged PDF.
50
+ - `PositionTracker` — per-line text positions.
51
+ - `BookmarkResolver` — outline entries.
52
+ - `HyperlinkResolver` / `HyperlinkEmitter` — link annotations.
53
+
54
+ Renderers (OCP registry dispatch via `PageItemRenderer::RENDERER_MAP`):
55
+ - RectangleRenderer, PolygonRenderer, GraphicLineRenderer.
56
+ - TextFrameRenderer, GroupRenderer, TableRenderer.
57
+
58
+ Text engine (`TextEngine` namespace):
59
+ - Shaper, LineBreaker, Justifier, VerticalLayout, CjkLayout.
60
+ - PdfrbFontMetrics (adapter from Fontisan legacy to pdfrb).
61
+
62
+ ## Quality
63
+
64
+ - 2614 examples, 0 failures, 8 pending.
65
+ - 98.12% line coverage.
66
+ - 211 lib files, 52 spec files.
67
+ - 0 anti-pattern violations (`.send`, `instance_variable_*`, `respond_to?`,
68
+ `require_relative`, internal `require`, `double()`, Nokogiri, REXML).
69
+ - 0 rubocop offenses.
70
+
71
+ ## Deferred (TODOs 84 + 85 fixture validation)
72
+
73
+ Two TODOs are documented as architecture-complete but fixture-deferred:
74
+
75
+ - **TODO 84** — Schema-faithful Table/Cell/Row structure. The current
76
+ `Elements::Table`/`TableRow`/`TableCell` classes don't match the RNC
77
+ schema (real IDML has `<Cell>` and `<Row>` as siblings under `<Table>`,
78
+ and Tables live in Stories not Spreads). The architectural correction
79
+ requires a real IDML fixture with tables to validate. The current
80
+ synthetic test continues to pass.
81
+
82
+ - **TODO 85** — Per-source hyperlink rect precision. The architecture
83
+ is in place (PositionTracker + CSR#attributed_text +
84
+ HyperlinkEmitter source-range lookup) but no fixture has hyperlinks
85
+ to validate end-to-end.
86
+
87
+ ## Path forward for the deferred work
88
+
89
+ 1. Acquire a real IDML fixture (from Adobe InDesign or hand-crafted
90
+ per the RNC) with tables and hyperlinks.
91
+ 2. Add to `spec/fixtures/`.
92
+ 3. Implement TODO 84 schema correction; verify with fixture.
93
+ 4. Verify TODO 85 emits precise per-source rects against the fixture.
94
+
95
+ Until then, the gem renders the existing fixtures correctly and
96
+ emits valid PDF/A, tagged PDF, and PDF/UA-adjacent structure for
97
+ the supported feature set.
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Cell>` — a single cell in an IDML Table. Per `Cell_Object`
6
+ # in `reference-docs/schemas/package/Stories/Story.rnc`.
7
+ #
8
+ # Schema note: real IDML has `<Cell>` and `<Row>` as **siblings**
9
+ # inside `<Table>`, not nested `<TableRow><TableCell>`. This
10
+ # class is the schema-faithful model. The legacy
11
+ # `Idml::Elements::TableCell` (which models the non-standard
12
+ # `<TableCell>` element) is preserved for back-compat with the
13
+ # existing synthetic test fixture — see TODO 84.
14
+ #
15
+ # The cell's `Name` attribute encodes its column/row position
16
+ # (e.g., `"0_0"` for column 0, row 0). Carries text content via
17
+ # inline `<CharacterStyleRange>` children.
18
+ class Cell < Lutaml::Model::Serializable
19
+ attribute :self_attr, :string
20
+ attribute :name, :string
21
+ attribute :row_span, :integer
22
+ attribute :column_span, :integer
23
+ attribute :top_inset, :float
24
+ attribute :left_inset, :float
25
+ attribute :bottom_inset, :float
26
+ attribute :right_inset, :float
27
+ attribute :fill_color, :string
28
+ attribute :fill_tint, :float
29
+ attribute :overprint_fill, :boolean
30
+ attribute :clip_content_to_cell, :boolean
31
+ attribute :vertical_justification, :string
32
+ attribute :paragraph_spacing_limit, :float
33
+ attribute :rotation_angle, :float
34
+ attribute :character_style_range, Idml::Elements::CharacterStyleRange,
35
+ collection: true
36
+
37
+ xml do
38
+ root "Cell"
39
+ map_attribute "Self", to: :self_attr
40
+ map_attribute "Name", to: :name
41
+ map_attribute "RowSpan", to: :row_span
42
+ map_attribute "ColumnSpan", to: :column_span
43
+ map_attribute "TopInset", to: :top_inset
44
+ map_attribute "LeftInset", to: :left_inset
45
+ map_attribute "BottomInset", to: :bottom_inset
46
+ map_attribute "RightInset", to: :right_inset
47
+ map_attribute "FillColor", to: :fill_color
48
+ map_attribute "FillTint", to: :fill_tint
49
+ map_attribute "OverprintFill", to: :overprint_fill
50
+ map_attribute "ClipContentToCell", to: :clip_content_to_cell
51
+ map_attribute "VerticalJustification", to: :vertical_justification
52
+ map_attribute "ParagraphSpacingLimit", to: :paragraph_spacing_limit
53
+ map_attribute "RotationAngle", to: :rotation_angle
54
+ map_element "CharacterStyleRange", to: :character_style_range
55
+ end
56
+
57
+ def text_content
58
+ character_style_range.filter_map(&:text_content).join
59
+ end
60
+
61
+ # Cell name format is "column_row" (e.g., "0_0", "1_2").
62
+ # Returns [col, row] integers or nil if Name is malformed.
63
+ def col_row
64
+ return nil unless name
65
+
66
+ parts = name.split("_")
67
+ return nil unless parts.length == 2
68
+
69
+ [parts[0].to_i, parts[1].to_i]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -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
- # 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.
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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Row>` — a row definition in an IDML Table. Per `Row_Object`
6
+ # in `reference-docs/schemas/package/Stories/Story.rnc`.
7
+ #
8
+ # Schema note: real IDML has `<Cell>` and `<Row>` as siblings
9
+ # inside `<Table>`. This class is the schema-faithful model.
10
+ # The legacy `Idml::Elements::TableRow` (which models the
11
+ # non-standard `<TableRow>` element) is preserved for back-compat
12
+ # with the existing synthetic test fixture — see TODO 84.
13
+ #
14
+ # Rows carry visual properties (fill, insets, minimum height)
15
+ # but not cell content — cells live as `<Cell>` siblings.
16
+ class Row < Lutaml::Model::Serializable
17
+ attribute :self_attr, :string
18
+ attribute :name, :string
19
+ attribute :top_inset, :float
20
+ attribute :left_inset, :float
21
+ attribute :bottom_inset, :float
22
+ attribute :right_inset, :float
23
+ attribute :fill_color, :string
24
+ attribute :fill_tint, :float
25
+ attribute :overprint_fill, :boolean
26
+ attribute :single_row_height, :float
27
+ attribute :minimum_height, :float
28
+ attribute :maximum_height, :float
29
+
30
+ xml do
31
+ root "Row"
32
+ map_attribute "Self", to: :self_attr
33
+ map_attribute "Name", to: :name
34
+ map_attribute "TopInset", to: :top_inset
35
+ map_attribute "LeftInset", to: :left_inset
36
+ map_attribute "BottomInset", to: :bottom_inset
37
+ map_attribute "RightInset", to: :right_inset
38
+ map_attribute "FillColor", to: :fill_color
39
+ map_attribute "FillTint", to: :fill_tint
40
+ map_attribute "OverprintFill", to: :overprint_fill
41
+ map_attribute "SingleRowHeight", to: :single_row_height
42
+ map_attribute "MinimumHeight", to: :minimum_height
43
+ map_attribute "MaximumHeight", to: :maximum_height
44
+ end
45
+ end
46
+ end
47
+ end
@@ -16,6 +16,8 @@ module Idml
16
16
  attribute :stroke_color, :string
17
17
  attribute :stroke_weight, :float
18
18
  attribute :table_row, Idml::Elements::TableRow, collection: true
19
+ attribute :cell, Idml::Elements::Cell, collection: true
20
+ attribute :row, Idml::Elements::Row, collection: true
19
21
  attribute :properties, Idml::Elements::Properties, collection: true
20
22
 
21
23
  xml do
@@ -29,6 +31,8 @@ module Idml
29
31
  map_attribute "StrokeColor", to: :stroke_color
30
32
  map_attribute "StrokeWeight", to: :stroke_weight
31
33
  map_element "TableRow", to: :table_row
34
+ map_element "Cell", to: :cell
35
+ map_element "Row", to: :row
32
36
  map_element "Properties", to: :properties
33
37
  end
34
38
 
data/lib/idml/elements.rb CHANGED
@@ -16,6 +16,7 @@ module Idml
16
16
  autoload :BlendingSetting, "idml/elements/blending_setting"
17
17
  autoload :ButtonPreference, "idml/elements/pref_button_preference"
18
18
  autoload :Bookmark, "idml/elements/bookmark"
19
+ autoload :Cell, "idml/elements/cell"
19
20
  autoload :CellStyle, "idml/elements/cell_style"
20
21
  autoload :CellStyleGroup, "idml/elements/cell_style_group"
21
22
  autoload :ChapterNumberPreference,
@@ -118,6 +119,7 @@ module Idml
118
119
  autoload :Properties, "idml/elements/properties"
119
120
  autoload :GeometryPathType, "idml/elements/geometry_path_type"
120
121
  autoload :Rectangle, "idml/elements/rectangle"
122
+ autoload :Row, "idml/elements/row"
121
123
  autoload :RootCellStyleGroup, "idml/elements/root_cell_style_group"
122
124
  autoload :RootCharacterStyleGroup,
123
125
  "idml/elements/root_character_style_group"
@@ -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
- 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)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.4.3"
4
+ VERSION = "0.4.5"
5
5
  end
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.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -213,6 +213,7 @@ files:
213
213
  - TODO.pdf/84-schema-faithful-table.md
214
214
  - TODO.pdf/85-per-text-range-hyperlinks.md
215
215
  - TODO.pdf/86-caching-performance.md
216
+ - TODO.pdf/87-final-state-summary.md
216
217
  - TODO.pdf/README.md
217
218
  - exe/idml
218
219
  - idml.gemspec
@@ -231,6 +232,7 @@ files:
231
232
  - lib/idml/elements/bevel_and_emboss_setting.rb
232
233
  - lib/idml/elements/blending_setting.rb
233
234
  - lib/idml/elements/bookmark.rb
235
+ - lib/idml/elements/cell.rb
234
236
  - lib/idml/elements/cell_style.rb
235
237
  - lib/idml/elements/cell_style_group.rb
236
238
  - lib/idml/elements/character_style.rb
@@ -333,6 +335,7 @@ files:
333
335
  - lib/idml/elements/root_object_style_group.rb
334
336
  - lib/idml/elements/root_paragraph_style_group.rb
335
337
  - lib/idml/elements/root_table_style_group.rb
338
+ - lib/idml/elements/row.rb
336
339
  - lib/idml/elements/satin_setting.rb
337
340
  - lib/idml/elements/spread_object.rb
338
341
  - lib/idml/elements/story_inner.rb