hexapdf 0.23.0 → 0.24.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 (243) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/LICENSE +1 -1
  4. data/Rakefile +1 -1
  5. data/examples/016-frame_automatic_box_placement.rb +7 -2
  6. data/examples/017-frame_text_flow.rb +10 -18
  7. data/examples/020-column_box.rb +40 -0
  8. data/examples/021-list_box.rb +26 -0
  9. data/lib/hexapdf/cli/batch.rb +1 -1
  10. data/lib/hexapdf/cli/command.rb +1 -1
  11. data/lib/hexapdf/cli/files.rb +1 -1
  12. data/lib/hexapdf/cli/fonts.rb +1 -1
  13. data/lib/hexapdf/cli/form.rb +2 -2
  14. data/lib/hexapdf/cli/image2pdf.rb +1 -1
  15. data/lib/hexapdf/cli/images.rb +1 -1
  16. data/lib/hexapdf/cli/info.rb +2 -2
  17. data/lib/hexapdf/cli/inspect.rb +2 -2
  18. data/lib/hexapdf/cli/merge.rb +1 -1
  19. data/lib/hexapdf/cli/modify.rb +1 -1
  20. data/lib/hexapdf/cli/optimize.rb +1 -1
  21. data/lib/hexapdf/cli/split.rb +1 -1
  22. data/lib/hexapdf/cli/watermark.rb +1 -1
  23. data/lib/hexapdf/cli.rb +1 -1
  24. data/lib/hexapdf/composer.rb +45 -126
  25. data/lib/hexapdf/configuration.rb +17 -1
  26. data/lib/hexapdf/content/canvas.rb +1 -1
  27. data/lib/hexapdf/content/color_space.rb +1 -1
  28. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +2 -1
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  32. data/lib/hexapdf/content/graphic_object.rb +1 -1
  33. data/lib/hexapdf/content/graphics_state.rb +1 -1
  34. data/lib/hexapdf/content/operator.rb +1 -1
  35. data/lib/hexapdf/content/parser.rb +1 -1
  36. data/lib/hexapdf/content/processor.rb +1 -1
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +1 -1
  39. data/lib/hexapdf/data_dir.rb +1 -1
  40. data/lib/hexapdf/dictionary.rb +1 -1
  41. data/lib/hexapdf/dictionary_fields.rb +1 -1
  42. data/lib/hexapdf/document/files.rb +1 -1
  43. data/lib/hexapdf/document/fonts.rb +1 -1
  44. data/lib/hexapdf/document/images.rb +1 -1
  45. data/lib/hexapdf/document/layout.rb +397 -0
  46. data/lib/hexapdf/document/pages.rb +17 -1
  47. data/lib/hexapdf/document/signatures.rb +5 -4
  48. data/lib/hexapdf/document.rb +8 -1
  49. data/lib/hexapdf/encryption/aes.rb +1 -1
  50. data/lib/hexapdf/encryption/arc4.rb +1 -1
  51. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  52. data/lib/hexapdf/encryption/fast_arc4.rb +30 -21
  53. data/lib/hexapdf/encryption/identity.rb +1 -1
  54. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  55. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  56. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  57. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  58. data/lib/hexapdf/encryption.rb +1 -1
  59. data/lib/hexapdf/error.rb +1 -1
  60. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  61. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  62. data/lib/hexapdf/filter/crypt.rb +1 -1
  63. data/lib/hexapdf/filter/encryption.rb +1 -1
  64. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  65. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  66. data/lib/hexapdf/filter/pass_through.rb +1 -1
  67. data/lib/hexapdf/filter/predictor.rb +1 -1
  68. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  69. data/lib/hexapdf/filter.rb +1 -1
  70. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  71. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  72. data/lib/hexapdf/font/cmap.rb +1 -1
  73. data/lib/hexapdf/font/encoding/base.rb +1 -1
  74. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  75. data/lib/hexapdf/font/encoding/glyph_list.rb +2 -2
  76. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  77. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  79. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  82. data/lib/hexapdf/font/encoding.rb +1 -1
  83. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  84. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  85. data/lib/hexapdf/font/true_type/font.rb +1 -1
  86. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  87. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  88. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  89. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  90. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  91. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  92. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  93. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  94. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  95. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  96. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  97. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  98. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  99. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  100. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  101. data/lib/hexapdf/font/true_type/table.rb +1 -1
  102. data/lib/hexapdf/font/true_type.rb +1 -1
  103. data/lib/hexapdf/font/true_type_wrapper.rb +1 -1
  104. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  105. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  106. data/lib/hexapdf/font/type1/font.rb +1 -1
  107. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  108. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  109. data/lib/hexapdf/font/type1.rb +1 -1
  110. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  111. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  112. data/lib/hexapdf/font_loader/from_file.rb +1 -1
  113. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  114. data/lib/hexapdf/font_loader.rb +1 -1
  115. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  116. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  117. data/lib/hexapdf/image_loader/png.rb +1 -1
  118. data/lib/hexapdf/image_loader.rb +1 -1
  119. data/lib/hexapdf/importer.rb +1 -1
  120. data/lib/hexapdf/layout/box.rb +121 -22
  121. data/lib/hexapdf/layout/box_fitter.rb +136 -0
  122. data/lib/hexapdf/layout/column_box.rb +247 -0
  123. data/lib/hexapdf/layout/frame.rb +155 -139
  124. data/lib/hexapdf/layout/image_box.rb +19 -4
  125. data/lib/hexapdf/layout/inline_box.rb +1 -1
  126. data/lib/hexapdf/layout/line.rb +1 -1
  127. data/lib/hexapdf/layout/list_box.rb +355 -0
  128. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  129. data/lib/hexapdf/layout/style.rb +5 -1
  130. data/lib/hexapdf/layout/text_box.rb +20 -9
  131. data/lib/hexapdf/layout/text_fragment.rb +3 -2
  132. data/lib/hexapdf/layout/text_layouter.rb +17 -2
  133. data/lib/hexapdf/layout/text_shaper.rb +1 -1
  134. data/lib/hexapdf/layout/width_from_polygon.rb +12 -7
  135. data/lib/hexapdf/layout.rb +4 -1
  136. data/lib/hexapdf/name_tree_node.rb +1 -1
  137. data/lib/hexapdf/number_tree_node.rb +1 -1
  138. data/lib/hexapdf/object.rb +1 -1
  139. data/lib/hexapdf/parser.rb +1 -8
  140. data/lib/hexapdf/pdf_array.rb +1 -1
  141. data/lib/hexapdf/rectangle.rb +1 -1
  142. data/lib/hexapdf/reference.rb +1 -1
  143. data/lib/hexapdf/revision.rb +1 -1
  144. data/lib/hexapdf/revisions.rb +1 -1
  145. data/lib/hexapdf/serializer.rb +1 -1
  146. data/lib/hexapdf/stream.rb +1 -1
  147. data/lib/hexapdf/task/dereference.rb +1 -1
  148. data/lib/hexapdf/task/optimize.rb +1 -1
  149. data/lib/hexapdf/task.rb +1 -1
  150. data/lib/hexapdf/tokenizer.rb +1 -1
  151. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  152. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  153. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  154. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  155. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  156. data/lib/hexapdf/type/acro_form/signature_field.rb +1 -1
  157. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  158. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  159. data/lib/hexapdf/type/acro_form.rb +1 -1
  160. data/lib/hexapdf/type/action.rb +1 -1
  161. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  162. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  163. data/lib/hexapdf/type/actions/launch.rb +1 -1
  164. data/lib/hexapdf/type/actions/uri.rb +1 -1
  165. data/lib/hexapdf/type/actions.rb +1 -1
  166. data/lib/hexapdf/type/annotation.rb +1 -1
  167. data/lib/hexapdf/type/annotations/link.rb +1 -1
  168. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  169. data/lib/hexapdf/type/annotations/text.rb +1 -1
  170. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  171. data/lib/hexapdf/type/annotations.rb +1 -1
  172. data/lib/hexapdf/type/catalog.rb +1 -1
  173. data/lib/hexapdf/type/cid_font.rb +1 -1
  174. data/lib/hexapdf/type/embedded_file.rb +1 -1
  175. data/lib/hexapdf/type/file_specification.rb +1 -1
  176. data/lib/hexapdf/type/font.rb +1 -1
  177. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  178. data/lib/hexapdf/type/font_simple.rb +1 -1
  179. data/lib/hexapdf/type/font_true_type.rb +1 -1
  180. data/lib/hexapdf/type/font_type0.rb +1 -1
  181. data/lib/hexapdf/type/font_type1.rb +1 -1
  182. data/lib/hexapdf/type/font_type3.rb +1 -1
  183. data/lib/hexapdf/type/form.rb +1 -1
  184. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  185. data/lib/hexapdf/type/icon_fit.rb +1 -1
  186. data/lib/hexapdf/type/image.rb +1 -1
  187. data/lib/hexapdf/type/info.rb +1 -1
  188. data/lib/hexapdf/type/names.rb +1 -1
  189. data/lib/hexapdf/type/object_stream.rb +1 -1
  190. data/lib/hexapdf/type/page.rb +1 -1
  191. data/lib/hexapdf/type/page_tree_node.rb +19 -2
  192. data/lib/hexapdf/type/resources.rb +1 -1
  193. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  194. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +1 -1
  195. data/lib/hexapdf/type/signature/handler.rb +1 -1
  196. data/lib/hexapdf/type/signature/verification_result.rb +1 -1
  197. data/lib/hexapdf/type/signature.rb +1 -1
  198. data/lib/hexapdf/type/trailer.rb +2 -2
  199. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  200. data/lib/hexapdf/type/xref_stream.rb +1 -1
  201. data/lib/hexapdf/type.rb +1 -1
  202. data/lib/hexapdf/utils/bit_field.rb +1 -1
  203. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  204. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  205. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  206. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  207. data/lib/hexapdf/utils/object_hash.rb +1 -1
  208. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  209. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  210. data/lib/hexapdf/version.rb +2 -2
  211. data/lib/hexapdf/writer.rb +9 -7
  212. data/lib/hexapdf/xref_section.rb +1 -1
  213. data/lib/hexapdf.rb +1 -1
  214. data/test/hexapdf/content/graphic_object/test_geom2d.rb +1 -1
  215. data/test/hexapdf/document/test_destinations.rb +1 -1
  216. data/test/hexapdf/document/test_images.rb +1 -1
  217. data/test/hexapdf/document/test_layout.rb +264 -0
  218. data/test/hexapdf/document/test_pages.rb +9 -0
  219. data/test/hexapdf/document/test_signatures.rb +10 -3
  220. data/test/hexapdf/encryption/test_security_handler.rb +1 -1
  221. data/test/hexapdf/font/encoding/test_glyph_list.rb +4 -0
  222. data/test/hexapdf/layout/test_box.rb +53 -3
  223. data/test/hexapdf/layout/test_box_fitter.rb +62 -0
  224. data/test/hexapdf/layout/test_column_box.rb +159 -0
  225. data/test/hexapdf/layout/test_frame.rb +99 -38
  226. data/test/hexapdf/layout/test_image_box.rb +1 -1
  227. data/test/hexapdf/layout/test_list_box.rb +249 -0
  228. data/test/hexapdf/layout/test_text_box.rb +17 -2
  229. data/test/hexapdf/layout/test_text_fragment.rb +1 -1
  230. data/test/hexapdf/layout/test_text_layouter.rb +42 -17
  231. data/test/hexapdf/layout/test_width_from_polygon.rb +13 -0
  232. data/test/hexapdf/test_composer.rb +11 -0
  233. data/test/hexapdf/test_dictionary_fields.rb +9 -9
  234. data/test/hexapdf/test_document.rb +4 -4
  235. data/test/hexapdf/test_filter.rb +1 -1
  236. data/test/hexapdf/test_parser.rb +0 -2
  237. data/test/hexapdf/test_revisions.rb +2 -2
  238. data/test/hexapdf/test_serializer.rb +1 -5
  239. data/test/hexapdf/test_writer.rb +58 -3
  240. data/test/hexapdf/type/test_page_tree_node.rb +21 -1
  241. data/test/hexapdf/type/test_trailer.rb +3 -3
  242. data/test/test_helper.rb +5 -1
  243. metadata +28 -3
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2021 Thomas Leitner
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -40,6 +40,8 @@ module HexaPDF
40
40
 
41
41
  # The base class for all layout boxes.
42
42
  #
43
+ # == Box Model
44
+ #
43
45
  # HexaPDF uses the following box model:
44
46
  #
45
47
  # * Each box can specify a width and height. Padding and border are inside, the margin outside
@@ -49,12 +51,35 @@ module HexaPDF
49
51
  # the content box without padding and the border.
50
52
  #
51
53
  # * If width or height is set to zero, they are determined automatically during layouting.
54
+ #
55
+ #
56
+ # == Subclasses
57
+ #
58
+ # Each subclass should only take keyword arguments on initialization so that the boxes can be
59
+ # instantiated from the common convenience method HexaPDF::Document::Layout#box. To use this
60
+ # facility subclasses need to be registered with the configuration option 'layout.boxes.map'.
61
+ #
62
+ # The methods #fit, #split or #split_content, and #draw or #draw_content need to be customized
63
+ # according to the subclass's use case.
64
+ #
65
+ # #fit:: This method should return +true+ if fitting was successful. Additionally, the
66
+ # @fit_successful instance variable needs to be set to the fit result as it is used in
67
+ # #split.
68
+ #
69
+ # #split:: This method splits the content so that the available space is used as good as
70
+ # possible. The default implementation should be fine for most use-cases, so only
71
+ # #split_content needs to be implemented. The method #create_split_box should be used
72
+ # for getting a basic cloned box.
73
+ #
74
+ # #draw:: This method draws the content and the default implementation already handles things
75
+ # like drawing the border and background. Therefore it's best to implement #draw_content
76
+ # which should just draw the content.
52
77
  class Box
53
78
 
54
79
  # Creates a new Box object, using the provided block as drawing block (see ::new).
55
80
  #
56
81
  # If +content_box+ is +true+, the width and height are taken to mean the content width and
57
- # height and the style's padding and border are removed from them appropriately.
82
+ # height and the style's padding and border are added to them appropriately.
58
83
  #
59
84
  # The +style+ argument defines the Style object (see Style::create for details) for the box.
60
85
  # Any additional keyword arguments have to be style properties and are applied to the style
@@ -102,6 +127,18 @@ module HexaPDF
102
127
  @height = @initial_height = height
103
128
  @style = Style.create(style)
104
129
  @draw_block = block
130
+ @fit_successful = false
131
+ @split_box = false
132
+ end
133
+
134
+ # Returns +true+ if this is a split box, i.e. the rest of another box after it was split.
135
+ def split_box?
136
+ @split_box
137
+ end
138
+
139
+ # Returns +false+ since a basic box doesn't support the 'position' style property value :flow.
140
+ def supports_position_flow?
141
+ false
105
142
  end
106
143
 
107
144
  # The width of the content box, i.e. without padding and/or borders.
@@ -123,16 +160,16 @@ module HexaPDF
123
160
  def fit(available_width, available_height, _frame)
124
161
  @width = (@initial_width > 0 ? @initial_width : available_width)
125
162
  @height = (@initial_height > 0 ? @initial_height : available_height)
126
- @width <= available_width && @height <= available_height
163
+ @fit_successful = (@width <= available_width && @height <= available_height)
127
164
  end
128
165
 
129
166
  # Tries to split the box into two, the first of which needs to fit into the available space,
130
167
  # and returns the parts as array.
131
168
  #
132
- # In many cases the first box in the list will be this box, meaning that even when #fit fails,
133
- # a part of the box may still fit. Note that #fit may not be called if the first box is this
134
- # box since it is assumed that it is already fitted. If not even a part of this box fits into
135
- # the available space, +nil+ should be returned as the first array element.
169
+ # If the first item in the result array is not +nil+, it needs to be this box and it means
170
+ # that even when #fit fails, a part of the box may still fit. Note that #fit should not be
171
+ # called before #draw on the first box since it is already fitted. If not even a part of this
172
+ # box fits into the available space, +nil+ should be returned as the first array element.
136
173
  #
137
174
  # Possible return values:
138
175
  #
@@ -140,15 +177,24 @@ module HexaPDF
140
177
  # [nil, self]:: The box can't be split or no part of the box fits into the available space.
141
178
  # [self, new_box]:: A part of the box fits and a new box is returned for the rest.
142
179
  #
143
- # This default implementation provides no splitting functionality.
144
- def split(_available_width, _available_height, _frame)
145
- [nil, self]
180
+ # This default implementation provides the basic functionality based on the #fit result that
181
+ # should be sufficient for most subclasses; only #split_content needs to be implemented if
182
+ # necessary.
183
+ def split(available_width, available_height, frame)
184
+ if @fit_successful
185
+ [self, nil]
186
+ elsif (style.position != :flow && (@width > available_width || @height > available_height)) ||
187
+ content_height == 0 || content_width == 0
188
+ [nil, self]
189
+ else
190
+ split_content(available_width, available_height, frame)
191
+ end
146
192
  end
147
193
 
148
194
  # Draws the content of the box onto the canvas at the position (x, y).
149
195
  #
150
196
  # The coordinate system is translated so that the origin is at the bottom left corner of the
151
- # **content box** during the drawing operations.
197
+ # **content box** during the drawing operations when +@draw_block+ is used.
152
198
  #
153
199
  # The block specified when creating the box is invoked with the canvas and the box as
154
200
  # arguments. Subclasses can specify an on-demand drawing method by setting the +@draw_block+
@@ -165,11 +211,7 @@ module HexaPDF
165
211
  style.underlays.draw(canvas, x, y, self) if style.underlays?
166
212
  style.border.draw(canvas, x, y, width, height) if style.border?
167
213
 
168
- cx = x
169
- cy = y
170
- (cx += style.padding.left; cy += style.padding.bottom) if style.padding?
171
- (cx += style.border.width.left; cy += style.border.width.bottom) if style.border?
172
- draw_content(canvas, cx, cy)
214
+ draw_content(canvas, x + reserved_width_left, y + reserved_height_bottom)
173
215
 
174
216
  style.overlays.draw(canvas, x, y, self) if style.overlays?
175
217
  end
@@ -187,17 +229,47 @@ module HexaPDF
187
229
 
188
230
  # Returns the width that is reserved by the padding and border style properties.
189
231
  def reserved_width
190
- result = 0
191
- result += style.padding.left + style.padding.right if style.padding?
192
- result += style.border.width.left + style.border.width.right if style.border?
193
- result
232
+ reserved_width_left + reserved_width_right
194
233
  end
195
234
 
196
235
  # Returns the height that is reserved by the padding and border style properties.
197
236
  def reserved_height
237
+ reserved_height_top + reserved_height_bottom
238
+ end
239
+
240
+ # Returns the width that is reserved by the padding and the border style properties on the
241
+ # left side of the box.
242
+ def reserved_width_left
198
243
  result = 0
199
- result += style.padding.top + style.padding.bottom if style.padding?
200
- result += style.border.width.top + style.border.width.bottom if style.border?
244
+ result += style.padding.left if style.padding?
245
+ result += style.border.width.left if style.border?
246
+ result
247
+ end
248
+
249
+ # Returns the width that is reserved by the padding and the border style properties on the
250
+ # right side of the box.
251
+ def reserved_width_right
252
+ result = 0
253
+ result += style.padding.right if style.padding?
254
+ result += style.border.width.right if style.border?
255
+ result
256
+ end
257
+
258
+ # Returns the height that is reserved by the padding and the border style properties on the
259
+ # top side of the box.
260
+ def reserved_height_top
261
+ result = 0
262
+ result += style.padding.top if style.padding?
263
+ result += style.border.width.top if style.border?
264
+ result
265
+ end
266
+
267
+ # Returns the height that is reserved by the padding and the border style properties on the
268
+ # bottom side of the box.
269
+ def reserved_height_bottom
270
+ result = 0
271
+ result += style.padding.bottom if style.padding?
272
+ result += style.border.width.bottom if style.border?
201
273
  result
202
274
  end
203
275
 
@@ -209,6 +281,33 @@ module HexaPDF
209
281
  end
210
282
  end
211
283
 
284
+ # Splits the content of the box.
285
+ #
286
+ # This is just a stub implementation, returning [nil, self] since we can't know how to split
287
+ # the content when it didn't fit.
288
+ #
289
+ # Subclasses that support splitting content need to provide an appropriate implementation and
290
+ # use #create_split_box to create a cloned box to supply as the second argument.
291
+ def split_content(_available_width, _available_height, _frame)
292
+ [nil, self]
293
+ end
294
+
295
+ # Creates a new box based on this one and resets the data back to their original values.
296
+ #
297
+ # The variable +@split_box+ is set to +split_box_value+ (defaults to +true+) to make the new
298
+ # box aware that it is a split box. If needed, subclasses can set the variable to other truthy
299
+ # values to convey more meaning.
300
+ #
301
+ # This method should be used by subclasses to create their split box.
302
+ def create_split_box(split_box_value: true)
303
+ box = clone
304
+ box.instance_variable_set(:@width, @initial_width)
305
+ box.instance_variable_set(:@height, @initial_height)
306
+ box.instance_variable_set(:@fit_successful, nil)
307
+ box.instance_variable_set(:@split_box, split_box_value)
308
+ box
309
+ end
310
+
212
311
  end
213
312
 
214
313
  end
@@ -0,0 +1,136 @@
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-2022 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
+ module HexaPDF
38
+ module Layout
39
+
40
+ # A BoxFitter instance contains an array of Frame objects and allows placing boxes one after the
41
+ # other in them. Such functionality is useful, for example, for boxes that provide multiple
42
+ # frames for content.
43
+ #
44
+ # == Usage
45
+ #
46
+ # * First one needs to add the frame objects via #<< or provide them on initialization.
47
+ #
48
+ # * Then use the #fit method to fit boxes one after the other. No drawing is done.
49
+ #
50
+ # * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #fit_successful?
51
+ # methods can be used to get the result:
52
+ #
53
+ # - If there are no remaining boxes, all boxes were successfully fitted into the frames.
54
+ # - If there are remaining boxes but no fit results, the first box could not be fitted.
55
+ # - If there are remaining boxes and fit results, some boxes were able to fit.
56
+ class BoxFitter
57
+
58
+ # The array of frames inside of which the boxes should be laid out.
59
+ #
60
+ # Use #<< to add additional frames.
61
+ attr_reader :frames
62
+
63
+ # The Frame::FitResult objects for the successfully fitted objects in the order the boxes were
64
+ # fitted.
65
+ attr_reader :fit_results
66
+
67
+ # The boxes that could not be fitted into the frames.
68
+ attr_reader :remaining_boxes
69
+
70
+ # Creates a new BoxFitter object for the given +frames+.
71
+ def initialize(frames = [])
72
+ @frames = []
73
+ @content_heights = []
74
+ @initial_frame_y = []
75
+ @frame_index = 0
76
+ @fit_results = []
77
+ @remaining_boxes = []
78
+
79
+ frames.each {|frame| self << frame }
80
+ end
81
+
82
+ # Add the given frame to the list of frames.
83
+ def <<(frame)
84
+ @frames << frame
85
+ @initial_frame_y << frame.y
86
+ @content_heights << 0
87
+ end
88
+
89
+ # Fits the given box at the current location.
90
+ def fit(box)
91
+ unless @remaining_boxes.empty?
92
+ @remaining_boxes << box
93
+ return
94
+ end
95
+
96
+ while (current_frame = @frames[@frame_index])
97
+ result = current_frame.fit(box)
98
+ if result.success?
99
+ current_frame.remove_area(result.mask)
100
+ @content_heights[@frame_index] = [@content_heights[@frame_index],
101
+ @initial_frame_y[@frame_index] - result.mask[0].y].max
102
+ @fit_results << result
103
+ box = nil
104
+ break
105
+ elsif current_frame.full?
106
+ @frame_index += 1
107
+ else
108
+ draw_box, box = current_frame.split(result)
109
+ if draw_box
110
+ current_frame.remove_area(result.mask)
111
+ @content_heights[@frame_index] = [@content_heights[@frame_index],
112
+ @initial_frame_y[@frame_index] - result.mask[0].y].max
113
+ @fit_results << result
114
+ elsif !current_frame.find_next_region
115
+ @frame_index += 1
116
+ end
117
+ end
118
+ end
119
+
120
+ @remaining_boxes << box if box
121
+ end
122
+
123
+ # Returns an array with the heights of the content of each frame.
124
+ def content_heights
125
+ @content_heights
126
+ end
127
+
128
+ # Returns +true+ if all boxes were successfully fitted.
129
+ def fit_successful?
130
+ @remaining_boxes.empty?
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,247 @@
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-2022 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
+ require 'hexapdf/layout/box'
37
+ require 'hexapdf/layout/box_fitter'
38
+
39
+ module HexaPDF
40
+ module Layout
41
+
42
+ # A ColumnBox arranges boxes in one or more columns.
43
+ #
44
+ # The number and width of the columns as well as the size of the gap between the columns can be
45
+ # modified. Additionally, the contents can either fill the columns one after the other or the
46
+ # columns can be made equally high.
47
+ #
48
+ # If the column box has padding and/or borders specified, they are handled like with any other
49
+ # box. This means they are around all columns and their contents and are not used separately for
50
+ # each column.
51
+ #
52
+ # The following style properties are used (additionally to those used by the parent class):
53
+ #
54
+ # Style#position::
55
+ # If this is set to :flow, the frames created for the columns will take the shape of the
56
+ # frame into account. This also means that the +available_width+ and +available_height+
57
+ # arguments are ignored.
58
+ class ColumnBox < Box
59
+
60
+ # The child boxes of this ColumnBox. They need to be finalized before #fit is called.
61
+ attr_reader :children
62
+
63
+ # The columns definition.
64
+ #
65
+ # This is an array containing the widths of the columns. The size of the array is the number
66
+ # of columns.
67
+ #
68
+ # If a negative integer is used for the width, the column is auto-sized. Such columns split
69
+ # the remaining width (after substracting the widths of the fixed columns) proportionally
70
+ # among them. For example, if the definition is [-1, -2, -2], the first column is a fifth of
71
+ # the width and the other columns are each two fifth of the width.
72
+ #
73
+ # Examples:
74
+ #
75
+ # #>pdf-composer
76
+ # composer.box(:column, columns: 2, gaps: 10,
77
+ # children: [composer.document.layout.lorem_ipsum_box])
78
+ #
79
+ # ---
80
+ #
81
+ # #>pdf-composer
82
+ # composer.box(:column, columns: [50, -2, -1], gaps: [10, 5],
83
+ # children: [composer.document.layout.lorem_ipsum_box])
84
+ attr_reader :columns
85
+
86
+ # The size of the gaps between the columns.
87
+ #
88
+ # This is an array containing the width of the gaps. If there are more gaps than numbers in
89
+ # the array, the array is cycled.
90
+ #
91
+ # Examples: see #columns
92
+ attr_reader :gaps
93
+
94
+ # Determines whether the columns should all be equally high or not.
95
+ #
96
+ # Examples:
97
+ #
98
+ # #>pdf-composer
99
+ # composer.box(:column, children: [composer.document.layout.lorem_ipsum_box])
100
+ #
101
+ # ---
102
+ #
103
+ # #>pdf-composer
104
+ # composer.box(:column, equal_height: false,
105
+ # children: [composer.document.layout.lorem_ipsum_box])
106
+ attr_reader :equal_height
107
+
108
+ # Creates a new ColumnBox object for the given child boxes in +children+.
109
+ #
110
+ # +columns+::
111
+ #
112
+ # Can either simply integer specify the number of columns or be a full column definition
113
+ # (see #columns for details).
114
+ #
115
+ # +gaps+::
116
+ # Can either be a simply integer specifying the width between two columns or a full gap
117
+ # definition (see #gap for details).
118
+ #
119
+ # +equal_height+::
120
+ # If +true+, the #fit method tries to balance the columns in terms of their height.
121
+ # Otherwise the columns are filled from the left.
122
+ def initialize(children: [], columns: 2, gaps: 36, equal_height: true, **kwargs)
123
+ super(**kwargs)
124
+ @children = children
125
+ @columns = (columns.kind_of?(Array) ? columns : [-1] * columns)
126
+ @gaps = (gaps.kind_of?(Array) ? gaps : [gaps])
127
+ @equal_height = equal_height
128
+ end
129
+
130
+ # Returns +true+ as the 'position' style property value :flow is supported.
131
+ def supports_position_flow?
132
+ true
133
+ end
134
+
135
+ # Fits the column box into the available space.
136
+ #
137
+ # If the style property 'position' is set to :flow, the columns might not be rectangles but
138
+ # arbitrary (sets of) polygons since the +frame+s shape is taken into account.
139
+ def fit(available_width, available_height, frame)
140
+ initial_fit_successful = (@equal_height ? nil : false)
141
+ tries = 0
142
+ @width = if style.position == :flow
143
+ (@initial_width > 0 ? @initial_width : frame.width) - reserved_width
144
+ else
145
+ (@initial_width > 0 ? @initial_width : available_width) - reserved_width
146
+ end
147
+ height = if style.position == :flow
148
+ (@initial_height > 0 ? @initial_height : frame.height) - reserved_height
149
+ else
150
+ (@initial_height > 0 ? @initial_height : available_height) - reserved_height
151
+ end
152
+
153
+ columns = calculate_columns(@width)
154
+ return false if columns.empty?
155
+
156
+ left = (style.position == :flow ? frame.left : frame.x) + reserved_width_left
157
+ top = (style.position == :flow ? frame.bottom + frame.height : frame.y) - reserved_height_top
158
+ successful_height = height
159
+ unsuccessful_height = 0
160
+
161
+ while true
162
+ @box_fitter = BoxFitter.new
163
+
164
+ columns.each do |col_x, column_width|
165
+ column_left = left + col_x
166
+ column_bottom = top - height
167
+ if style.position == :flow
168
+ rect = Geom2D::Polygon([column_left, column_bottom],
169
+ [column_left + column_width, column_bottom],
170
+ [column_left + column_width, column_bottom + height],
171
+ [column_left, column_bottom + height])
172
+ shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
173
+ end
174
+ column_frame = Frame.new(column_left, column_bottom, column_width, height, shape: shape)
175
+ @box_fitter << column_frame
176
+ end
177
+
178
+ children.each {|box| @box_fitter.fit(box) }
179
+
180
+ fit_successful = @box_fitter.fit_successful?
181
+ initial_fit_successful = fit_successful if initial_fit_successful.nil?
182
+
183
+ if fit_successful
184
+ successful_height = height if successful_height > height
185
+ elsif unsuccessful_height < height
186
+ unsuccessful_height = height
187
+ end
188
+
189
+ break if !initial_fit_successful || tries > 40 ||
190
+ (fit_successful && successful_height - unsuccessful_height < 10)
191
+
192
+ height = if successful_height - unsuccessful_height <= 5
193
+ successful_height
194
+ else
195
+ (successful_height + unsuccessful_height) / 2.0
196
+ end
197
+ tries += 1
198
+ end
199
+
200
+ @width = columns[-1].sum + reserved_width
201
+ @height = @box_fitter.content_heights.max + reserved_height
202
+
203
+ @box_fitter.fit_successful?
204
+ end
205
+
206
+ private
207
+
208
+ # Calculates the x-coordinates and widths of all columns based on the given total available
209
+ # width.
210
+ #
211
+ # If it is not possible to fit all columns into the given +width+, an empty array is returned.
212
+ def calculate_columns(width)
213
+ number_of_columns = @columns.size
214
+ gaps = @gaps.cycle.take(number_of_columns - 1)
215
+ fixed_width, variable_width = @columns.partition(&:positive?).map {|c| c.sum(&:abs) }
216
+ rest_width = width - fixed_width - gaps.sum
217
+ return [] if rest_width <= 0
218
+
219
+ variable_width_unit = rest_width / variable_width.to_f
220
+ position = 0
221
+ @columns.map.with_index do |column, index|
222
+ result = if column > 0
223
+ [position, column]
224
+ else
225
+ [position, column.abs * variable_width_unit]
226
+ end
227
+ position += result[1] + (gaps[index] || 0)
228
+ result
229
+ end
230
+ end
231
+
232
+ # Splits the content of the column box. This method is called from Box#split.
233
+ def split_content(_available_width, _available_height, _frame)
234
+ box = create_split_box
235
+ box.instance_variable_set(:@children, @box_fitter.remaining_boxes)
236
+ [self, box]
237
+ end
238
+
239
+ # Draws the child boxes onto the canvas at position [x, y].
240
+ def draw_content(canvas, _x, _y)
241
+ @box_fitter.fit_results.each {|result| result.draw(canvas) }
242
+ end
243
+
244
+ end
245
+
246
+ end
247
+ end