hexapdf 0.23.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
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