hexapdf 0.32.2 → 0.34.0

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 (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