idml 0.6.2 → 0.7.1
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 +1 -1
- data/TODO.pdf/100-character-level-styling.md +11 -1
- data/TODO.pdf/101-paragraph-rules-borders.md +10 -1
- data/TODO.pdf/104-table-schema-completion.md +6 -1
- data/TODO.pdf/105-whole-package-round-trip.md +4 -1
- data/TODO.pdf/108-multi-frame-story-flow.md +50 -0
- data/TODO.pdf/109-dry-color-tint.md +42 -0
- data/TODO.pdf/110-conditional-text.md +37 -0
- data/TODO.pdf/111-footnote-support.md +40 -0
- data/TODO.pdf/94-text-frame-vertical-layout.md +5 -1
- data/TODO.pdf/95-paragraph-spacing-indents.md +2 -1
- data/TODO.pdf/96-text-frame-insets-actual.md +3 -1
- data/TODO.pdf/97-table-cell-rendering-fidelity.md +4 -1
- data/TODO.pdf/98-rowspan-columnspan.md +4 -1
- data/TODO.pdf/99-table-column-widths.md +5 -1
- data/idml.gemspec +1 -1
- data/lib/idml/elements/table.rb +48 -4
- data/lib/idml/render/character_style.rb +148 -0
- data/lib/idml/render/color_helper.rb +27 -3
- data/lib/idml/render/paragraph_rules.rb +125 -0
- data/lib/idml/render/renderers/table_renderer.rb +188 -27
- data/lib/idml/render/renderers/text_frame_renderer.rb +217 -59
- data/lib/idml/render/style_resolver.rb +123 -7
- data/lib/idml/render.rb +2 -0
- data/lib/idml/text_engine/vertical_layout.rb +50 -45
- data/lib/idml/text_engine.rb +3 -2
- data/lib/idml/version.rb +1 -1
- metadata +10 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Emits paragraph-level rules (RuleAbove / RuleBelow) on a pdfrb
|
|
6
|
+
# canvas. Centralizes the IDML → PDF mapping for the paragraph
|
|
7
|
+
# rule attributes carried by `StyleResolver::Paragraph`.
|
|
8
|
+
#
|
|
9
|
+
# Each rule is a horizontal stroke above (RuleAbove) or below
|
|
10
|
+
# (RuleBelow) the paragraph block. The rule's weight, color,
|
|
11
|
+
# tint, offset, and indent come from the PSR's RuleAbove* /
|
|
12
|
+
# RuleBelow* attributes; defaults approximate InDesign behavior
|
|
13
|
+
# when an attribute is missing.
|
|
14
|
+
#
|
|
15
|
+
# Used by `TextFrameRenderer` so paragraph rules live in one
|
|
16
|
+
# place (DRY).
|
|
17
|
+
module ParagraphRules
|
|
18
|
+
DEFAULT_LINE_WEIGHT = 0.5
|
|
19
|
+
DEFAULT_TINT = 1.0
|
|
20
|
+
DEFAULT_OFFSET = 0.0
|
|
21
|
+
|
|
22
|
+
# Emits RuleAbove if `paragraph.rule_above` is true. The rule
|
|
23
|
+
# sits above the paragraph's first line. `top_y` is the y of
|
|
24
|
+
# the paragraph's top edge (before SpaceBefore is applied);
|
|
25
|
+
# `frame_left` / `frame_right` are the column's text bounds.
|
|
26
|
+
def self.emit_rule_above(canvas, paragraph, context, top_y,
|
|
27
|
+
frame_left, frame_right)
|
|
28
|
+
return unless paragraph.rule_above
|
|
29
|
+
|
|
30
|
+
emit_rule(canvas, paragraph, context, top_y, frame_left,
|
|
31
|
+
frame_right,
|
|
32
|
+
weight: paragraph.rule_above_line_weight,
|
|
33
|
+
color: paragraph.rule_above_color,
|
|
34
|
+
tint: paragraph.rule_above_tint,
|
|
35
|
+
offset: paragraph.rule_above_offset,
|
|
36
|
+
left_indent: paragraph.rule_above_left_indent,
|
|
37
|
+
right_indent: paragraph.rule_above_right_indent,
|
|
38
|
+
width_mode: paragraph.rule_above_width,
|
|
39
|
+
position: :above)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Emits RuleBelow if `paragraph.rule_below` is true. The rule
|
|
43
|
+
# sits below the paragraph's last line. `bottom_y` is the y of
|
|
44
|
+
# the paragraph's bottom edge (after SpaceAfter is applied).
|
|
45
|
+
def self.emit_rule_below(canvas, paragraph, context, bottom_y,
|
|
46
|
+
frame_left, frame_right)
|
|
47
|
+
return unless paragraph.rule_below
|
|
48
|
+
|
|
49
|
+
emit_rule(canvas, paragraph, context, bottom_y, frame_left,
|
|
50
|
+
frame_right,
|
|
51
|
+
weight: paragraph.rule_below_line_weight,
|
|
52
|
+
color: paragraph.rule_below_color,
|
|
53
|
+
tint: paragraph.rule_below_tint,
|
|
54
|
+
offset: paragraph.rule_below_offset,
|
|
55
|
+
left_indent: paragraph.rule_below_left_indent,
|
|
56
|
+
right_indent: paragraph.rule_below_right_indent,
|
|
57
|
+
width_mode: paragraph.rule_below_width,
|
|
58
|
+
position: :below)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.emit_rule(canvas, paragraph, context, anchor_y,
|
|
62
|
+
frame_left, frame_right,
|
|
63
|
+
weight:, color:, tint:, offset:,
|
|
64
|
+
left_indent:, right_indent:, width_mode:,
|
|
65
|
+
position:)
|
|
66
|
+
thickness = (weight || DEFAULT_LINE_WEIGHT).to_f
|
|
67
|
+
return unless thickness.positive?
|
|
68
|
+
|
|
69
|
+
tint_value = (tint || DEFAULT_TINT).to_f
|
|
70
|
+
return unless tint_value.positive?
|
|
71
|
+
|
|
72
|
+
offset_value = (offset || DEFAULT_OFFSET).to_f
|
|
73
|
+
x1, x2 = rule_extents(frame_left, frame_right, left_indent,
|
|
74
|
+
right_indent, width_mode, paragraph)
|
|
75
|
+
y = rule_y(anchor_y, offset_value, position)
|
|
76
|
+
|
|
77
|
+
apply_rule_color(canvas, color, tint_value, context)
|
|
78
|
+
canvas.line_width = thickness
|
|
79
|
+
canvas.move_to(x1, y)
|
|
80
|
+
canvas.line_to(x2, y)
|
|
81
|
+
canvas.stroke
|
|
82
|
+
end
|
|
83
|
+
private_class_method :emit_rule
|
|
84
|
+
|
|
85
|
+
# Returns `[x1, x2]` for the rule. Width mode "Text" indents
|
|
86
|
+
# the rule by the paragraph's left/right indent (matches the
|
|
87
|
+
# text bounds); "Column" (default) spans the full column.
|
|
88
|
+
def self.rule_extents(frame_left, frame_right, rule_left_indent,
|
|
89
|
+
rule_right_indent, width_mode, paragraph)
|
|
90
|
+
left = frame_left
|
|
91
|
+
right = frame_right
|
|
92
|
+
if width_mode == "Text"
|
|
93
|
+
left += (paragraph.left_indent || 0).to_f
|
|
94
|
+
right -= (paragraph.right_indent || 0).to_f
|
|
95
|
+
end
|
|
96
|
+
left += (rule_left_indent || 0).to_f
|
|
97
|
+
right -= (rule_right_indent || 0).to_f
|
|
98
|
+
[left, right]
|
|
99
|
+
end
|
|
100
|
+
private_class_method :rule_extents
|
|
101
|
+
|
|
102
|
+
def self.rule_y(anchor_y, offset, position)
|
|
103
|
+
position == :above ? anchor_y + offset : anchor_y - offset
|
|
104
|
+
end
|
|
105
|
+
private_class_method :rule_y
|
|
106
|
+
|
|
107
|
+
def self.apply_rule_color(canvas, color_name, tint_value, context)
|
|
108
|
+
color = resolve_color(color_name, tint_value, context)
|
|
109
|
+
canvas.stroke_color(color)
|
|
110
|
+
end
|
|
111
|
+
private_class_method :apply_rule_color
|
|
112
|
+
|
|
113
|
+
def self.resolve_color(color_name, tint_value, context)
|
|
114
|
+
return [:gray, 1.0 * tint_value] unless color_name
|
|
115
|
+
return [:gray, 1.0 * tint_value] if color_name == "Color/None"
|
|
116
|
+
|
|
117
|
+
color = context&.color_resolver&.resolve(color_name)
|
|
118
|
+
return [:gray, 1.0 * tint_value] unless color
|
|
119
|
+
|
|
120
|
+
ColorHelper.apply_tint(color, tint_value)
|
|
121
|
+
end
|
|
122
|
+
private_class_method :resolve_color
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -4,9 +4,8 @@ module Idml
|
|
|
4
4
|
module Render
|
|
5
5
|
module Renderers
|
|
6
6
|
# Renders an IDML Table. Draws the cell grid via rectangle
|
|
7
|
-
# ops, then renders inline text in each cell
|
|
8
|
-
#
|
|
9
|
-
# rectangles.
|
|
7
|
+
# ops, then renders inline text in each cell. Cells with no
|
|
8
|
+
# text render as empty rectangles.
|
|
10
9
|
#
|
|
11
10
|
# Two table layouts are supported:
|
|
12
11
|
#
|
|
@@ -19,9 +18,20 @@ module Idml
|
|
|
19
18
|
# `table_cell` collection. Nested structure.
|
|
20
19
|
#
|
|
21
20
|
# The renderer auto-detects which layout is present.
|
|
21
|
+
#
|
|
22
|
+
# Honors per-cell:
|
|
23
|
+
# - `fill_color` / `fill_tint` — background fill before stroke.
|
|
24
|
+
# - `top_inset` / `left_inset` / `bottom_inset` / `right_inset`
|
|
25
|
+
# — text insets (replaces fixed `INSET = 4.0`).
|
|
26
|
+
# - `vertical_justification` — Top / Center / Bottom / Justify.
|
|
27
|
+
# - `paragraph_style_range` → typed runs (per-run font + size).
|
|
28
|
+
#
|
|
29
|
+
# Honors per-table:
|
|
30
|
+
# - `single_column_width` — overrides the even-division default
|
|
31
|
+
# for column widths.
|
|
22
32
|
class TableRenderer
|
|
23
33
|
DEFAULT_SIZE = 10.0
|
|
24
|
-
|
|
34
|
+
DEFAULT_INSET = 4.0
|
|
25
35
|
|
|
26
36
|
def self.render(canvas, context)
|
|
27
37
|
table = context.item
|
|
@@ -57,17 +67,42 @@ module Idml
|
|
|
57
67
|
|
|
58
68
|
# Real IDML: Table > {Cell, Row} siblings. Cell Name is
|
|
59
69
|
# "col:row". Row count from `row` collection; column count
|
|
60
|
-
# derived from max col + 1
|
|
70
|
+
# derived from `column_count` attribute or max col + 1.
|
|
61
71
|
def self.render_schema_faithful(canvas, table, box, context)
|
|
62
72
|
layout = SchemaLayout.new(table: table, box: box)
|
|
63
73
|
layout.each_cell do |cell_x, cell_y, cell_w, cell_h, cell|
|
|
64
|
-
canvas
|
|
65
|
-
|
|
74
|
+
render_cell_background(canvas, cell, cell_x, cell_y, cell_w, cell_h,
|
|
75
|
+
context)
|
|
76
|
+
render_cell_border(canvas, cell_x, cell_y, cell_w, cell_h)
|
|
66
77
|
render_cell_text(canvas, cell, cell_x, cell_y, cell_h, context)
|
|
67
78
|
end
|
|
68
79
|
end
|
|
69
80
|
private_class_method :render_schema_faithful
|
|
70
81
|
|
|
82
|
+
def self.render_cell_background(canvas, cell, x, y, w, h, context)
|
|
83
|
+
color = cell_fill_color(cell, context)
|
|
84
|
+
return unless color
|
|
85
|
+
|
|
86
|
+
canvas.fill_color(ColorHelper.to_canvas(color))
|
|
87
|
+
canvas.rectangle(x, y, w, h)
|
|
88
|
+
canvas.fill
|
|
89
|
+
end
|
|
90
|
+
private_class_method :render_cell_background
|
|
91
|
+
|
|
92
|
+
def self.render_cell_border(canvas, x, y, w, h)
|
|
93
|
+
canvas.rectangle(x, y, w, h)
|
|
94
|
+
canvas.stroke
|
|
95
|
+
end
|
|
96
|
+
private_class_method :render_cell_border
|
|
97
|
+
|
|
98
|
+
def self.cell_fill_color(cell, context)
|
|
99
|
+
return nil unless cell.fill_color
|
|
100
|
+
return nil if cell.fill_color == "Color/None"
|
|
101
|
+
|
|
102
|
+
context.color_resolver&.resolve(cell.fill_color)
|
|
103
|
+
end
|
|
104
|
+
private_class_method :cell_fill_color
|
|
105
|
+
|
|
71
106
|
# Legacy: Table > TableRow > TableCell nested.
|
|
72
107
|
def self.render_legacy(canvas, table, box, context)
|
|
73
108
|
return if table.table_row.empty?
|
|
@@ -93,33 +128,122 @@ module Idml
|
|
|
93
128
|
private_class_method :render_legacy
|
|
94
129
|
|
|
95
130
|
def self.render_cell_text(canvas, cell, x, y, height, context)
|
|
131
|
+
runs = cell_text_runs(cell, context)
|
|
132
|
+
return if runs.empty?
|
|
133
|
+
|
|
134
|
+
insets = cell_insets(cell)
|
|
135
|
+
baseline = vertical_baseline(y, height, insets, cell)
|
|
136
|
+
canvas.text_rich(runs, at: [x + insets[:left], baseline])
|
|
137
|
+
end
|
|
138
|
+
private_class_method :render_cell_text
|
|
139
|
+
|
|
140
|
+
# Builds pdfrb runs from the cell's typed PSR/CSR children.
|
|
141
|
+
# Walks ParagraphStyleRange → CharacterStyleRange → Content
|
|
142
|
+
# so each run carries its own font + size (per-cell CSR
|
|
143
|
+
# styling). Falls back to a single DEFAULT_SIZE run when
|
|
144
|
+
# the cell has no typed paragraphs (legacy TableCell).
|
|
145
|
+
def self.cell_text_runs(cell, context)
|
|
146
|
+
psr = typed_paragraphs(cell)
|
|
147
|
+
return default_text_runs(cell, context) if psr.empty?
|
|
148
|
+
|
|
149
|
+
psr.flat_map { |paragraph| paragraph_runs(paragraph, context) }
|
|
150
|
+
end
|
|
151
|
+
private_class_method :cell_text_runs
|
|
152
|
+
|
|
153
|
+
def self.paragraph_runs(paragraph, context)
|
|
154
|
+
paragraph.character_style_range.filter_map do |csr|
|
|
155
|
+
text = csr.text_content
|
|
156
|
+
next if text.nil? || text.empty?
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
text: text,
|
|
160
|
+
font: context.font_ps_name,
|
|
161
|
+
size: csr.point_size || DEFAULT_SIZE,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
private_class_method :paragraph_runs
|
|
166
|
+
|
|
167
|
+
# Real IDML `Cell` exposes paragraph_style_range; the legacy
|
|
168
|
+
# `TableCell` (synthetic fixture only) doesn't, so we treat
|
|
169
|
+
# it as having no typed paragraphs and fall through to the
|
|
170
|
+
# default_text_runs path.
|
|
171
|
+
def self.typed_paragraphs(cell)
|
|
172
|
+
return [] unless cell.is_a?(Idml::Elements::Cell)
|
|
173
|
+
|
|
174
|
+
cell.paragraph_style_range
|
|
175
|
+
end
|
|
176
|
+
private_class_method :typed_paragraphs
|
|
177
|
+
|
|
178
|
+
def self.default_text_runs(cell, context)
|
|
96
179
|
text = cell.text_content
|
|
97
|
-
return if text.nil? || text.empty?
|
|
180
|
+
return [] if text.nil? || text.empty?
|
|
98
181
|
|
|
99
|
-
|
|
182
|
+
[{
|
|
100
183
|
text: text,
|
|
101
184
|
font: context.font_ps_name,
|
|
102
185
|
size: DEFAULT_SIZE,
|
|
103
186
|
}]
|
|
104
|
-
baseline = y + (height / 2)
|
|
105
|
-
canvas.text_rich(runs, at: [x + INSET, baseline])
|
|
106
187
|
end
|
|
107
|
-
private_class_method :
|
|
188
|
+
private_class_method :default_text_runs
|
|
189
|
+
|
|
190
|
+
# Cell insets: prefer typed TextTopInset/Left/Bottom/Right,
|
|
191
|
+
# fall back to DEFAULT_INSET for any that are missing.
|
|
192
|
+
def self.cell_insets(cell)
|
|
193
|
+
{
|
|
194
|
+
top: cell.top_inset || DEFAULT_INSET,
|
|
195
|
+
bottom: cell.bottom_inset || DEFAULT_INSET,
|
|
196
|
+
left: cell.left_inset || DEFAULT_INSET,
|
|
197
|
+
right: cell.right_inset || DEFAULT_INSET,
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
private_class_method :cell_insets
|
|
201
|
+
|
|
202
|
+
# Vertical baseline for the cell's first text line, per the
|
|
203
|
+
# cell's VerticalJustification. IDML values: TopAlign,
|
|
204
|
+
# CenterAlign, BottomAlign, JustifyAlign. Default: CenterAlign
|
|
205
|
+
# (legacy behavior).
|
|
206
|
+
def self.vertical_baseline(y, height, insets, cell)
|
|
207
|
+
case cell.vertical_justification
|
|
208
|
+
when "TopAlign"
|
|
209
|
+
y + height - insets[:top]
|
|
210
|
+
when "BottomAlign"
|
|
211
|
+
y + insets[:bottom]
|
|
212
|
+
else # CenterAlign, JustifyAlign, nil
|
|
213
|
+
y + (height / 2)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
private_class_method :vertical_baseline
|
|
108
217
|
|
|
109
218
|
# Computes per-cell rects for the schema-faithful layout.
|
|
110
219
|
# Row heights come from `Row#single_row_height` when present,
|
|
111
220
|
# else fall back to evenly-divided table height. Column
|
|
112
|
-
# widths
|
|
113
|
-
#
|
|
221
|
+
# widths come from `Table#single_column_width` when present
|
|
222
|
+
# (uniform columns only — per-column widths would require
|
|
223
|
+
# ColumnAttributes, which IDML doesn't model separately),
|
|
224
|
+
# else divide evenly.
|
|
225
|
+
#
|
|
226
|
+
# Honors `Cell#column_span` and `Cell#row_span`: a spanning
|
|
227
|
+
# cell covers multiple grid positions and is rendered once
|
|
228
|
+
# at its top-left corner with the merged rect. Cells covered
|
|
229
|
+
# by another cell's span are skipped.
|
|
114
230
|
SchemaLayout = Struct.new(:table, :box, keyword_init: true) do
|
|
115
231
|
def each_cell
|
|
232
|
+
covered = Set.new
|
|
233
|
+
|
|
116
234
|
row_count.times do |row_idx|
|
|
117
235
|
col_count.times do |col_idx|
|
|
236
|
+
next if covered.include?([col_idx, row_idx])
|
|
237
|
+
|
|
118
238
|
cell = cell_at(col_idx, row_idx)
|
|
119
239
|
next unless cell
|
|
120
240
|
|
|
121
|
-
|
|
122
|
-
|
|
241
|
+
col_span = cell.column_span || 1
|
|
242
|
+
row_span = cell.row_span || 1
|
|
243
|
+
mark_covered(covered, col_idx, row_idx, col_span, row_span)
|
|
244
|
+
|
|
245
|
+
yield cell_x(col_idx), cell_y(row_idx, col_span, row_span),
|
|
246
|
+
cell_w(col_span), cell_h(row_idx, row_span), cell
|
|
123
247
|
end
|
|
124
248
|
end
|
|
125
249
|
end
|
|
@@ -130,11 +254,16 @@ module Idml
|
|
|
130
254
|
|
|
131
255
|
def col_count
|
|
132
256
|
@col_count ||= begin
|
|
133
|
-
|
|
134
|
-
|
|
257
|
+
declared = table.column_count
|
|
258
|
+
declared&.positive? ? declared : derived_col_count
|
|
135
259
|
end
|
|
136
260
|
end
|
|
137
261
|
|
|
262
|
+
def derived_col_count
|
|
263
|
+
max_col = cells.filter_map(&:col_row).map(&:first).max || 0
|
|
264
|
+
max_col + 1
|
|
265
|
+
end
|
|
266
|
+
|
|
138
267
|
def rows
|
|
139
268
|
table.row
|
|
140
269
|
end
|
|
@@ -147,20 +276,52 @@ module Idml
|
|
|
147
276
|
cells.find { |c| c.col_row == [col, row] }
|
|
148
277
|
end
|
|
149
278
|
|
|
279
|
+
def mark_covered(covered, col, row, col_span, row_span)
|
|
280
|
+
col_span.times do |dc|
|
|
281
|
+
row_span.times do |dr|
|
|
282
|
+
covered << [col + dc, row + dr]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
private :mark_covered
|
|
287
|
+
|
|
150
288
|
def cell_x(col)
|
|
151
|
-
box[:x] + (col *
|
|
289
|
+
box[:x] + (col * per_col_width)
|
|
152
290
|
end
|
|
153
291
|
|
|
154
|
-
|
|
155
|
-
|
|
292
|
+
# PDF-coordinate bottom y of the cell's rectangle. For a
|
|
293
|
+
# row_spanning cell, this is the bottom edge of the last
|
|
294
|
+
# spanned row. PDF rectangle takes (x, y_bottom, w, h).
|
|
295
|
+
def cell_y(row, _col_span, row_span)
|
|
296
|
+
last_row = row + row_span - 1
|
|
297
|
+
box[:y] + total_height - cumulative_height(last_row + 1)
|
|
156
298
|
end
|
|
157
299
|
|
|
158
|
-
|
|
159
|
-
|
|
300
|
+
# Width covered by `span` columns starting at the cell's col.
|
|
301
|
+
def cell_w(span)
|
|
302
|
+
per_col_width * (span || 1)
|
|
160
303
|
end
|
|
161
304
|
|
|
162
|
-
|
|
163
|
-
|
|
305
|
+
# Total height covered by `span` rows starting at `row_idx`.
|
|
306
|
+
def cell_h(row_idx, span)
|
|
307
|
+
(span || 1).times.sum { |i| row_height(row_idx + i) }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def per_col_width
|
|
311
|
+
return @per_col_width if @per_col_width
|
|
312
|
+
|
|
313
|
+
declared = table.single_column_width
|
|
314
|
+
@per_col_width = if declared&.positive?
|
|
315
|
+
declared
|
|
316
|
+
else
|
|
317
|
+
box[:width] / [col_count, 1].max
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def row_height(idx)
|
|
322
|
+
return 0.0 unless idx.between?(0, row_count - 1)
|
|
323
|
+
|
|
324
|
+
row_heights[idx] || (box[:height] / [row_count, 1].max)
|
|
164
325
|
end
|
|
165
326
|
|
|
166
327
|
def row_heights
|
|
@@ -171,11 +332,11 @@ module Idml
|
|
|
171
332
|
end
|
|
172
333
|
|
|
173
334
|
def total_height
|
|
174
|
-
@total_height ||= row_count.times.sum { |i|
|
|
335
|
+
@total_height ||= row_count.times.sum { |i| row_height(i) }
|
|
175
336
|
end
|
|
176
337
|
|
|
177
338
|
def cumulative_height(up_to_row)
|
|
178
|
-
up_to_row.times.sum { |i|
|
|
339
|
+
up_to_row.times.sum { |i| row_height(i) }
|
|
179
340
|
end
|
|
180
341
|
end
|
|
181
342
|
private_constant :SchemaLayout
|