hexapdf 0.32.2 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -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/examples/026-optional_content.rb +55 -0
  19. data/examples/027-composer_optional_content.rb +83 -0
  20. data/lib/hexapdf/cli/command.rb +12 -3
  21. data/lib/hexapdf/cli/fonts.rb +1 -1
  22. data/lib/hexapdf/cli/form.rb +5 -5
  23. data/lib/hexapdf/cli/inspect.rb +5 -7
  24. data/lib/hexapdf/composer.rb +106 -53
  25. data/lib/hexapdf/configuration.rb +65 -40
  26. data/lib/hexapdf/content/canvas.rb +445 -267
  27. data/lib/hexapdf/content/color_space.rb +72 -25
  28. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  32. data/lib/hexapdf/content/graphic_object.rb +6 -7
  33. data/lib/hexapdf/content/graphics_state.rb +54 -45
  34. data/lib/hexapdf/content/operator.rb +54 -54
  35. data/lib/hexapdf/content/parser.rb +2 -2
  36. data/lib/hexapdf/content/processor.rb +15 -15
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +5 -0
  39. data/lib/hexapdf/dictionary.rb +7 -5
  40. data/lib/hexapdf/dictionary_fields.rb +43 -16
  41. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  42. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  43. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  44. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  45. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  46. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  47. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  48. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  49. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  50. data/lib/hexapdf/digital_signature/verification_result.rb +3 -4
  51. data/lib/hexapdf/digital_signature.rb +7 -2
  52. data/lib/hexapdf/document/destinations.rb +12 -11
  53. data/lib/hexapdf/document/files.rb +1 -1
  54. data/lib/hexapdf/document/fonts.rb +1 -1
  55. data/lib/hexapdf/document/layout.rb +170 -39
  56. data/lib/hexapdf/document/pages.rb +4 -3
  57. data/lib/hexapdf/document.rb +96 -55
  58. data/lib/hexapdf/encryption/aes.rb +5 -5
  59. data/lib/hexapdf/encryption/arc4.rb +1 -1
  60. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  61. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/identity.rb +1 -1
  63. data/lib/hexapdf/encryption/ruby_aes.rb +11 -21
  64. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  65. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  66. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  67. data/lib/hexapdf/encryption.rb +7 -2
  68. data/lib/hexapdf/error.rb +18 -0
  69. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  70. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  71. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  72. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  73. data/lib/hexapdf/filter/pass_through.rb +1 -1
  74. data/lib/hexapdf/filter/predictor.rb +1 -1
  75. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  76. data/lib/hexapdf/filter.rb +55 -6
  77. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  78. data/lib/hexapdf/font/cmap.rb +1 -1
  79. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  82. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  83. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  84. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  85. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  86. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  87. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  88. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  89. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  90. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  91. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  92. data/lib/hexapdf/font_loader.rb +3 -0
  93. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  94. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  95. data/lib/hexapdf/image_loader/png.rb +2 -2
  96. data/lib/hexapdf/image_loader.rb +1 -1
  97. data/lib/hexapdf/importer.rb +13 -0
  98. data/lib/hexapdf/layout/box.rb +32 -5
  99. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  100. data/lib/hexapdf/layout/column_box.rb +20 -5
  101. data/lib/hexapdf/layout/frame.rb +53 -18
  102. data/lib/hexapdf/layout/image_box.rb +5 -0
  103. data/lib/hexapdf/layout/inline_box.rb +21 -9
  104. data/lib/hexapdf/layout/list_box.rb +50 -20
  105. data/lib/hexapdf/layout/page_style.rb +6 -5
  106. data/lib/hexapdf/layout/style.rb +64 -9
  107. data/lib/hexapdf/layout/table_box.rb +684 -0
  108. data/lib/hexapdf/layout/text_box.rb +12 -3
  109. data/lib/hexapdf/layout/text_fragment.rb +29 -3
  110. data/lib/hexapdf/layout/text_layouter.rb +32 -8
  111. data/lib/hexapdf/layout.rb +1 -0
  112. data/lib/hexapdf/name_tree_node.rb +1 -1
  113. data/lib/hexapdf/number_tree_node.rb +1 -1
  114. data/lib/hexapdf/object.rb +18 -7
  115. data/lib/hexapdf/parser.rb +7 -7
  116. data/lib/hexapdf/pdf_array.rb +1 -1
  117. data/lib/hexapdf/rectangle.rb +1 -1
  118. data/lib/hexapdf/reference.rb +1 -1
  119. data/lib/hexapdf/revision.rb +1 -1
  120. data/lib/hexapdf/revisions.rb +3 -3
  121. data/lib/hexapdf/serializer.rb +15 -15
  122. data/lib/hexapdf/stream.rb +5 -4
  123. data/lib/hexapdf/tokenizer.rb +14 -14
  124. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  125. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  127. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  128. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  129. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  130. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  131. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  132. data/lib/hexapdf/type/acro_form.rb +1 -1
  133. data/lib/hexapdf/type/action.rb +1 -1
  134. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  135. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  136. data/lib/hexapdf/type/actions/launch.rb +1 -1
  137. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  138. data/lib/hexapdf/type/actions/uri.rb +1 -1
  139. data/lib/hexapdf/type/actions.rb +2 -1
  140. data/lib/hexapdf/type/annotation.rb +3 -3
  141. data/lib/hexapdf/type/annotations/link.rb +1 -1
  142. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  143. data/lib/hexapdf/type/annotations/text.rb +2 -3
  144. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  145. data/lib/hexapdf/type/annotations.rb +1 -1
  146. data/lib/hexapdf/type/catalog.rb +11 -2
  147. data/lib/hexapdf/type/cid_font.rb +18 -4
  148. data/lib/hexapdf/type/embedded_file.rb +1 -1
  149. data/lib/hexapdf/type/file_specification.rb +2 -2
  150. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  151. data/lib/hexapdf/type/font_simple.rb +2 -2
  152. data/lib/hexapdf/type/font_type0.rb +3 -3
  153. data/lib/hexapdf/type/font_type3.rb +1 -1
  154. data/lib/hexapdf/type/form.rb +76 -6
  155. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  156. data/lib/hexapdf/type/icon_fit.rb +1 -1
  157. data/lib/hexapdf/type/image.rb +1 -1
  158. data/lib/hexapdf/type/info.rb +1 -1
  159. data/lib/hexapdf/type/mark_information.rb +1 -1
  160. data/lib/hexapdf/type/names.rb +2 -2
  161. data/lib/hexapdf/type/object_stream.rb +2 -1
  162. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  163. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  164. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  165. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  166. data/lib/hexapdf/type/outline.rb +1 -1
  167. data/lib/hexapdf/type/outline_item.rb +1 -1
  168. data/lib/hexapdf/type/page.rb +46 -21
  169. data/lib/hexapdf/type/page_label.rb +5 -9
  170. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  171. data/lib/hexapdf/type/resources.rb +1 -1
  172. data/lib/hexapdf/type/trailer.rb +2 -2
  173. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  174. data/lib/hexapdf/type/xref_stream.rb +2 -2
  175. data/lib/hexapdf/type.rb +4 -0
  176. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -2
  177. data/lib/hexapdf/version.rb +1 -1
  178. data/lib/hexapdf/writer.rb +4 -4
  179. data/lib/hexapdf/xref_section.rb +2 -2
  180. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  181. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  182. data/test/hexapdf/content/test_canvas.rb +49 -1
  183. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  184. data/test/hexapdf/document/test_files.rb +2 -2
  185. data/test/hexapdf/document/test_layout.rb +105 -2
  186. data/test/hexapdf/document/test_pages.rb +6 -6
  187. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  188. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  189. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  190. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  191. data/test/hexapdf/layout/test_box.rb +14 -5
  192. data/test/hexapdf/layout/test_column_box.rb +65 -21
  193. data/test/hexapdf/layout/test_frame.rb +27 -15
  194. data/test/hexapdf/layout/test_image_box.rb +4 -0
  195. data/test/hexapdf/layout/test_inline_box.rb +17 -3
  196. data/test/hexapdf/layout/test_list_box.rb +84 -33
  197. data/test/hexapdf/layout/test_page_style.rb +3 -2
  198. data/test/hexapdf/layout/test_style.rb +60 -0
  199. data/test/hexapdf/layout/test_table_box.rb +728 -0
  200. data/test/hexapdf/layout/test_text_box.rb +26 -0
  201. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  202. data/test/hexapdf/layout/test_text_layouter.rb +36 -5
  203. data/test/hexapdf/test_composer.rb +10 -0
  204. data/test/hexapdf/test_dictionary.rb +10 -0
  205. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  206. data/test/hexapdf/test_document.rb +5 -0
  207. data/test/hexapdf/test_filter.rb +8 -0
  208. data/test/hexapdf/test_importer.rb +9 -0
  209. data/test/hexapdf/test_object.rb +16 -5
  210. data/test/hexapdf/test_stream.rb +7 -0
  211. data/test/hexapdf/test_writer.rb +3 -3
  212. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  213. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  214. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  215. data/test/hexapdf/type/test_catalog.rb +11 -0
  216. data/test/hexapdf/type/test_form.rb +119 -0
  217. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  218. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  219. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  220. data/test/hexapdf/type/test_page.rb +20 -6
  221. metadata +28 -8
@@ -0,0 +1,684 @@
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, context: frame.context)
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 +frame+ argument is further handed down to the Cell instances for fitting.
380
+ #
381
+ # The fitting of a cell is done through the Cell#fit method which stores the result in the
382
+ # cell itself. Furthermore, Cell#left and Cell#top are also assigned correctly.
383
+ def fit_rows(start_row, available_height, column_info, frame)
384
+ height = available_height
385
+ last_fitted_row_index = -1
386
+ @cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
387
+ row_fit = true
388
+ row_height = 0
389
+ columns.each_with_index do |cell, col_index|
390
+ next if cell.row != row_index || cell.column != col_index
391
+ available_cell_width = if cell.col_span > 1
392
+ column_info[cell.column, cell.col_span].map(&:last).sum
393
+ else
394
+ column_info[cell.column].last
395
+ end
396
+ unless cell.fit(available_cell_width, available_height, frame)
397
+ row_fit = false
398
+ break
399
+ end
400
+ cell.left = column_info[cell.column].first
401
+ cell.top = height - available_height
402
+ row_height = cell.preferred_height if row_height < cell.preferred_height
403
+ end
404
+
405
+ if row_fit
406
+ seen = {}
407
+ columns.each do |cell|
408
+ next if seen[cell]
409
+ cell.update_height(cell.row == row_index ? row_height : cell.height + row_height)
410
+ seen[cell] = true
411
+ end
412
+
413
+ last_fitted_row_index = row_index
414
+ available_height -= row_height
415
+ else
416
+ last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
417
+ break
418
+ end
419
+ end
420
+ [height - available_height, last_fitted_row_index]
421
+ end
422
+
423
+ # Draws the rows from +start_row+ to +end_row+ on the given +canvas+, with the top-left
424
+ # corner of the resulting table being at (+x+, +y+).
425
+ def draw_rows(start_row, end_row, canvas, x, y)
426
+ @cells[start_row..end_row].each.with_index(start_row) do |columns, row_index|
427
+ columns.each_with_index do |cell, col_index|
428
+ next if cell.row != row_index || cell.column != col_index
429
+ cell.draw(canvas, x + cell.left, y - cell.top - cell.height)
430
+ end
431
+ end
432
+ end
433
+
434
+ private
435
+
436
+ # Assigns the +data+ to the individual cells, taking row and column spans into account.
437
+ #
438
+ # For details on the +cell_style+ argument see ::new.
439
+ def assign_data(data, cell_style)
440
+ cell_style = data.shift unless data[0].kind_of?(Array)
441
+ cell_style_block = if cell_style.kind_of?(Hash)
442
+ lambda {|cell| cell.style.update(**cell_style) }
443
+ else
444
+ cell_style
445
+ end
446
+
447
+ data.each_with_index do |cols, row_index|
448
+ # Only add new row array if it hasn't been added due to row spans before
449
+ @cells << [] unless @cells[row_index]
450
+ row = @cells[row_index]
451
+ col_index = 0
452
+
453
+ cols.each do |content|
454
+ # Ignore already filled in cells due to row/col spans
455
+ col_index += 1 while row[col_index]
456
+
457
+ children = content
458
+ if content.kind_of?(Hash)
459
+ children = content.delete(:content)
460
+ row_span = content.delete(:row_span)
461
+ col_span = content.delete(:col_span)
462
+ properties = content.delete(:properties)
463
+ style = content
464
+ end
465
+ cell = Cell.new(children: children, row: row_index, column: col_index,
466
+ row_span: row_span, col_span: col_span)
467
+ cell_style_block&.call(cell)
468
+ cell.style.update(**style) if style
469
+ cell.properties.update(properties) if properties
470
+
471
+ row[col_index] = cell
472
+ if cell.row_span > 1 || cell.col_span > 1
473
+ row_index.upto(row_index + cell.row_span - 1) do |r|
474
+ @cells << [] unless @cells[r]
475
+ col_index.upto(col_index + cell.col_span - 1) do |c|
476
+ @cells[r][c] = cell
477
+ end
478
+ end
479
+ end
480
+
481
+ col_index += cell.col_span
482
+ end
483
+
484
+ @number_of_columns = col_index if @number_of_columns < col_index
485
+ end
486
+ end
487
+
488
+ end
489
+
490
+ # The Cells instance containing the data of the table.
491
+ #
492
+ # If this is an instance that was split from another one, the cells contain *all* the rows,
493
+ # not just the ones for this split instance.
494
+ #
495
+ # Also see #start_row_index.
496
+ attr_reader :cells
497
+
498
+ # The Cells instance containing the header cells of the table.
499
+ #
500
+ # If this is a TableBox instance that was split from another one, the header cells are created
501
+ # again through the use of +header+ block supplied to ::new.
502
+ attr_reader :header_cells
503
+
504
+ # The Cells instance containing the footer cells of the table.
505
+ #
506
+ # If this is a TableBox instance that was split from another one, the footer cells are created
507
+ # again through the use of +footer+ block supplied to ::new.
508
+ attr_reader :footer_cells
509
+
510
+ # The column widths definition.
511
+ #
512
+ # See ::new for details.
513
+ attr_reader :column_widths
514
+
515
+ # The row index into the #cells from which this instance starts fitting the rows.
516
+ #
517
+ # This value is 0 if this instance was not split from another one. Otherwise, it contains the
518
+ # correct start index.
519
+ attr_reader :start_row_index
520
+
521
+ # This value is -1 if #fit was not yet called. Otherwise it contains the row index of the last
522
+ # row that could be fitted.
523
+ attr_reader :last_fitted_row_index
524
+
525
+ # Creates a new TableBox instance.
526
+ #
527
+ # +cells+::
528
+ #
529
+ # This needs to be an array of arrays containing the data of the table. See Cells for more
530
+ # information on the allowed contents.
531
+ #
532
+ # Alternatively, a Cells instance can be used. Note that in this case the +cell_style+
533
+ # argument is not used.
534
+ #
535
+ # +column_widths+::
536
+ #
537
+ # An array defining the width of the columns of the table. If not set, defaults to an
538
+ # empty array.
539
+ #
540
+ # Each entry in the array may either be a positive or negative number. A positive number
541
+ # sets a fixed width for the respective column.
542
+ #
543
+ # A negative number specifies that the respective column is auto-sized. Such columns split
544
+ # the remaining width (after substracting the widths of the fixed columns) proportionally
545
+ # among them. For example, if the column width definition is [-1, -2, -2], the first
546
+ # column is a fifth of the width and the other two columns are each two fifth of the
547
+ # width.
548
+ #
549
+ # If the +cells+ definition has more columns than specified by +column_widths+, the
550
+ # missing entries are assumed to be -1.
551
+ #
552
+ # +header+::
553
+ #
554
+ # A callable object that needs to accept this TableBox instance as argument and that
555
+ # returns an array of arrays containing the header rows.
556
+ #
557
+ # The header rows are shown for the table instance and all split boxes.
558
+ #
559
+ # +footer+::
560
+ #
561
+ # A callable object that needs to accept this TableBox instance as argument and that
562
+ # returns an array of arrays containing the footer rows.
563
+ #
564
+ # The footer rows are shown for the table instance and all split boxes.
565
+ #
566
+ # +cell_style+::
567
+ #
568
+ # Contains styling information that should be applied to all header, body and footer
569
+ # cells.
570
+ #
571
+ # This can either be a hash containing style properties or a callable object accepting a
572
+ # cell as argument.
573
+ def initialize(cells:, column_widths: nil, header: nil, footer: nil, cell_style: nil, **kwargs)
574
+ super(**kwargs)
575
+ @cell_style = cell_style
576
+ @cells = cells.kind_of?(Cells) ? cells : Cells.new(cells, cell_style: @cell_style)
577
+ @column_widths = column_widths || []
578
+ @start_row_index = 0
579
+ @last_fitted_row_index = -1
580
+ @header = header
581
+ @header_cells = Cells.new(header.call(self), cell_style: @cell_style) if header
582
+ @footer = footer
583
+ @footer_cells = Cells.new(footer.call(self), cell_style: @cell_style) if footer
584
+ end
585
+
586
+ # Returns +true+ if not a single row could be fit.
587
+ def empty?
588
+ super && (!@last_fitted_row_index || @last_fitted_row_index < 0)
589
+ end
590
+
591
+ # Fits the table into the available space.
592
+ def fit(available_width, available_height, frame)
593
+ return false if (@initial_width > 0 && @initial_width > available_width) ||
594
+ (@initial_height > 0 && @initial_height > available_height)
595
+
596
+ # Adjust reserved width/height to include space used by the edge cells for their border
597
+ # since cell borders are drawn on the bounds and not inside.
598
+ # This uses the top-left and bottom-right cells and so might not be correct in all cases.
599
+ @cell_tl_border_width = @cells[0, 0].style.border.width
600
+ cell_br_border_width = @cells[-1, -1].style.border.width
601
+ rw = reserved_width + (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
602
+ rh = reserved_height + (@cell_tl_border_width.top + cell_br_border_width.bottom) / 2.0
603
+
604
+ width = (@initial_width > 0 ? @initial_width : available_width) - rw
605
+ height = (@initial_height > 0 ? @initial_height : available_height) - rh
606
+ used_height = 0
607
+ columns = calculate_column_widths(width)
608
+ return false if columns.empty?
609
+
610
+ @special_cells_fit_not_successful = false
611
+ [@header_cells, @footer_cells].each do |special_cells|
612
+ next unless special_cells
613
+ special_used_height, last_fitted_row_index = special_cells.fit_rows(0, height, columns, frame)
614
+ height -= special_used_height
615
+ used_height += special_used_height
616
+ @special_cells_fit_not_successful = (last_fitted_row_index != special_cells.number_of_rows - 1)
617
+ return false if @special_cells_fit_not_successful
618
+ end
619
+
620
+ main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns, frame)
621
+ used_height += main_used_height
622
+
623
+ @width = (@initial_width > 0 ? @initial_width : columns[-1].sum + rw)
624
+ @height = (@initial_height > 0 ? @initial_height : used_height + rh)
625
+ @fit_successful = (@last_fitted_row_index == @cells.number_of_rows - 1)
626
+ end
627
+
628
+ private
629
+
630
+ # Calculates and returns the x-coordinates and widths of all columns based on the given total
631
+ # available width.
632
+ #
633
+ # If it is not possible to fit all columns into the given +width+, an empty array is returned.
634
+ def calculate_column_widths(width)
635
+ @column_widths.concat([-1] * (@cells.number_of_columns - @column_widths.size))
636
+ fixed_width, variable_width = @column_widths.partition(&:positive?).map {|c| c.sum(&:abs) }
637
+ rest_width = width - fixed_width
638
+ return [] if rest_width <= 0
639
+
640
+ variable_width_unit = rest_width / variable_width.to_f
641
+ position = 0
642
+ @column_widths.map do |column|
643
+ result = column > 0 ? [position, column] : [position, column.abs * variable_width_unit]
644
+ position += result[1]
645
+ result
646
+ end
647
+ end
648
+
649
+ # Splits the content of the column box. This method is called from Box#split.
650
+ def split_content(_available_width, _available_height, _frame)
651
+ if @special_cells_fit_not_successful || @last_fitted_row_index < 0
652
+ [nil, self]
653
+ else
654
+ box = create_split_box
655
+ box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
656
+ box.instance_variable_set(:@last_fitted_row_index, -1)
657
+ box.instance_variable_set(:@special_cells_fit_not_successful, nil)
658
+ header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
659
+ box.instance_variable_set(:@header_cells, header_cells)
660
+ footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
661
+ box.instance_variable_set(:@footer_cells, footer_cells)
662
+ [self, box]
663
+ end
664
+ end
665
+
666
+ # Draws the child boxes onto the canvas at position [x, y].
667
+ def draw_content(canvas, x, y)
668
+ x += @cell_tl_border_width.left / 2.0
669
+ y += content_height - @cell_tl_border_width.top / 2.0
670
+ if @header_cells
671
+ @header_cells.draw_rows(0, -1, canvas, x, y)
672
+ y -= @header_cells[-1, 0].top + @header_cells[-1, 0].height
673
+ end
674
+ @cells.draw_rows(@start_row_index, @last_fitted_row_index, canvas, x, y)
675
+ if @footer_cells
676
+ y -= @cells[@last_fitted_row_index, 0].top + @cells[@last_fitted_row_index, 0].height
677
+ @footer_cells.draw_rows(0, -1, canvas, x, y)
678
+ end
679
+ end
680
+
681
+ end
682
+
683
+ end
684
+ end