idml 0.4.6 → 0.4.8

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: 64f0fb65e931a0ba911043cdc0a885dab5d79257519b519896c07f8a8de9cc14
4
- data.tar.gz: c25ece7b2f8d61ee13e7e572a3e4089cda665f307e20831a7a46cfa7c7c2ff52
3
+ metadata.gz: bfe99408096a550d69277aae9bf709c0dcb0257f17756fcef498c9ea3982a603
4
+ data.tar.gz: 83fc58f40dccb82324b6098aab6665765cd0e1a08b12ade7e08f6d600c6e69c3
5
5
  SHA512:
6
- metadata.gz: e2113e93725afbf9d5c025b1d0b50d698b4bd0ef47815f7fda87a703e007627b769bbb1118b924ce6d932cf78aff23877d20de303b60f3a9df1cd51f53947849
7
- data.tar.gz: 856b9aa6fad47550fd6da61a1ebbc348be2752cde9eb5a1a165beadfd8b16c5d2ed763d0b5af68af6252b2631561b1c521200cadae00bfdafb0a15a0807d702e
6
+ metadata.gz: 8b9646bd611f3618e6a7c3c689a402b368b231f66f6f06430098f9357179d4b79ae0884058b20c92df03dce6e65aa7cf74c245bb8100a772a9454c37f310b4bd
7
+ data.tar.gz: 7b639edc906f60c9e82128bf599966cd37e2fb0458e06e62158d14767d2293d918bed5fe75163cd2caa2cb20fed7174a0c18a709ac722a673b9099532e8f5b33
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.4.6)
4
+ idml (0.4.8)
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.6)
186
+ idml (0.4.8)
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
@@ -48,15 +48,21 @@ The existing `Elements::Table`/`TableRow`/`TableCell` classes
48
48
  `table_renderer_spec.rb` test. They are not removed because no
49
49
  real IDML uses them and removing them would break the existing test.
50
50
 
51
- ## Future work
52
-
53
- - Migrate `TableRenderer` to use the schema-faithful `cell`/`row`
54
- collections instead of legacy `table_row`. The renderer is the
55
- last consumer of the legacy structure.
56
- - Render Tables discovered in Stories (currently only Spread-level
57
- tables are dispatched to TableRenderer; CSR-embedded Tables are
58
- skipped they need TextFrameRenderer integration to render in
59
- place).
51
+ ## What was completed in this round
52
+
53
+ - `TableRenderer` migrated to auto-detect schema-faithful
54
+ (`cell`/`row` siblings) vs legacy (`table_row`/`table_cell`
55
+ nested) layout. New `SchemaLayout` Struct computes per-cell rects
56
+ from `Row#single_row_height` and `Cell#col_row` max col + 1.
57
+ - `TextFrameRenderer` discovers inline Tables via
58
+ `Story > PSR > CSR > Table` and renders each within the frame's
59
+ bounds via `TableRenderer.render_in_box`. This is the path that
60
+ real IDML fixtures use (Tables live in Stories, not Spreads).
61
+ - New `TableRenderer.render_in_box(canvas, table, box, context)`
62
+ API accepts caller-supplied bounds (real Tables have no own
63
+ geometry).
64
+ - The sample-with-table-more fixture's Table now renders 83 cell
65
+ rectangles end-to-end (verified by spec).
60
66
 
61
67
  ## Current state vs schema
62
68
 
@@ -3,10 +3,22 @@
3
3
  module Idml
4
4
  module Render
5
5
  module Renderers
6
- # Renders an IDML Table. Draws the cell grid via rectangle ops,
7
- # then renders inline `<CharacterStyleRange>` text in each cell
8
- # via `canvas.text_rich`. Cells with no text render as empty
6
+ # Renders an IDML Table. Draws the cell grid via rectangle
7
+ # ops, then renders inline text in each cell via
8
+ # `canvas.text_rich`. Cells with no text render as empty
9
9
  # rectangles.
10
+ #
11
+ # Two table layouts are supported:
12
+ #
13
+ # 1. **Schema-faithful** (real IDML): `Table` has `cell`
14
+ # and `row` sibling collections. Cell `Name` encodes
15
+ # column and row as `"col:row"`. Row order determines
16
+ # vertical position; cell Name determines horizontal.
17
+ # 2. **Legacy** (synthetic test fixture): `Table` has a
18
+ # `table_row` collection, each `TableRow` has a
19
+ # `table_cell` collection. Nested structure.
20
+ #
21
+ # The renderer auto-detects which layout is present.
10
22
  class TableRenderer
11
23
  DEFAULT_SIZE = 10.0
12
24
  INSET = 4.0
@@ -14,37 +26,71 @@ module Idml
14
26
  def self.render(canvas, context)
15
27
  table = context.item
16
28
  return if table.visible == false
17
- return if table.table_row.empty?
18
29
 
19
30
  box = Placement.box(table, context.page_height)
20
31
  return unless box
21
32
 
22
- row_count = table.table_row.length
23
- row_height = box[:height] / row_count
33
+ render_in_box(canvas, table, box, context)
34
+ end
35
+
36
+ # Renders a Table using caller-supplied bounds (no
37
+ # Placement.box lookup). Used by TextFrameRenderer to
38
+ # render Tables inlined in a story — real IDML Tables
39
+ # have no own geometry, so the containing TextFrame's
40
+ # bounds stand in for the Table's bounds.
41
+ def self.render_in_box(canvas, table, box, context)
42
+ return if table.visible == false
24
43
 
25
44
  canvas.save_graphics_state do
26
- table.table_row.each_with_index do |row, row_index|
27
- render_row(canvas, row, row_index, row_count, box, row_height,
28
- context)
45
+ if schema_faithful?(table)
46
+ render_schema_faithful(canvas, table, box, context)
47
+ else
48
+ render_legacy(canvas, table, box, context)
29
49
  end
30
50
  end
31
51
  end
32
52
 
33
- def self.render_row(canvas, row, row_index, row_count, box, row_height,
34
- context)
35
- row_y = box[:y] + ((row_count - 1 - row_index) * row_height)
36
- cell_count = row.table_cell.length
37
- return unless cell_count.positive?
53
+ def self.schema_faithful?(table)
54
+ !table.cell.empty? || !table.row.empty?
55
+ end
56
+ private_class_method :schema_faithful?
38
57
 
39
- cell_width = box[:width] / cell_count
40
- row.table_cell.each_with_index do |cell, cell_index|
41
- cell_x = box[:x] + (cell_index * cell_width)
42
- canvas.rectangle(cell_x, row_y, cell_width, row_height)
58
+ # Real IDML: Table > {Cell, Row} siblings. Cell Name is
59
+ # "col:row". Row count from `row` collection; column count
60
+ # derived from max col + 1 across cells.
61
+ def self.render_schema_faithful(canvas, table, box, context)
62
+ layout = SchemaLayout.new(table: table, box: box)
63
+ layout.each_cell do |cell_x, cell_y, cell_w, cell_h, cell|
64
+ canvas.rectangle(cell_x, cell_y, cell_w, cell_h)
43
65
  canvas.stroke
44
- render_cell_text(canvas, cell, cell_x, row_y, row_height, context)
66
+ render_cell_text(canvas, cell, cell_x, cell_y, cell_h, context)
67
+ end
68
+ end
69
+ private_class_method :render_schema_faithful
70
+
71
+ # Legacy: Table > TableRow > TableCell nested.
72
+ def self.render_legacy(canvas, table, box, context)
73
+ return if table.table_row.empty?
74
+
75
+ row_count = table.table_row.length
76
+ row_height = box[:height] / row_count
77
+
78
+ table.table_row.each_with_index do |row, row_index|
79
+ row_y = box[:y] + ((row_count - 1 - row_index) * row_height)
80
+ cell_count = row.table_cell.length
81
+ next unless cell_count.positive?
82
+
83
+ cell_width = box[:width] / cell_count
84
+ row.table_cell.each_with_index do |cell, cell_index|
85
+ cell_x = box[:x] + (cell_index * cell_width)
86
+ canvas.rectangle(cell_x, row_y, cell_width, row_height)
87
+ canvas.stroke
88
+ render_cell_text(canvas, cell, cell_x, row_y, row_height,
89
+ context)
90
+ end
45
91
  end
46
92
  end
47
- private_class_method :render_row
93
+ private_class_method :render_legacy
48
94
 
49
95
  def self.render_cell_text(canvas, cell, x, y, height, context)
50
96
  text = cell.text_content
@@ -59,6 +105,80 @@ module Idml
59
105
  canvas.text_rich(runs, at: [x + INSET, baseline])
60
106
  end
61
107
  private_class_method :render_cell_text
108
+
109
+ # Computes per-cell rects for the schema-faithful layout.
110
+ # Row heights come from `Row#single_row_height` when present,
111
+ # else fall back to evenly-divided table height. Column
112
+ # widths are evenly divided (precise column widths would
113
+ # require ColumnAttributes lookup, deferred).
114
+ SchemaLayout = Struct.new(:table, :box, keyword_init: true) do
115
+ def each_cell
116
+ row_count.times do |row_idx|
117
+ col_count.times do |col_idx|
118
+ cell = cell_at(col_idx, row_idx)
119
+ next unless cell
120
+
121
+ yield cell_x(col_idx), cell_y(row_idx), cell_w, cell_h(row_idx),
122
+ cell
123
+ end
124
+ end
125
+ end
126
+
127
+ def row_count
128
+ rows.length
129
+ end
130
+
131
+ def col_count
132
+ @col_count ||= begin
133
+ max_col = cells.filter_map(&:col_row).map(&:first).max || 0
134
+ max_col + 1
135
+ end
136
+ end
137
+
138
+ def rows
139
+ table.row
140
+ end
141
+
142
+ def cells
143
+ table.cell
144
+ end
145
+
146
+ def cell_at(col, row)
147
+ cells.find { |c| c.col_row == [col, row] }
148
+ end
149
+
150
+ def cell_x(col)
151
+ box[:x] + (col * cell_w)
152
+ end
153
+
154
+ def cell_y(row)
155
+ box[:y] + total_height - cumulative_height(row) - cell_h(row)
156
+ end
157
+
158
+ def cell_w
159
+ box[:width] / [col_count, 1].max
160
+ end
161
+
162
+ def cell_h(row_idx)
163
+ row_heights[row_idx] || (box[:height] / [row_count, 1].max)
164
+ end
165
+
166
+ def row_heights
167
+ @row_heights ||= rows.map do |row|
168
+ h = row.single_row_height
169
+ h&.positive? ? h : nil
170
+ end
171
+ end
172
+
173
+ def total_height
174
+ @total_height ||= row_count.times.sum { |i| cell_h(i) }
175
+ end
176
+
177
+ def cumulative_height(up_to_row)
178
+ up_to_row.times.sum { |i| cell_h(i) }
179
+ end
180
+ end
181
+ private_constant :SchemaLayout
62
182
  end
63
183
  end
64
184
  end
@@ -20,13 +20,41 @@ module Idml
20
20
  story = context.package&.story_by_id(frame.parent_story)
21
21
  return unless story
22
22
 
23
+ box = frame_box(frame, context.page_height)
24
+ render_inline_tables(canvas, story, box, context)
25
+
23
26
  runs = StyleResolver.extract_runs(story)
24
27
  return if runs.empty?
25
28
 
26
- box = frame_box(frame, context.page_height)
27
29
  render_text(canvas, runs, context, box)
28
30
  end
29
31
 
32
+ # Discovers Tables inlined in the story (Story > PSR > CSR >
33
+ # Table — the real IDML structure) and renders each within
34
+ # the frame's bounds. Real IDML Tables have no own geometry,
35
+ # so the containing TextFrame's box stands in. Synthetic
36
+ # spread-level Tables dispatch separately via
37
+ # PageItemRenderer.
38
+ def self.render_inline_tables(canvas, story, box, context)
39
+ tables = tables_in_story(story)
40
+ return if tables.empty?
41
+
42
+ tables.each do |table|
43
+ TableRenderer.render_in_box(canvas, table, box, context)
44
+ end
45
+ end
46
+ private_class_method :render_inline_tables
47
+
48
+ def self.tables_in_story(story)
49
+ inner = story&.inner
50
+ return [] unless inner
51
+
52
+ inner.paragraph_style_range.flat_map do |psr|
53
+ psr.character_style_range.flat_map(&:table)
54
+ end.compact
55
+ end
56
+ private_class_method :tables_in_story
57
+
30
58
  def self.render_text(canvas, runs, context, box)
31
59
  font = context.font_metrics
32
60
  if font
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.6"
4
+ VERSION = "0.4.8"
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.6
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose