hexapdf 0.32.2 → 0.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -1
  3. data/README.md +9 -0
  4. data/examples/002-graphics.rb +15 -17
  5. data/examples/003-arcs.rb +9 -9
  6. data/examples/009-text_layouter_alignment.rb +1 -1
  7. data/examples/010-text_layouter_inline_boxes.rb +2 -2
  8. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  9. data/examples/012-text_layouter_styling.rb +7 -7
  10. data/examples/013-text_layouter_shapes.rb +1 -1
  11. data/examples/014-text_in_polygon.rb +1 -1
  12. data/examples/015-boxes.rb +8 -7
  13. data/examples/016-frame_automatic_box_placement.rb +2 -2
  14. data/examples/017-frame_text_flow.rb +2 -1
  15. data/examples/018-composer.rb +1 -1
  16. data/examples/020-column_box.rb +2 -1
  17. data/examples/025-table_box.rb +46 -0
  18. data/lib/hexapdf/cli/command.rb +5 -2
  19. data/lib/hexapdf/cli/form.rb +5 -5
  20. data/lib/hexapdf/cli/inspect.rb +3 -3
  21. data/lib/hexapdf/composer.rb +104 -52
  22. data/lib/hexapdf/configuration.rb +44 -39
  23. data/lib/hexapdf/content/canvas.rb +393 -267
  24. data/lib/hexapdf/content/color_space.rb +72 -25
  25. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  26. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  27. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  28. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  29. data/lib/hexapdf/content/graphic_object.rb +6 -7
  30. data/lib/hexapdf/content/graphics_state.rb +54 -45
  31. data/lib/hexapdf/content/operator.rb +52 -54
  32. data/lib/hexapdf/content/parser.rb +2 -2
  33. data/lib/hexapdf/content/processor.rb +15 -15
  34. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  35. data/lib/hexapdf/content.rb +5 -0
  36. data/lib/hexapdf/dictionary.rb +6 -5
  37. data/lib/hexapdf/dictionary_fields.rb +42 -14
  38. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  39. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  40. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  41. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  42. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  43. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  44. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  45. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  46. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  47. data/lib/hexapdf/digital_signature/verification_result.rb +2 -2
  48. data/lib/hexapdf/digital_signature.rb +7 -2
  49. data/lib/hexapdf/document/destinations.rb +12 -11
  50. data/lib/hexapdf/document/files.rb +1 -1
  51. data/lib/hexapdf/document/fonts.rb +1 -1
  52. data/lib/hexapdf/document/layout.rb +167 -39
  53. data/lib/hexapdf/document/pages.rb +3 -2
  54. data/lib/hexapdf/document.rb +89 -55
  55. data/lib/hexapdf/encryption/aes.rb +5 -5
  56. data/lib/hexapdf/encryption/arc4.rb +1 -1
  57. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  58. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  59. data/lib/hexapdf/encryption/identity.rb +1 -1
  60. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  61. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  63. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  64. data/lib/hexapdf/encryption.rb +7 -2
  65. data/lib/hexapdf/error.rb +18 -0
  66. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  67. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  68. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  69. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  70. data/lib/hexapdf/filter/pass_through.rb +1 -1
  71. data/lib/hexapdf/filter/predictor.rb +1 -1
  72. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  73. data/lib/hexapdf/filter.rb +55 -6
  74. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  75. data/lib/hexapdf/font/cmap.rb +1 -1
  76. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  77. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  79. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  82. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  83. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  84. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  85. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  86. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  87. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  88. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  89. data/lib/hexapdf/font_loader.rb +3 -0
  90. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  91. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  92. data/lib/hexapdf/image_loader/png.rb +2 -2
  93. data/lib/hexapdf/image_loader.rb +1 -1
  94. data/lib/hexapdf/importer.rb +13 -0
  95. data/lib/hexapdf/layout/box.rb +9 -2
  96. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  97. data/lib/hexapdf/layout/column_box.rb +18 -4
  98. data/lib/hexapdf/layout/frame.rb +30 -12
  99. data/lib/hexapdf/layout/image_box.rb +5 -0
  100. data/lib/hexapdf/layout/inline_box.rb +1 -0
  101. data/lib/hexapdf/layout/list_box.rb +17 -1
  102. data/lib/hexapdf/layout/page_style.rb +4 -4
  103. data/lib/hexapdf/layout/style.rb +18 -3
  104. data/lib/hexapdf/layout/table_box.rb +682 -0
  105. data/lib/hexapdf/layout/text_box.rb +5 -3
  106. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  107. data/lib/hexapdf/layout/text_layouter.rb +12 -4
  108. data/lib/hexapdf/layout.rb +1 -0
  109. data/lib/hexapdf/name_tree_node.rb +1 -1
  110. data/lib/hexapdf/number_tree_node.rb +1 -1
  111. data/lib/hexapdf/object.rb +18 -7
  112. data/lib/hexapdf/parser.rb +7 -7
  113. data/lib/hexapdf/pdf_array.rb +1 -1
  114. data/lib/hexapdf/rectangle.rb +1 -1
  115. data/lib/hexapdf/reference.rb +1 -1
  116. data/lib/hexapdf/revision.rb +1 -1
  117. data/lib/hexapdf/revisions.rb +3 -3
  118. data/lib/hexapdf/serializer.rb +15 -15
  119. data/lib/hexapdf/stream.rb +4 -2
  120. data/lib/hexapdf/tokenizer.rb +14 -14
  121. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  122. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  123. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  124. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  125. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  127. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  128. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  129. data/lib/hexapdf/type/acro_form.rb +1 -1
  130. data/lib/hexapdf/type/action.rb +1 -1
  131. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  132. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  133. data/lib/hexapdf/type/actions/launch.rb +1 -1
  134. data/lib/hexapdf/type/actions/uri.rb +1 -1
  135. data/lib/hexapdf/type/actions.rb +1 -1
  136. data/lib/hexapdf/type/annotation.rb +3 -3
  137. data/lib/hexapdf/type/annotations/link.rb +1 -1
  138. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  139. data/lib/hexapdf/type/annotations/text.rb +1 -1
  140. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  141. data/lib/hexapdf/type/annotations.rb +1 -1
  142. data/lib/hexapdf/type/catalog.rb +1 -1
  143. data/lib/hexapdf/type/cid_font.rb +3 -3
  144. data/lib/hexapdf/type/embedded_file.rb +1 -1
  145. data/lib/hexapdf/type/file_specification.rb +2 -2
  146. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  147. data/lib/hexapdf/type/font_simple.rb +2 -2
  148. data/lib/hexapdf/type/font_type0.rb +3 -3
  149. data/lib/hexapdf/type/font_type3.rb +1 -1
  150. data/lib/hexapdf/type/form.rb +1 -1
  151. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  152. data/lib/hexapdf/type/icon_fit.rb +1 -1
  153. data/lib/hexapdf/type/image.rb +1 -1
  154. data/lib/hexapdf/type/info.rb +1 -1
  155. data/lib/hexapdf/type/mark_information.rb +1 -1
  156. data/lib/hexapdf/type/names.rb +2 -2
  157. data/lib/hexapdf/type/object_stream.rb +2 -1
  158. data/lib/hexapdf/type/outline.rb +1 -1
  159. data/lib/hexapdf/type/outline_item.rb +1 -1
  160. data/lib/hexapdf/type/page.rb +19 -10
  161. data/lib/hexapdf/type/page_label.rb +1 -1
  162. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  163. data/lib/hexapdf/type/resources.rb +1 -1
  164. data/lib/hexapdf/type/trailer.rb +2 -2
  165. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  166. data/lib/hexapdf/type/xref_stream.rb +2 -2
  167. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  168. data/lib/hexapdf/version.rb +1 -1
  169. data/lib/hexapdf/writer.rb +4 -4
  170. data/lib/hexapdf/xref_section.rb +2 -2
  171. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  172. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  173. data/test/hexapdf/content/test_canvas.rb +0 -1
  174. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  175. data/test/hexapdf/document/test_files.rb +2 -2
  176. data/test/hexapdf/document/test_layout.rb +98 -0
  177. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  178. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  179. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  180. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  181. data/test/hexapdf/layout/test_box.rb +1 -1
  182. data/test/hexapdf/layout/test_column_box.rb +65 -21
  183. data/test/hexapdf/layout/test_frame.rb +14 -14
  184. data/test/hexapdf/layout/test_image_box.rb +4 -0
  185. data/test/hexapdf/layout/test_inline_box.rb +5 -0
  186. data/test/hexapdf/layout/test_list_box.rb +40 -6
  187. data/test/hexapdf/layout/test_page_style.rb +3 -2
  188. data/test/hexapdf/layout/test_style.rb +50 -0
  189. data/test/hexapdf/layout/test_table_box.rb +722 -0
  190. data/test/hexapdf/layout/test_text_box.rb +18 -0
  191. data/test/hexapdf/layout/test_text_layouter.rb +4 -0
  192. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  193. data/test/hexapdf/test_document.rb +1 -0
  194. data/test/hexapdf/test_filter.rb +8 -0
  195. data/test/hexapdf/test_importer.rb +9 -0
  196. data/test/hexapdf/test_object.rb +16 -5
  197. data/test/hexapdf/test_stream.rb +7 -0
  198. data/test/hexapdf/test_writer.rb +3 -3
  199. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  200. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  201. data/test/hexapdf/type/test_page.rb +18 -4
  202. metadata +17 -8
@@ -0,0 +1,682 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2023 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/layout/box'
38
+ require 'hexapdf/layout/frame'
39
+
40
+ module HexaPDF
41
+ module Layout
42
+
43
+ # A TableBox allows placing boxes in a table.
44
+ #
45
+ # A table box instance can be fit into a rectangular area. The widths of the columns is
46
+ # determined by the #column_widths definition. This means that there is no auto-sizing
47
+ # supported.
48
+ #
49
+ # If some rows don't fit into the provided area, the table is split. The style of the original
50
+ # table is also applied to the split box.
51
+ #
52
+ #
53
+ # == Table Cell
54
+ #
55
+ # Each table cell is a Box instance and can have an associated style, e.g. for creating borders
56
+ # around the cell contents. It is also possible to create cells that span more than one row or
57
+ # column. By default a cell has a solid, black, 1pt border and a padding of 5pt on all sides.
58
+ #
59
+ # It is important to note that the drawing of cell borders (just the drawing, size calculations
60
+ # are done as usual) are handled differently from standard box borders. While standard box
61
+ # borders are drawn inside the box, cell borders are drawn on the bounds of the box. This means
62
+ # that, visually, the borders of adjoining cells overlap, with the borders of cells to the right
63
+ # and bottom being on top.
64
+ #
65
+ # To make sure that the cell borders are not outside of the table's bounds, the left and top
66
+ # border widths of the top-left cell and the right and bottom border widths of the bottom-right
67
+ # cell are taken into account when calculating the available space.
68
+ #
69
+ #
70
+ # == Examples
71
+ #
72
+ # Let's start with a basic table:
73
+ #
74
+ # #>pdf-composer
75
+ # cells = [[layout.text('A'), layout.text('B')],
76
+ # [layout.text('C'), layout.text('D')]]
77
+ # composer.table(cells)
78
+ #
79
+ # The HexaPDF::Document::Layout#table_box method accepts the cells as positional argument
80
+ # instead of as keyword argument but all other arguments of ::new work the same.
81
+ #
82
+ # While the table box itself only allows box instances as cell contents, the layout helper
83
+ # method also allows text which it transforms to text boxes. So this is the same as the above:
84
+ #
85
+ # #>pdf-composer
86
+ # composer.table([['A', 'B'], ['C', 'D']])
87
+ #
88
+ # The style of the cells can be customized, e.g. to avoid drawing borders:
89
+ #
90
+ # #>pdf-composer
91
+ # cells = [[layout.text('A'), layout.text('B')],
92
+ # [layout.text('C'), layout.text('D')]]
93
+ # composer.table(cells, cell_style: {border: {width: 0}})
94
+ #
95
+ # If the table doesn't fit completely, it is automatically split (in this case, the last row
96
+ # gets moved to the second column):
97
+ #
98
+ # #>pdf-composer
99
+ # cells = [[layout.text('A'), layout.text('B')],
100
+ # [layout.text('C'), layout.text('D')],
101
+ # [layout.text('E'), layout.text('F')]]
102
+ # composer.column(height: 50) {|col| col.table(cells) }
103
+ #
104
+ # It is also possible to use row and column spans:
105
+ #
106
+ # #>pdf-composer
107
+ # cells = [[{content: layout.text('A'), col_span: 2}, {content: layout.text('B'), row_span: 2}],
108
+ # [{content: layout.text('C'), col_span: 2, row_span: 2}],
109
+ # [layout.text('D')]]
110
+ # composer.table(cells)
111
+ #
112
+ # Each table can have header rows and footer rows which are shown for all split parts:
113
+ #
114
+ # #>pdf-composer
115
+ # header = lambda {|tb| [[{content: layout.text('Header', align: :center), col_span: 2}]] }
116
+ # footer = lambda {|tb| [[layout.text('left'), layout.text('right', align: :right)]] }
117
+ # cells = [[layout.text('A'), layout.text('B')],
118
+ # [layout.text('C'), layout.text('D')],
119
+ # [layout.text('E'), layout.text('F')]]
120
+ # composer.column(height: 90) {|col| col.table(cells, header: header, footer: footer) }
121
+ #
122
+ # The cells can be styled using a callable object for more complex styling:
123
+ #
124
+ # #>pdf-composer
125
+ # cells = [[layout.text('A'), layout.text('B')],
126
+ # [layout.text('C'), layout.text('D')]]
127
+ # block = lambda do |cell|
128
+ # cell.style.background_color =
129
+ # (cell.row == 0 && cell.column == 0 ? 'ffffaa' : 'ffffee')
130
+ # end
131
+ # composer.table(cells, cell_style: block)
132
+ class TableBox < Box
133
+
134
+ # Represents a single cell of the table.
135
+ #
136
+ # A cell is a container box that fits and draws its children with a BoxFitter. Its dimensions
137
+ # (width and height) are not determined by its children but by the table layout algorithm.
138
+ # Furthermore, its style can be used for drawing e.g. a cell border.
139
+ #
140
+ # Cell borders work similar to the separated borders model of CSS, i.e. each cell has its own
141
+ # borders that do not overlap.
142
+ class Cell < Box
143
+
144
+ # The x-coordinate of the cell's top-left corner.
145
+ #
146
+ # The coordinate is relative to the table's content rectangle, with positive x-axis going to
147
+ # the right and positive y-axis going to the bottom.
148
+ #
149
+ # This value is set by the parent Cells object during fitting and may therefore only be
150
+ # relied on afterwards.
151
+ attr_accessor :left
152
+
153
+ # The y-coordinate of the cell's top-left corner.
154
+ #
155
+ # The coordinate is relative to the table's content rectangle, with positive x-axis going to
156
+ # the right and positive y-axis going to the bottom.
157
+ #
158
+ # This value is set by the parent Cells object during fitting and may therefore only be
159
+ # relied on afterwards.
160
+ attr_accessor :top
161
+
162
+ # The preferred width of the cell, determined during #fit.
163
+ attr_reader :preferred_width
164
+
165
+ # The preferred height of the cell, determined during #fit.
166
+ attr_reader :preferred_height
167
+
168
+ # The 0-based row number of the cell.
169
+ attr_reader :row
170
+
171
+ # The 0-based column number of the cell.
172
+ attr_reader :column
173
+
174
+ # The number of rows this cell spans.
175
+ attr_reader :row_span
176
+
177
+ # The number of columns this cell spans.
178
+ attr_reader :col_span
179
+
180
+ # The boxes to layout inside this cell.
181
+ #
182
+ # This may either be +nil+ (if the cell has no content), a single Box instance or an array
183
+ # of Box instances.
184
+ attr_accessor :children
185
+
186
+ # Creates a new Cell instance.
187
+ def initialize(row:, column:, children: nil, row_span: nil, col_span: nil, **kwargs)
188
+ super(**kwargs, width: 0, height: 0)
189
+ @children = children
190
+ @row = row
191
+ @column = column
192
+ @row_span = row_span || 1
193
+ @col_span = col_span || 1
194
+ style.border.width.set(1) unless style.border?
195
+ style.border.draw_on_bounds = true
196
+ style.padding.set(5) unless style.padding?
197
+ end
198
+
199
+ # Returns +true+ if the cell has no content.
200
+ def empty?
201
+ super && (!@fit_results || @fit_results.empty?)
202
+ end
203
+
204
+ # Updates the height of the box to the given value.
205
+ #
206
+ # The +height+ has to be greater than or equal to the fitted height.
207
+ def update_height(height)
208
+ if height < @height
209
+ raise HexaPDF::Error, "Given height needs to be at least as big as fitted height"
210
+ end
211
+ @height = height
212
+ end
213
+
214
+ # Fits the children of the table cell into the given rectangular area.
215
+ def fit(available_width, available_height, _frame)
216
+ @width = available_width
217
+ width = available_width - reserved_width
218
+ height = available_height - reserved_height
219
+ return false if width <= 0 || height <= 0
220
+
221
+ frame = Frame.new(0, 0, width, height)
222
+ case children
223
+ when Box
224
+ fit_result = frame.fit(children)
225
+ @preferred_width = fit_result.x + fit_result.box.width + reserved_width
226
+ @height = @preferred_height = fit_result.box.height + reserved_height
227
+ @fit_results = [fit_result]
228
+ @fit_successful = fit_result.success?
229
+ when Array
230
+ box_fitter = BoxFitter.new([frame])
231
+ children.each {|box| box_fitter.fit(box) }
232
+ max_x_result = box_fitter.fit_results.max_by {|result| result.x + result.box.width }
233
+ @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
234
+ @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
235
+ @fit_results = box_fitter.fit_results
236
+ @fit_successful = box_fitter.fit_successful?
237
+ else
238
+ @preferred_width = reserved_width
239
+ @height = @preferred_height = reserved_height
240
+ @fit_results = []
241
+ @fit_successful = true
242
+ end
243
+ end
244
+
245
+ # :nodoc:
246
+ def inspect
247
+ "<Cell (#{row},#{column}) #{row_span}x#{col_span} #{Array(children).map(&:class)}>"
248
+ end
249
+
250
+ private
251
+
252
+ # Draws the content of the cell.
253
+ def draw_content(canvas, x, y)
254
+ return if @fit_results.empty?
255
+
256
+ # available_width is always equal to content_width but we need to adjust for the
257
+ # difference in the y direction between fitting and drawing
258
+ y -= (@fit_results[0].available_height - content_height)
259
+ @fit_results.each do |fit_result|
260
+ fit_result.x += x
261
+ fit_result.y += y
262
+ fit_result.draw(canvas)
263
+ end
264
+ end
265
+
266
+ end
267
+
268
+ # Represents the cells of a TableBox.
269
+ #
270
+ # This class is a wrapper around an array of arrays and provides some utility methods for
271
+ # managing and styling the cells.
272
+ #
273
+ # == Table data transformation into correct form
274
+ #
275
+ # One of the main purposes of this class is to transform the cell data provided on
276
+ # initialization into the representation a TableBox instance can work with.
277
+ #
278
+ # The +data+ argument for ::new is an array of arrays representing the rows of the table. Each
279
+ # row array may contain one of the following items:
280
+ #
281
+ # * A single Box instance defining the content of the cell.
282
+ #
283
+ # * An array of Box instances defining the content of the cell.
284
+ #
285
+ # * A hash which defines the content of the cell as well as, optionally, additional
286
+ # information through the following keys:
287
+ #
288
+ # +:content+:: The content for the cell. This may be a single Box or an array of Box
289
+ # instances.
290
+ #
291
+ # +:row_span+:: An integer specifying the number of rows this cell should span.
292
+ #
293
+ # +:col_span+:: An integer specifying the number of columsn this cell should span.
294
+ #
295
+ # +:properties+:: A hash of properties (see Box#properties) to be set on the cell itself.
296
+ #
297
+ # All other key-value pairs are taken to be cell styling information (like
298
+ # +:background_color+) and assigned to the cell style.
299
+ #
300
+ # Additionally, the first item in the +data+ argument is treated specially if it is not an
301
+ # array:
302
+ #
303
+ # * If it is a hash, it is assumed to be style properties to be set on all created cell
304
+ # instances.
305
+ #
306
+ # * If it is a callable object, it needs to accept a cell as argument and is called for all
307
+ # created cell instances.
308
+ #
309
+ # Any properties or styling information retrieved from the respective item in +data+ takes
310
+ # precedence over the above globally specified information.
311
+ #
312
+ # Here is an example input data array:
313
+ #
314
+ # data = [[box1, {col_span: 2, content: box2}, box3],
315
+ # [box4, box5, {col_span: 2, row_span: 2, content: [box6.1, box6.2]}],
316
+ # [box7, box8]]
317
+ #
318
+ # And this is what the table will look like:
319
+ #
320
+ # | box1 | box2 | box 3 |
321
+ # | box4 | box5 | box6.1 box6.2 |
322
+ # | box7 | box8 | |
323
+ class Cells
324
+
325
+ # Creates a new Cells instance with the given +data+ which cannot be changed afterwards.
326
+ #
327
+ # The optional +cell_style+ argument can either be a hash of style properties to be assigned
328
+ # to every cell or a block accepting a cell for more control over e.g. style assignment. If
329
+ # the +data+ has such a cell style as its first item, the +cell_style+ argument is not used.
330
+ #
331
+ # See the class documentation for details on the +data+ argument.
332
+ def initialize(data, cell_style: nil)
333
+ @cells = []
334
+ @number_of_columns = 0
335
+ assign_data(data, cell_style)
336
+ end
337
+
338
+ # Returns the cell (a Cell instance) in the given row and column.
339
+ #
340
+ # Note that the same cell instance may be returned for different (row, column) arguments if
341
+ # the cell spans more than one row and/or column.
342
+ def [](row, column)
343
+ @cells[row]&.[](column)
344
+ end
345
+
346
+ # Returns the number of rows.
347
+ def number_of_rows
348
+ @cells.size
349
+ end
350
+
351
+ # Returns the number of columns.
352
+ def number_of_columns
353
+ @number_of_columns
354
+ end
355
+
356
+ # Iterates over each row.
357
+ def each_row(&block)
358
+ @cells.each(&block)
359
+ end
360
+
361
+ # Applies the given style properties to all cells and optionally yields all cells for more
362
+ # complex customization.
363
+ def style(**properties, &block)
364
+ @cells.each do |columns|
365
+ columns.each do |cell|
366
+ cell.style.update(**properties)
367
+ block&.call(cell)
368
+ end
369
+ end
370
+ end
371
+
372
+ # Fits all rows starting from +start_row+ into an area with the given +available_height+,
373
+ # using the column information in +column_info+. Returns the used height as well as the row
374
+ # index of the last row that fit (which may be -1 if no row fits).
375
+ #
376
+ # The +column_info+ argument needs to be an array of arrays of the form [x_pos, width]
377
+ # containing the horizontal positions and widths of each column.
378
+ #
379
+ # The fitting of a cell is done through the Cell#fit method which stores the result in the
380
+ # cell itself. Furthermore, Cell#left and Cell#top are also assigned correctly.
381
+ def fit_rows(start_row, available_height, column_info)
382
+ height = available_height
383
+ last_fitted_row_index = -1
384
+ @cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
385
+ row_fit = true
386
+ row_height = 0
387
+ columns.each_with_index do |cell, col_index|
388
+ next if cell.row != row_index || cell.column != col_index
389
+ available_cell_width = if cell.col_span > 1
390
+ column_info[cell.column, cell.col_span].map(&:last).sum
391
+ else
392
+ column_info[cell.column].last
393
+ end
394
+ unless cell.fit(available_cell_width, available_height, nil)
395
+ row_fit = false
396
+ break
397
+ end
398
+ cell.left = column_info[cell.column].first
399
+ cell.top = height - available_height
400
+ row_height = cell.preferred_height if row_height < cell.preferred_height
401
+ end
402
+
403
+ if row_fit
404
+ seen = {}
405
+ columns.each do |cell|
406
+ next if seen[cell]
407
+ cell.update_height(cell.row == row_index ? row_height : cell.height + row_height)
408
+ seen[cell] = true
409
+ end
410
+
411
+ last_fitted_row_index = row_index
412
+ available_height -= row_height
413
+ else
414
+ last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
415
+ break
416
+ end
417
+ end
418
+ [height - available_height, last_fitted_row_index]
419
+ end
420
+
421
+ # Draws the rows from +start_row+ to +end_row+ on the given +canvas+, with the top-left
422
+ # corner of the resulting table being at (+x+, +y+).
423
+ def draw_rows(start_row, end_row, canvas, x, y)
424
+ @cells[start_row..end_row].each.with_index(start_row) do |columns, row_index|
425
+ columns.each_with_index do |cell, col_index|
426
+ next if cell.row != row_index || cell.column != col_index
427
+ cell.draw(canvas, x + cell.left, y - cell.top - cell.height)
428
+ end
429
+ end
430
+ end
431
+
432
+ private
433
+
434
+ # Assigns the +data+ to the individual cells, taking row and column spans into account.
435
+ #
436
+ # For details on the +cell_style+ argument see ::new.
437
+ def assign_data(data, cell_style)
438
+ cell_style = data.shift unless data[0].kind_of?(Array)
439
+ cell_style_block = if cell_style.kind_of?(Hash)
440
+ lambda {|cell| cell.style.update(**cell_style) }
441
+ else
442
+ cell_style
443
+ end
444
+
445
+ data.each_with_index do |cols, row_index|
446
+ # Only add new row array if it hasn't been added due to row spans before
447
+ @cells << [] unless @cells[row_index]
448
+ row = @cells[row_index]
449
+ col_index = 0
450
+
451
+ cols.each do |content|
452
+ # Ignore already filled in cells due to row/col spans
453
+ col_index += 1 while row[col_index]
454
+
455
+ children = content
456
+ if content.kind_of?(Hash)
457
+ children = content.delete(:content)
458
+ row_span = content.delete(:row_span)
459
+ col_span = content.delete(:col_span)
460
+ properties = content.delete(:properties)
461
+ style = content
462
+ end
463
+ cell = Cell.new(children: children, row: row_index, column: col_index,
464
+ row_span: row_span, col_span: col_span)
465
+ cell_style_block&.call(cell)
466
+ cell.style.update(**style) if style
467
+ cell.properties.update(properties) if properties
468
+
469
+ row[col_index] = cell
470
+ if cell.row_span > 1 || cell.col_span > 1
471
+ row_index.upto(row_index + cell.row_span - 1) do |r|
472
+ @cells << [] unless @cells[r]
473
+ col_index.upto(col_index + cell.col_span - 1) do |c|
474
+ @cells[r][c] = cell
475
+ end
476
+ end
477
+ end
478
+
479
+ col_index += cell.col_span
480
+ end
481
+
482
+ @number_of_columns = col_index if @number_of_columns < col_index
483
+ end
484
+ end
485
+
486
+ end
487
+
488
+ # The Cells instance containing the data of the table.
489
+ #
490
+ # If this is an instance that was split from another one, the cells contain *all* the rows,
491
+ # not just the ones for this split instance.
492
+ #
493
+ # Also see #start_row_index.
494
+ attr_reader :cells
495
+
496
+ # The Cells instance containing the header cells of the table.
497
+ #
498
+ # If this is a TableBox instance that was split from another one, the header cells are created
499
+ # again through the use of +header+ block supplied to ::new.
500
+ attr_reader :header_cells
501
+
502
+ # The Cells instance containing the footer cells of the table.
503
+ #
504
+ # If this is a TableBox instance that was split from another one, the footer cells are created
505
+ # again through the use of +footer+ block supplied to ::new.
506
+ attr_reader :footer_cells
507
+
508
+ # The column widths definition.
509
+ #
510
+ # See ::new for details.
511
+ attr_reader :column_widths
512
+
513
+ # The row index into the #cells from which this instance starts fitting the rows.
514
+ #
515
+ # This value is 0 if this instance was not split from another one. Otherwise, it contains the
516
+ # correct start index.
517
+ attr_reader :start_row_index
518
+
519
+ # This value is -1 if #fit was not yet called. Otherwise it contains the row index of the last
520
+ # row that could be fitted.
521
+ attr_reader :last_fitted_row_index
522
+
523
+ # Creates a new TableBox instance.
524
+ #
525
+ # +cells+::
526
+ #
527
+ # This needs to be an array of arrays containing the data of the table. See Cells for more
528
+ # information on the allowed contents.
529
+ #
530
+ # Alternatively, a Cells instance can be used. Note that in this case the +cell_style+
531
+ # argument is not used.
532
+ #
533
+ # +column_widths+::
534
+ #
535
+ # An array defining the width of the columns of the table. If not set, defaults to an
536
+ # empty array.
537
+ #
538
+ # Each entry in the array may either be a positive or negative number. A positive number
539
+ # sets a fixed width for the respective column.
540
+ #
541
+ # A negative number specifies that the respective column is auto-sized. Such columns split
542
+ # the remaining width (after substracting the widths of the fixed columns) proportionally
543
+ # among them. For example, if the column width definition is [-1, -2, -2], the first
544
+ # column is a fifth of the width and the other two columns are each two fifth of the
545
+ # width.
546
+ #
547
+ # If the +cells+ definition has more columns than specified by +column_widths+, the
548
+ # missing entries are assumed to be -1.
549
+ #
550
+ # +header+::
551
+ #
552
+ # A callable object that needs to accept this TableBox instance as argument and that
553
+ # returns an array of arrays containing the header rows.
554
+ #
555
+ # The header rows are shown for the table instance and all split boxes.
556
+ #
557
+ # +footer+::
558
+ #
559
+ # A callable object that needs to accept this TableBox instance as argument and that
560
+ # returns an array of arrays containing the footer rows.
561
+ #
562
+ # The footer rows are shown for the table instance and all split boxes.
563
+ #
564
+ # +cell_style+::
565
+ #
566
+ # Contains styling information that should be applied to all header, body and footer
567
+ # cells.
568
+ #
569
+ # This can either be a hash containing style properties or a callable object accepting a
570
+ # cell as argument.
571
+ def initialize(cells:, column_widths: nil, header: nil, footer: nil, cell_style: nil, **kwargs)
572
+ super(**kwargs)
573
+ @cell_style = cell_style
574
+ @cells = cells.kind_of?(Cells) ? cells : Cells.new(cells, cell_style: @cell_style)
575
+ @column_widths = column_widths || []
576
+ @start_row_index = 0
577
+ @last_fitted_row_index = -1
578
+ @header = header
579
+ @header_cells = Cells.new(header.call(self), cell_style: @cell_style) if header
580
+ @footer = footer
581
+ @footer_cells = Cells.new(footer.call(self), cell_style: @cell_style) if footer
582
+ end
583
+
584
+ # Returns +true+ if not a single row could be fit.
585
+ def empty?
586
+ super && (!@last_fitted_row_index || @last_fitted_row_index < 0)
587
+ end
588
+
589
+ # Fits the table into the available space.
590
+ def fit(available_width, available_height, _frame)
591
+ return false if (@initial_width > 0 && @initial_width > available_width) ||
592
+ (@initial_height > 0 && @initial_height > available_height)
593
+
594
+ # Adjust reserved width/height to include space used by the edge cells for their border
595
+ # since cell borders are drawn on the bounds and not inside.
596
+ # This uses the top-left and bottom-right cells and so might not be correct in all cases.
597
+ @cell_tl_border_width = @cells[0, 0].style.border.width
598
+ cell_br_border_width = @cells[-1, -1].style.border.width
599
+ rw = reserved_width + (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
600
+ rh = reserved_height + (@cell_tl_border_width.top + cell_br_border_width.bottom) / 2.0
601
+
602
+ width = (@initial_width > 0 ? @initial_width : available_width) - rw
603
+ height = (@initial_height > 0 ? @initial_height : available_height) - rh
604
+ used_height = 0
605
+ columns = calculate_column_widths(width)
606
+ return false if columns.empty?
607
+
608
+ @special_cells_fit_not_successful = false
609
+ [@header_cells, @footer_cells].each do |special_cells|
610
+ next unless special_cells
611
+ special_used_height, last_fitted_row_index = special_cells.fit_rows(0, height, columns)
612
+ height -= special_used_height
613
+ used_height += special_used_height
614
+ @special_cells_fit_not_successful = (last_fitted_row_index != special_cells.number_of_rows - 1)
615
+ return false if @special_cells_fit_not_successful
616
+ end
617
+
618
+ main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns)
619
+ used_height += main_used_height
620
+
621
+ @width = (@initial_width > 0 ? @initial_width : columns[-1].sum + rw)
622
+ @height = (@initial_height > 0 ? @initial_height : used_height + rh)
623
+ @fit_successful = (@last_fitted_row_index == @cells.number_of_rows - 1)
624
+ end
625
+
626
+ private
627
+
628
+ # Calculates and returns the x-coordinates and widths of all columns based on the given total
629
+ # available width.
630
+ #
631
+ # If it is not possible to fit all columns into the given +width+, an empty array is returned.
632
+ def calculate_column_widths(width)
633
+ @column_widths.concat([-1] * (@cells.number_of_columns - @column_widths.size))
634
+ fixed_width, variable_width = @column_widths.partition(&:positive?).map {|c| c.sum(&:abs) }
635
+ rest_width = width - fixed_width
636
+ return [] if rest_width <= 0
637
+
638
+ variable_width_unit = rest_width / variable_width.to_f
639
+ position = 0
640
+ @column_widths.map do |column|
641
+ result = column > 0 ? [position, column] : [position, column.abs * variable_width_unit]
642
+ position += result[1]
643
+ result
644
+ end
645
+ end
646
+
647
+ # Splits the content of the column box. This method is called from Box#split.
648
+ def split_content(_available_width, _available_height, _frame)
649
+ if @special_cells_fit_not_successful || @last_fitted_row_index < 0
650
+ [nil, self]
651
+ else
652
+ box = create_split_box
653
+ box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
654
+ box.instance_variable_set(:@last_fitted_row_index, -1)
655
+ box.instance_variable_set(:@special_cells_fit_not_successful, nil)
656
+ header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
657
+ box.instance_variable_set(:@header_cells, header_cells)
658
+ footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
659
+ box.instance_variable_set(:@footer_cells, footer_cells)
660
+ [self, box]
661
+ end
662
+ end
663
+
664
+ # Draws the child boxes onto the canvas at position [x, y].
665
+ def draw_content(canvas, x, y)
666
+ x += @cell_tl_border_width.left / 2.0
667
+ y += content_height - @cell_tl_border_width.top / 2.0
668
+ if @header_cells
669
+ @header_cells.draw_rows(0, -1, canvas, x, y)
670
+ y -= @header_cells[-1, 0].top + @header_cells[-1, 0].height
671
+ end
672
+ @cells.draw_rows(@start_row_index, @last_fitted_row_index, canvas, x, y)
673
+ if @footer_cells
674
+ y -= @cells[@last_fitted_row_index, 0].top + @cells[@last_fitted_row_index, 0].height
675
+ @footer_cells.draw_rows(0, -1, canvas, x, y)
676
+ end
677
+ end
678
+
679
+ end
680
+
681
+ end
682
+ end