hexapdf 0.32.1 → 0.33.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -1
- data/README.md +9 -0
- data/examples/002-graphics.rb +15 -17
- data/examples/003-arcs.rb +9 -9
- data/examples/009-text_layouter_alignment.rb +1 -1
- data/examples/010-text_layouter_inline_boxes.rb +2 -2
- data/examples/011-text_layouter_line_wrapping.rb +1 -1
- data/examples/012-text_layouter_styling.rb +7 -7
- data/examples/013-text_layouter_shapes.rb +1 -1
- data/examples/014-text_in_polygon.rb +1 -1
- data/examples/015-boxes.rb +8 -7
- data/examples/016-frame_automatic_box_placement.rb +2 -2
- data/examples/017-frame_text_flow.rb +2 -1
- data/examples/018-composer.rb +1 -1
- data/examples/020-column_box.rb +2 -1
- data/examples/025-table_box.rb +46 -0
- data/lib/hexapdf/cli/command.rb +5 -2
- data/lib/hexapdf/cli/form.rb +5 -5
- data/lib/hexapdf/cli/inspect.rb +3 -3
- data/lib/hexapdf/cli.rb +4 -0
- data/lib/hexapdf/composer.rb +104 -52
- data/lib/hexapdf/configuration.rb +44 -39
- data/lib/hexapdf/content/canvas.rb +393 -267
- data/lib/hexapdf/content/color_space.rb +72 -25
- data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
- data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
- data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
- data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
- data/lib/hexapdf/content/graphic_object.rb +6 -7
- data/lib/hexapdf/content/graphics_state.rb +54 -45
- data/lib/hexapdf/content/operator.rb +52 -54
- data/lib/hexapdf/content/parser.rb +2 -2
- data/lib/hexapdf/content/processor.rb +15 -15
- data/lib/hexapdf/content/transformation_matrix.rb +1 -1
- data/lib/hexapdf/content.rb +5 -0
- data/lib/hexapdf/dictionary.rb +6 -5
- data/lib/hexapdf/dictionary_fields.rb +42 -14
- data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
- data/lib/hexapdf/digital_signature/handler.rb +1 -1
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
- data/lib/hexapdf/digital_signature/signature.rb +6 -6
- data/lib/hexapdf/digital_signature/signatures.rb +13 -12
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
- data/lib/hexapdf/digital_signature/signing.rb +4 -0
- data/lib/hexapdf/digital_signature/verification_result.rb +2 -2
- data/lib/hexapdf/digital_signature.rb +7 -2
- data/lib/hexapdf/document/destinations.rb +12 -11
- data/lib/hexapdf/document/files.rb +1 -1
- data/lib/hexapdf/document/fonts.rb +1 -1
- data/lib/hexapdf/document/layout.rb +167 -39
- data/lib/hexapdf/document/pages.rb +3 -2
- data/lib/hexapdf/document.rb +89 -55
- data/lib/hexapdf/encryption/aes.rb +5 -5
- data/lib/hexapdf/encryption/arc4.rb +1 -1
- data/lib/hexapdf/encryption/fast_aes.rb +2 -2
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/identity.rb +1 -1
- data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
- data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +31 -24
- data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
- data/lib/hexapdf/encryption.rb +7 -2
- data/lib/hexapdf/error.rb +18 -0
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
- data/lib/hexapdf/filter/flate_decode.rb +1 -1
- data/lib/hexapdf/filter/lzw_decode.rb +1 -1
- data/lib/hexapdf/filter/pass_through.rb +1 -1
- data/lib/hexapdf/filter/predictor.rb +1 -1
- data/lib/hexapdf/filter/run_length_decode.rb +1 -1
- data/lib/hexapdf/filter.rb +55 -6
- data/lib/hexapdf/font/cmap/parser.rb +2 -2
- data/lib/hexapdf/font/cmap.rb +1 -1
- data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
- data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
- data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
- data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
- data/lib/hexapdf/font/invalid_glyph.rb +3 -0
- data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
- data/lib/hexapdf/font/type1_wrapper.rb +19 -4
- data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
- data/lib/hexapdf/font_loader/from_file.rb +5 -5
- data/lib/hexapdf/font_loader/standard14.rb +3 -3
- data/lib/hexapdf/font_loader.rb +3 -0
- data/lib/hexapdf/image_loader/jpeg.rb +2 -2
- data/lib/hexapdf/image_loader/pdf.rb +1 -1
- data/lib/hexapdf/image_loader/png.rb +2 -2
- data/lib/hexapdf/image_loader.rb +1 -1
- data/lib/hexapdf/importer.rb +13 -0
- data/lib/hexapdf/layout/box.rb +9 -2
- data/lib/hexapdf/layout/box_fitter.rb +2 -2
- data/lib/hexapdf/layout/column_box.rb +18 -4
- data/lib/hexapdf/layout/frame.rb +30 -12
- data/lib/hexapdf/layout/image_box.rb +5 -0
- data/lib/hexapdf/layout/inline_box.rb +1 -0
- data/lib/hexapdf/layout/list_box.rb +17 -1
- data/lib/hexapdf/layout/page_style.rb +4 -4
- data/lib/hexapdf/layout/style.rb +18 -3
- data/lib/hexapdf/layout/table_box.rb +682 -0
- data/lib/hexapdf/layout/text_box.rb +5 -3
- data/lib/hexapdf/layout/text_fragment.rb +1 -1
- data/lib/hexapdf/layout/text_layouter.rb +12 -4
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/name_tree_node.rb +1 -1
- data/lib/hexapdf/number_tree_node.rb +1 -1
- data/lib/hexapdf/object.rb +18 -7
- data/lib/hexapdf/parser.rb +8 -8
- data/lib/hexapdf/pdf_array.rb +1 -1
- data/lib/hexapdf/rectangle.rb +1 -1
- data/lib/hexapdf/reference.rb +1 -1
- data/lib/hexapdf/revision.rb +1 -1
- data/lib/hexapdf/revisions.rb +3 -3
- data/lib/hexapdf/serializer.rb +15 -15
- data/lib/hexapdf/stream.rb +4 -2
- data/lib/hexapdf/tokenizer.rb +14 -14
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
- data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
- data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
- data/lib/hexapdf/type/acro_form/field.rb +2 -2
- data/lib/hexapdf/type/acro_form/form.rb +1 -1
- data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
- data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
- data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
- data/lib/hexapdf/type/acro_form.rb +1 -1
- data/lib/hexapdf/type/action.rb +1 -1
- data/lib/hexapdf/type/actions/go_to.rb +1 -1
- data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
- data/lib/hexapdf/type/actions/launch.rb +1 -1
- data/lib/hexapdf/type/actions/uri.rb +1 -1
- data/lib/hexapdf/type/actions.rb +1 -1
- data/lib/hexapdf/type/annotation.rb +3 -3
- data/lib/hexapdf/type/annotations/link.rb +1 -1
- data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
- data/lib/hexapdf/type/annotations/text.rb +1 -1
- data/lib/hexapdf/type/annotations/widget.rb +2 -2
- data/lib/hexapdf/type/annotations.rb +1 -1
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/cid_font.rb +3 -3
- data/lib/hexapdf/type/embedded_file.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +2 -2
- data/lib/hexapdf/type/font_descriptor.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +2 -2
- data/lib/hexapdf/type/font_type0.rb +3 -3
- data/lib/hexapdf/type/font_type3.rb +1 -1
- data/lib/hexapdf/type/form.rb +1 -1
- data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
- data/lib/hexapdf/type/icon_fit.rb +1 -1
- data/lib/hexapdf/type/image.rb +1 -1
- data/lib/hexapdf/type/info.rb +1 -1
- data/lib/hexapdf/type/mark_information.rb +1 -1
- data/lib/hexapdf/type/names.rb +2 -2
- data/lib/hexapdf/type/object_stream.rb +7 -3
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/outline_item.rb +1 -1
- data/lib/hexapdf/type/page.rb +19 -10
- data/lib/hexapdf/type/page_label.rb +1 -1
- data/lib/hexapdf/type/page_tree_node.rb +1 -1
- data/lib/hexapdf/type/resources.rb +1 -1
- data/lib/hexapdf/type/trailer.rb +2 -2
- data/lib/hexapdf/type/viewer_preferences.rb +1 -1
- data/lib/hexapdf/type/xref_stream.rb +2 -2
- data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +4 -4
- data/lib/hexapdf/xref_section.rb +2 -2
- data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
- data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
- data/test/hexapdf/content/test_canvas.rb +0 -1
- data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
- data/test/hexapdf/document/test_files.rb +2 -2
- data/test/hexapdf/document/test_layout.rb +98 -0
- data/test/hexapdf/encryption/test_security_handler.rb +12 -11
- data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
- data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
- data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
- data/test/hexapdf/layout/test_box.rb +1 -1
- data/test/hexapdf/layout/test_column_box.rb +65 -21
- data/test/hexapdf/layout/test_frame.rb +14 -14
- data/test/hexapdf/layout/test_image_box.rb +4 -0
- data/test/hexapdf/layout/test_inline_box.rb +5 -0
- data/test/hexapdf/layout/test_list_box.rb +40 -6
- data/test/hexapdf/layout/test_page_style.rb +3 -2
- data/test/hexapdf/layout/test_style.rb +50 -0
- data/test/hexapdf/layout/test_table_box.rb +722 -0
- data/test/hexapdf/layout/test_text_box.rb +18 -0
- data/test/hexapdf/layout/test_text_layouter.rb +4 -0
- data/test/hexapdf/test_dictionary_fields.rb +4 -1
- data/test/hexapdf/test_document.rb +1 -0
- data/test/hexapdf/test_filter.rb +8 -0
- data/test/hexapdf/test_importer.rb +9 -0
- data/test/hexapdf/test_object.rb +16 -5
- data/test/hexapdf/test_parser.rb +1 -1
- data/test/hexapdf/test_stream.rb +7 -0
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
- data/test/hexapdf/type/acro_form/test_form.rb +4 -3
- data/test/hexapdf/type/test_object_stream.rb +9 -3
- data/test/hexapdf/type/test_page.rb +18 -4
- 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
|