prawnx 0.1.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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module PrawnX
6
+ class Document
7
+ def cell(options = {})
8
+ cell = Table::Cell.make(self, options.delete(:content), options)
9
+ cell.draw
10
+ cell
11
+ end
12
+
13
+ def make_cell(content, options = {})
14
+ Table::Cell.make(self, content, options)
15
+ end
16
+
17
+ # Runs the block, then puts back the listed attributes as they were.
18
+ def mask(*fields)
19
+ stored = fields.to_h { [it, public_send(it)] }
20
+ yield
21
+ stored.each { |field, value| public_send(:"#{field}=", value) }
22
+ end
23
+ end
24
+
25
+ class Table
26
+ class Cell
27
+ # Extra width given to a cell's content box, so text measured to fit exactly is not wrapped.
28
+ FPTolerance = 1
29
+ SIDE_INDEX = {top: 0, right: 1, bottom: 2, left: 3}.freeze
30
+
31
+ attr_reader :padding, :colspan, :rowspan, :dummy_cells, :border_widths, :border_colors, :border_lines
32
+ attr_writer :height
33
+ attr_accessor :borders, :content, :background_color
34
+
35
+ def self.make(pdf, content, options = {})
36
+ at = options.delete(:at) || [0, pdf.cursor]
37
+ content = content.to_s if content.nil? || content.is_a?(Numeric) || content.is_a?(Date)
38
+ if content.is_a?(Hash)
39
+ return Cell::Image.new(pdf, at, content) if content[:image]
40
+
41
+ options.update(content)
42
+ content = options[:content]
43
+ else
44
+ options[:content] = content
45
+ end
46
+ options[:content] = content = "" if content.nil?
47
+
48
+ case content
49
+ when Cell then content
50
+ when String then Cell::Text.new(pdf, at, options)
51
+ when Table then Cell::Subtable.new(pdf, at, options)
52
+ when Array then Cell::Subtable.new(pdf, at, options.merge(content: Table.new(options[:content], pdf, {})))
53
+ else raise Errors::UnrecognizedTableContent
54
+ end
55
+ end
56
+
57
+ def self.draw_cells(cells)
58
+ cells.each do |cell, point|
59
+ cell.set_width_constraints
60
+ cell.draw_background(point)
61
+ end
62
+ cells.each do |cell, point|
63
+ cell.draw_borders(point)
64
+ cell.draw_bounded_content(point)
65
+ end
66
+ end
67
+
68
+ def initialize(pdf, point, options = {})
69
+ @pdf = pdf
70
+ @point = point
71
+ @padding = [5, 5, 5, 5]
72
+ @borders = %i[top bottom left right]
73
+ @border_widths = [1] * 4
74
+ @border_colors = ["000000"] * 4
75
+ @border_lines = [:solid] * 4
76
+ @colspan = 1
77
+ @rowspan = 1
78
+ @dummy_cells = []
79
+ options.each { |key, value| send(:"#{key}=", value) }
80
+ @initializer_run = true
81
+ end
82
+
83
+ def min_width_ignoring_span
84
+ set_width_constraints
85
+ @min_width
86
+ end
87
+
88
+ def min_width
89
+ return min_width_ignoring_span if @colspan == 1
90
+
91
+ min_widths = Hash.new(0)
92
+ dummy_cells.each { min_widths[it.column] = [min_widths[it.column], it.min_width].max }
93
+ min_widths[column] = [min_widths[column], min_width_ignoring_span].max
94
+ min_widths.values.inject(0, :+)
95
+ end
96
+
97
+ def avg_spanned_min_width
98
+ min_width.to_f / colspan
99
+ end
100
+
101
+ def max_width_ignoring_span
102
+ set_width_constraints
103
+ @max_width
104
+ end
105
+
106
+ def max_width
107
+ return max_width_ignoring_span if @colspan == 1
108
+
109
+ max_widths = Hash.new(0)
110
+ dummy_cells.each { max_widths[it.column] = [max_widths[it.column], it.max_width].min }
111
+ max_widths[column] = [max_widths[column], max_width_ignoring_span].min
112
+ max_widths.values.inject(0, :+)
113
+ end
114
+
115
+ def style(options = {}, &block)
116
+ options.each { |key, value| send(:"#{key}=", value) if respond_to?(:"#{key}=") }
117
+ block&.call(self)
118
+ end
119
+
120
+ def width_ignoring_span
121
+ @width || (content_width + padding_left + padding_right)
122
+ end
123
+
124
+ def width
125
+ return width_ignoring_span if @colspan == 1 && @rowspan == 1
126
+
127
+ column_widths = Hash.new(0)
128
+ dummy_cells.each { column_widths[it.column] = [column_widths[it.column], it.width].max }
129
+ column_widths[column] = [column_widths[column], width_ignoring_span].max
130
+ column_widths.values.inject(0, :+)
131
+ end
132
+
133
+ def width=(width)
134
+ @width = @min_width = @max_width = width
135
+ end
136
+
137
+ def content_width
138
+ return @width - padding_left - padding_right if @width
139
+
140
+ natural_content_width
141
+ end
142
+
143
+ def spanned_content_width
144
+ width - padding_left - padding_right
145
+ end
146
+
147
+ def natural_content_width
148
+ raise NotImplementedError, "subclasses must implement natural_content_width"
149
+ end
150
+
151
+ def height_ignoring_span
152
+ @height || (content_height + padding_top + padding_bottom)
153
+ end
154
+
155
+ def height
156
+ return height_ignoring_span if @colspan == 1 && @rowspan == 1
157
+
158
+ row_heights = Hash.new(0)
159
+ dummy_cells.each { row_heights[it.row] = [row_heights[it.row], it.height].max }
160
+ row_heights[row] = [row_heights[row], height_ignoring_span].max
161
+ row_heights.values.inject(0, :+)
162
+ end
163
+
164
+ def content_height
165
+ return @height - padding_top - padding_bottom if @height
166
+
167
+ natural_content_height
168
+ end
169
+
170
+ def spanned_content_height
171
+ height - padding_top - padding_bottom
172
+ end
173
+
174
+ def natural_content_height
175
+ raise NotImplementedError, "subclasses must implement natural_content_height"
176
+ end
177
+
178
+ def colspan=(span)
179
+ raise Errors::InvalidTableSpan, "colspan must be provided in the table's structure, never in the initialization block. See Prawn's documentation for details." if @initializer_run
180
+
181
+ @colspan = span
182
+ end
183
+
184
+ def rowspan=(span)
185
+ raise Errors::InvalidTableSpan, "rowspan must be provided in the table's structure, never in the initialization block. See Prawn's documentation for details." if @initializer_run
186
+
187
+ @rowspan = span
188
+ end
189
+
190
+ def draw(point = [x, y])
191
+ Cell.draw_cells([[self, point]])
192
+ end
193
+
194
+ def draw_bounded_content(point)
195
+ @pdf.float do
196
+ @pdf.bounding_box([point[0] + padding_left, point[1] - padding_top],
197
+ width: spanned_content_width + FPTolerance, height: spanned_content_height + FPTolerance) { draw_content }
198
+ end
199
+ end
200
+
201
+ def x
202
+ @point[0]
203
+ end
204
+
205
+ def x=(value)
206
+ @point[0] = value
207
+ end
208
+
209
+ def relative_x
210
+ x + @pdf.bounds.left_side - @pdf.bounds.absolute_left
211
+ end
212
+
213
+ def y
214
+ @point[1]
215
+ end
216
+
217
+ def y=(value)
218
+ @point[1] = value
219
+ end
220
+
221
+ def relative_y(offset = 0)
222
+ y + offset - @pdf.bounds.absolute_bottom
223
+ end
224
+
225
+ def padding=(padding)
226
+ @padding = four_sides(padding, nil => [0, 0, 0, 0], Numeric => [padding] * 4) do
227
+ raise ArgumentError, ":padding must be a number or an array [v,h] or [t,r,b,l]"
228
+ end
229
+ end
230
+
231
+ %i[top right bottom left].each_with_index do |side, index|
232
+ define_method(:"padding_#{side}") { @padding[index] }
233
+ define_method(:"padding_#{side}=") { |value| @padding[index] = value }
234
+ define_method(:"border_#{side}_color") { @border_colors[index] }
235
+ define_method(:"border_#{side}_color=") { |value| @border_colors[index] = value }
236
+ define_method(:"border_#{side}_width") { @borders.include?(side) ? @border_widths[index] : 0 }
237
+ define_method(:"border_#{side}_width=") { |value| @border_widths[index] = value }
238
+ define_method(:"border_#{side}_line") { @borders.include?(side) ? @border_lines[index] : 0 }
239
+ define_method(:"border_#{side}_line=") { |value| @border_lines[index] = value }
240
+ end
241
+
242
+ def border_color=(color)
243
+ @border_colors = four_sides(color, nil => ["000000"] * 4, String => [color] * 4) do
244
+ raise ArgumentError, ":border_color must be a string or an array [v,h] or [t,r,b,l]"
245
+ end
246
+ end
247
+ alias border_colors= border_color=
248
+
249
+ # Prawn-table resets a nil border width to four "000000": kept for compatibility.
250
+ def border_width=(width)
251
+ @border_widths = four_sides(width, nil => ["000000"] * 4, Numeric => [width] * 4) do
252
+ raise ArgumentError, ":border_width must be a string or an array [v,h] or [t,r,b,l]"
253
+ end
254
+ end
255
+ alias border_widths= border_width=
256
+
257
+ def border_line=(line)
258
+ @border_lines =
259
+ if line.nil? then [:solid] * 4
260
+ elsif line.length == 1 then [line[0]] * 4
261
+ else four_sides(line) { raise ArgumentError, "border_line must be one of :solid, :dashed, :dotted or an array [v,h] or [t,r,b,l]" }
262
+ end
263
+ end
264
+ alias border_lines= border_line=
265
+
266
+ def set_width_constraints
267
+ @min_width ||= padding_left + padding_right
268
+ @max_width ||= @pdf.bounds.width
269
+ end
270
+
271
+ def draw_background(point)
272
+ return unless background_color
273
+
274
+ @pdf.mask(:fill_color) do
275
+ @pdf.fill_color background_color
276
+ @pdf.fill_rectangle point, width, height
277
+ end
278
+ end
279
+
280
+ def draw_borders(point)
281
+ x, y = point
282
+ @pdf.mask(:line_width, :stroke_color) do
283
+ @borders.each do |border|
284
+ index = SIDE_INDEX[border]
285
+ border_width = @border_widths[index]
286
+ next if border_width <= 0
287
+
288
+ from, to =
289
+ case border
290
+ when :top then [[x, y], [x + width, y]]
291
+ when :bottom then [[x, y - height], [x + width, y - height]]
292
+ when :left then [[x, y + (border_top_width / 2.0)], [x, y - height - (border_bottom_width / 2.0)]]
293
+ when :right then [[x + width, y + (border_top_width / 2.0)], [x + width, y - height - (border_bottom_width / 2.0)]]
294
+ end
295
+ case @border_lines[index]
296
+ when :dashed then @pdf.dash border_width * 4
297
+ when :dotted then @pdf.dash border_width, space: border_width * 2
298
+ when :solid then nil
299
+ else raise ArgumentError, "border_line must be :solid, :dotted or :dashed"
300
+ end
301
+ @pdf.line_width = border_width
302
+ @pdf.stroke_color = @border_colors[index]
303
+ @pdf.stroke_line(from, to)
304
+ @pdf.undash if @pdf.dashed?
305
+ end
306
+ end
307
+ end
308
+
309
+ def draw_content
310
+ raise NotImplementedError, "subclasses must implement draw_content"
311
+ end
312
+
313
+ private
314
+
315
+ # A value for each side, CSS style: one for all, [vertical, horizontal], [top, horizontal, bottom] or four.
316
+ def four_sides(value, special_cases = {})
317
+ special_cases.each { |kind, sides| return sides if kind.nil? ? value.nil? : kind === value }
318
+ case value.length
319
+ when 2 then [value[0], value[1], value[0], value[1]]
320
+ when 3 then [value[0], value[1], value[2], value[1]]
321
+ when 4 then [value[0], value[1], value[2], value[3]]
322
+ else yield
323
+ end
324
+ end
325
+
326
+ module InTable
327
+ attr_accessor :row, :column
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnX
4
+ class Table
5
+ # A selection of a table's cells, addressable by row and column, styled all at once.
6
+ class Cells < Array
7
+ def fits_on_current_page?(offset, reference_bounds)
8
+ return true if empty?
9
+
10
+ height_with_span < (self[0, 0].y + offset) - reference_bounds.absolute_bottom
11
+ end
12
+
13
+ def rows(row_spec)
14
+ index_cells unless @indexed
15
+ row_spec = transform_spec(row_spec, @first_row, @row_count)
16
+ Cells.new(@rows[row_spec] ||= select { row_spec.respond_to?(:include?) ? row_spec.include?(it.row) : row_spec === it.row })
17
+ end
18
+ alias row rows
19
+
20
+ def row_count
21
+ index_cells unless @indexed
22
+ @row_count
23
+ end
24
+
25
+ def columns(column_spec)
26
+ index_cells unless @indexed
27
+ column_spec = transform_spec(column_spec, @first_column, @column_count)
28
+ Cells.new(@columns[column_spec] ||= select { column_spec.respond_to?(:include?) ? column_spec.include?(it.column) : column_spec === it.column })
29
+ end
30
+ alias column columns
31
+
32
+ def column_count
33
+ index_cells unless @indexed
34
+ @column_count
35
+ end
36
+
37
+ def filter(&)
38
+ Cells.new(select(&))
39
+ end
40
+
41
+ def [](row, column)
42
+ return nil if empty?
43
+
44
+ index_cells unless @indexed
45
+ row_cells = @rows[@first_row + row] || []
46
+ column_cells = @columns[@first_column + column] || []
47
+ if row_cells.length < column_cells.length
48
+ row_cells.find { it.column == @first_column + column }
49
+ else
50
+ column_cells.find { it.row == @first_row + row }
51
+ end
52
+ end
53
+
54
+ def []=(row, column, cell)
55
+ cell.extend(Cell::InTable)
56
+ cell.row = row
57
+ cell.column = column
58
+ if @indexed
59
+ (@rows[row] ||= []) << cell
60
+ (@columns[column] ||= []) << cell
61
+ @first_row = row if !@first_row || row < @first_row
62
+ @first_column = column if !@first_column || column < @first_column
63
+ @row_count = @rows.size
64
+ @column_count = @columns.size
65
+ end
66
+ self << cell
67
+ end
68
+
69
+ def style(options = {}, &)
70
+ each { it.style(options, &) unless it.is_a?(Cell::SpanDummy) }
71
+ end
72
+
73
+ def width
74
+ ColumnWidthCalculator.new(self).natural_widths.inject(0, :+)
75
+ end
76
+
77
+ def min_width
78
+ aggregate_cell_values(:column, :avg_spanned_min_width, :max)
79
+ end
80
+
81
+ def max_width
82
+ aggregate_cell_values(:column, :max_width_ignoring_span, :max)
83
+ end
84
+
85
+ def height
86
+ aggregate_cell_values(:row, :height_ignoring_span, :max)
87
+ end
88
+
89
+ def height_with_span
90
+ aggregate_cell_values(:row, :height, :max)
91
+ end
92
+
93
+ # Setters reach every cell that has them: cells.background_color = "eeeeee".
94
+ def method_missing(name, *arguments, &)
95
+ return super unless name.end_with?("=")
96
+
97
+ each { it.public_send(name, *arguments, &) if it.respond_to?(name) }
98
+ end
99
+
100
+ def respond_to_missing?(name, include_private = false)
101
+ name.end_with?("=") || super
102
+ end
103
+
104
+ protected
105
+
106
+ def index_cells
107
+ @rows = {}
108
+ @columns = {}
109
+ each do |cell|
110
+ (@rows[cell.row] ||= []) << cell
111
+ (@columns[cell.column] ||= []) << cell
112
+ end
113
+ @first_row = @rows.keys.min
114
+ @first_column = @columns.keys.min
115
+ @row_count = @rows.size
116
+ @column_count = @columns.size
117
+ @indexed = true
118
+ end
119
+
120
+ def aggregate_cell_values(row_or_column, method, aggregate)
121
+ ColumnWidthCalculator.new(self).aggregate_cell_values(row_or_column, method, aggregate)
122
+ end
123
+
124
+ def transform_spec(spec, first, total)
125
+ case spec
126
+ when Range then transform_spec(spec.begin, first, total)..transform_spec(spec.end, first, total)
127
+ when Integer then spec.negative? ? first + total + spec : first + spec
128
+ when Enumerable then spec.map { first + it }
129
+ else raise "Don't understand spec #{spec.inspect}"
130
+ end
131
+ end
132
+ end
133
+
134
+ # Widths of columns, and sums of per-row or per-column cell values, accounting for cells spanning several.
135
+ class ColumnWidthCalculator
136
+ def initialize(cells)
137
+ @cells = cells
138
+ @widths_by_column = Hash.new(0)
139
+ @rows_with_a_span_dummy = Hash.new(false)
140
+ @cells.each { @rows_with_a_span_dummy[it.row] = true if it.is_a?(Cell::SpanDummy) }
141
+ end
142
+
143
+ def has_a_span_dummy?(row)
144
+ @rows_with_a_span_dummy[row]
145
+ end
146
+
147
+ def fill_values_if_needed(values, cell, index, method)
148
+ missing_count = cell.colspan.times.count { values[index + it].nil? }
149
+ return values if missing_count.zero? || missing_count == cell.colspan || !has_a_span_dummy?(cell.row)
150
+
151
+ new_sum = cell.send(method) * cell.colspan
152
+ cell.colspan.times { new_sum -= values[index + it] unless values[index + it].nil? }
153
+ new_value = new_sum.to_f / missing_count
154
+ cell.colspan.times { values[index + it] = new_value if values[index + it].nil? }
155
+ values
156
+ end
157
+
158
+ def natural_widths
159
+ @cells.each do |cell|
160
+ @widths_by_column[cell.column] = [@widths_by_column[cell.column], cell.width.to_f].max unless has_a_span_dummy?(cell.row)
161
+ end
162
+ @cells.each do |cell|
163
+ next unless has_a_span_dummy?(cell.row)
164
+ next if cell.is_a?(Cell::SpanDummy)
165
+
166
+ if cell.colspan == 1
167
+ @widths_by_column[cell.column] = [@widths_by_column[cell.column], cell.width.to_f].max
168
+ else
169
+ current_width_of_spanned_cells = @widths_by_column.to_a[cell.column..(cell.column + cell.colspan - 1)].map(&:last).inject(0, :+)
170
+ if (cell.width.to_f - current_width_of_spanned_cells) > FLOAT_PRECISION
171
+ cell.colspan.times { @widths_by_column[cell.column + it] = cell.width.to_f / cell.colspan }
172
+ end
173
+ end
174
+ end
175
+ @widths_by_column.sort_by { |column, _| column }.map { |_, width| width }
176
+ end
177
+
178
+ def aggregate_cell_values(row_or_column, method, aggregate)
179
+ values = {}
180
+ @cells.each do |cell|
181
+ next unless cell.colspan == 1 && cell.class != Cell::SpanDummy
182
+
183
+ index = cell.send(row_or_column)
184
+ values[index] = [values[index], cell.send(method)].compact.send(aggregate)
185
+ end
186
+
187
+ spanned_width_needs_fixing = true
188
+ @cells.each do |cell|
189
+ index = cell.send(row_or_column)
190
+ if cell.colspan > 1
191
+ values = fill_values_if_needed(values, cell, index, method) if row_or_column == :column
192
+ old_sum = cell.colspan.times.inject(0) { |sum, offset| values[index + offset] ? sum + values[index + offset] : sum }
193
+ new_sum = cell.send(method) * cell.colspan
194
+ spanned_width_needs_fixing = (new_sum - old_sum) > FLOAT_PRECISION
195
+ if spanned_width_needs_fixing
196
+ values[index] = [values[index], cell.send(method)].compact.send(aggregate)
197
+ entries_exist = cell.colspan.times.none? { values[index + it].nil? }
198
+ cell.colspan.times { values[index + it] = cell.send(method) if entries_exist }
199
+ end
200
+ elsif spanned_width_needs_fixing && cell.class == Cell::SpanDummy
201
+ values[index] = [values[index], cell.send(method)].compact.send(aggregate)
202
+ end
203
+ end
204
+ values.values.inject(0, :+)
205
+ end
206
+ end
207
+ end
208
+ end