hexapdf 0.32.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +9 -0
  4. data/examples/002-graphics.rb +15 -17
  5. data/examples/003-arcs.rb +9 -9
  6. data/examples/009-text_layouter_alignment.rb +1 -1
  7. data/examples/010-text_layouter_inline_boxes.rb +2 -2
  8. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  9. data/examples/012-text_layouter_styling.rb +7 -7
  10. data/examples/013-text_layouter_shapes.rb +1 -1
  11. data/examples/014-text_in_polygon.rb +1 -1
  12. data/examples/015-boxes.rb +8 -7
  13. data/examples/016-frame_automatic_box_placement.rb +2 -2
  14. data/examples/017-frame_text_flow.rb +2 -1
  15. data/examples/018-composer.rb +1 -1
  16. data/examples/020-column_box.rb +2 -1
  17. data/examples/025-table_box.rb +46 -0
  18. data/examples/026-optional_content.rb +55 -0
  19. data/examples/027-composer_optional_content.rb +83 -0
  20. data/lib/hexapdf/cli/command.rb +12 -3
  21. data/lib/hexapdf/cli/fonts.rb +1 -1
  22. data/lib/hexapdf/cli/form.rb +5 -5
  23. data/lib/hexapdf/cli/inspect.rb +5 -7
  24. data/lib/hexapdf/composer.rb +106 -53
  25. data/lib/hexapdf/configuration.rb +65 -40
  26. data/lib/hexapdf/content/canvas.rb +445 -267
  27. data/lib/hexapdf/content/color_space.rb +72 -25
  28. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  32. data/lib/hexapdf/content/graphic_object.rb +6 -7
  33. data/lib/hexapdf/content/graphics_state.rb +54 -45
  34. data/lib/hexapdf/content/operator.rb +54 -54
  35. data/lib/hexapdf/content/parser.rb +2 -2
  36. data/lib/hexapdf/content/processor.rb +15 -15
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +5 -0
  39. data/lib/hexapdf/dictionary.rb +7 -5
  40. data/lib/hexapdf/dictionary_fields.rb +43 -16
  41. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  42. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  43. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  44. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  45. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  46. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  47. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  48. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  49. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  50. data/lib/hexapdf/digital_signature/verification_result.rb +3 -4
  51. data/lib/hexapdf/digital_signature.rb +7 -2
  52. data/lib/hexapdf/document/destinations.rb +12 -11
  53. data/lib/hexapdf/document/files.rb +1 -1
  54. data/lib/hexapdf/document/fonts.rb +1 -1
  55. data/lib/hexapdf/document/layout.rb +170 -39
  56. data/lib/hexapdf/document/pages.rb +4 -3
  57. data/lib/hexapdf/document.rb +96 -55
  58. data/lib/hexapdf/encryption/aes.rb +5 -5
  59. data/lib/hexapdf/encryption/arc4.rb +1 -1
  60. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  61. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/identity.rb +1 -1
  63. data/lib/hexapdf/encryption/ruby_aes.rb +11 -21
  64. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  65. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  66. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  67. data/lib/hexapdf/encryption.rb +7 -2
  68. data/lib/hexapdf/error.rb +18 -0
  69. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  70. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  71. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  72. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  73. data/lib/hexapdf/filter/pass_through.rb +1 -1
  74. data/lib/hexapdf/filter/predictor.rb +1 -1
  75. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  76. data/lib/hexapdf/filter.rb +55 -6
  77. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  78. data/lib/hexapdf/font/cmap.rb +1 -1
  79. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  82. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  83. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  84. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  85. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  86. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  87. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  88. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  89. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  90. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  91. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  92. data/lib/hexapdf/font_loader.rb +3 -0
  93. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  94. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  95. data/lib/hexapdf/image_loader/png.rb +2 -2
  96. data/lib/hexapdf/image_loader.rb +1 -1
  97. data/lib/hexapdf/importer.rb +13 -0
  98. data/lib/hexapdf/layout/box.rb +32 -5
  99. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  100. data/lib/hexapdf/layout/column_box.rb +20 -5
  101. data/lib/hexapdf/layout/frame.rb +53 -18
  102. data/lib/hexapdf/layout/image_box.rb +5 -0
  103. data/lib/hexapdf/layout/inline_box.rb +21 -9
  104. data/lib/hexapdf/layout/list_box.rb +50 -20
  105. data/lib/hexapdf/layout/page_style.rb +6 -5
  106. data/lib/hexapdf/layout/style.rb +64 -9
  107. data/lib/hexapdf/layout/table_box.rb +684 -0
  108. data/lib/hexapdf/layout/text_box.rb +12 -3
  109. data/lib/hexapdf/layout/text_fragment.rb +29 -3
  110. data/lib/hexapdf/layout/text_layouter.rb +32 -8
  111. data/lib/hexapdf/layout.rb +1 -0
  112. data/lib/hexapdf/name_tree_node.rb +1 -1
  113. data/lib/hexapdf/number_tree_node.rb +1 -1
  114. data/lib/hexapdf/object.rb +18 -7
  115. data/lib/hexapdf/parser.rb +7 -7
  116. data/lib/hexapdf/pdf_array.rb +1 -1
  117. data/lib/hexapdf/rectangle.rb +1 -1
  118. data/lib/hexapdf/reference.rb +1 -1
  119. data/lib/hexapdf/revision.rb +1 -1
  120. data/lib/hexapdf/revisions.rb +3 -3
  121. data/lib/hexapdf/serializer.rb +15 -15
  122. data/lib/hexapdf/stream.rb +5 -4
  123. data/lib/hexapdf/tokenizer.rb +14 -14
  124. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  125. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  127. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  128. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  129. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  130. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  131. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  132. data/lib/hexapdf/type/acro_form.rb +1 -1
  133. data/lib/hexapdf/type/action.rb +1 -1
  134. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  135. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  136. data/lib/hexapdf/type/actions/launch.rb +1 -1
  137. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  138. data/lib/hexapdf/type/actions/uri.rb +1 -1
  139. data/lib/hexapdf/type/actions.rb +2 -1
  140. data/lib/hexapdf/type/annotation.rb +3 -3
  141. data/lib/hexapdf/type/annotations/link.rb +1 -1
  142. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  143. data/lib/hexapdf/type/annotations/text.rb +2 -3
  144. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  145. data/lib/hexapdf/type/annotations.rb +1 -1
  146. data/lib/hexapdf/type/catalog.rb +11 -2
  147. data/lib/hexapdf/type/cid_font.rb +18 -4
  148. data/lib/hexapdf/type/embedded_file.rb +1 -1
  149. data/lib/hexapdf/type/file_specification.rb +2 -2
  150. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  151. data/lib/hexapdf/type/font_simple.rb +2 -2
  152. data/lib/hexapdf/type/font_type0.rb +3 -3
  153. data/lib/hexapdf/type/font_type3.rb +1 -1
  154. data/lib/hexapdf/type/form.rb +76 -6
  155. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  156. data/lib/hexapdf/type/icon_fit.rb +1 -1
  157. data/lib/hexapdf/type/image.rb +1 -1
  158. data/lib/hexapdf/type/info.rb +1 -1
  159. data/lib/hexapdf/type/mark_information.rb +1 -1
  160. data/lib/hexapdf/type/names.rb +2 -2
  161. data/lib/hexapdf/type/object_stream.rb +2 -1
  162. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  163. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  164. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  165. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  166. data/lib/hexapdf/type/outline.rb +1 -1
  167. data/lib/hexapdf/type/outline_item.rb +1 -1
  168. data/lib/hexapdf/type/page.rb +46 -21
  169. data/lib/hexapdf/type/page_label.rb +5 -9
  170. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  171. data/lib/hexapdf/type/resources.rb +1 -1
  172. data/lib/hexapdf/type/trailer.rb +2 -2
  173. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  174. data/lib/hexapdf/type/xref_stream.rb +2 -2
  175. data/lib/hexapdf/type.rb +4 -0
  176. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -2
  177. data/lib/hexapdf/version.rb +1 -1
  178. data/lib/hexapdf/writer.rb +4 -4
  179. data/lib/hexapdf/xref_section.rb +2 -2
  180. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  181. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  182. data/test/hexapdf/content/test_canvas.rb +49 -1
  183. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  184. data/test/hexapdf/document/test_files.rb +2 -2
  185. data/test/hexapdf/document/test_layout.rb +105 -2
  186. data/test/hexapdf/document/test_pages.rb +6 -6
  187. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  188. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  189. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  190. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  191. data/test/hexapdf/layout/test_box.rb +14 -5
  192. data/test/hexapdf/layout/test_column_box.rb +65 -21
  193. data/test/hexapdf/layout/test_frame.rb +27 -15
  194. data/test/hexapdf/layout/test_image_box.rb +4 -0
  195. data/test/hexapdf/layout/test_inline_box.rb +17 -3
  196. data/test/hexapdf/layout/test_list_box.rb +84 -33
  197. data/test/hexapdf/layout/test_page_style.rb +3 -2
  198. data/test/hexapdf/layout/test_style.rb +60 -0
  199. data/test/hexapdf/layout/test_table_box.rb +728 -0
  200. data/test/hexapdf/layout/test_text_box.rb +26 -0
  201. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  202. data/test/hexapdf/layout/test_text_layouter.rb +36 -5
  203. data/test/hexapdf/test_composer.rb +10 -0
  204. data/test/hexapdf/test_dictionary.rb +10 -0
  205. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  206. data/test/hexapdf/test_document.rb +5 -0
  207. data/test/hexapdf/test_filter.rb +8 -0
  208. data/test/hexapdf/test_importer.rb +9 -0
  209. data/test/hexapdf/test_object.rb +16 -5
  210. data/test/hexapdf/test_stream.rb +7 -0
  211. data/test/hexapdf/test_writer.rb +3 -3
  212. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  213. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  214. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  215. data/test/hexapdf/type/test_catalog.rb +11 -0
  216. data/test/hexapdf/type/test_form.rb +119 -0
  217. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  218. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  219. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  220. data/test/hexapdf/type/test_page.rb +20 -6
  221. metadata +28 -8
@@ -106,8 +106,8 @@ module HexaPDF
106
106
  # The available height in the frame for this particular box.
107
107
  attr_accessor :available_height
108
108
 
109
- # The rectangle (a Geom2D::Polygon object) that will be removed from the frame when drawing
110
- # the box.
109
+ # The rectangle (a Geom2D::Rectangle object) that will be removed from the frame when
110
+ # drawing the box.
111
111
  attr_accessor :mask
112
112
 
113
113
  # Initialize the result object for the given box.
@@ -133,12 +133,18 @@ module HexaPDF
133
133
  # The configuration option "debug" can be used to add visual debug output with respect to
134
134
  # box placement.
135
135
  def draw(canvas)
136
- if canvas.context.document.config['debug']
137
- canvas.save_graphics_state do
138
- canvas.fill_color("green").stroke_color("darkgreen").
139
- opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
140
- draw(:geom2d, object: mask, path_only: true).fill_stroke
136
+ doc = canvas.context.document
137
+ if doc.config['debug']
138
+ name = "#{box.class} (#{x.to_i},#{y.to_i}-#{box.width.to_i}x#{box.height.to_i})"
139
+ ocg = doc.optional_content.ocg(name)
140
+ canvas.optional_content(ocg) do
141
+ canvas.save_graphics_state do
142
+ canvas.fill_color("green").stroke_color("darkgreen").
143
+ opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
144
+ draw(:geom2d, object: mask, path_only: true).fill_stroke
145
+ end
141
146
  end
147
+ doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: 'Debug')
142
148
  end
143
149
  box.draw(canvas, x, y)
144
150
  end
@@ -157,7 +163,8 @@ module HexaPDF
157
163
  # The height of the frame.
158
164
  attr_reader :height
159
165
 
160
- # The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
166
+ # The shape of the frame, either a Geom2D::Rectangle in the simple case or a
167
+ # Geom2D::PolygonSet consisting of rectilinear polygons in the more complex case.
161
168
  attr_reader :shape
162
169
 
163
170
  # The x-coordinate where the next box will be placed.
@@ -181,15 +188,19 @@ module HexaPDF
181
188
  # Also see the note in the #x documentation for further information.
182
189
  attr_reader :available_height
183
190
 
191
+ # The context object (a HexaPDF::Type::Page or HexaPDF::Type::Form) for which this frame
192
+ # should be used.
193
+ attr_reader :context
194
+
184
195
  # Creates a new Frame object for the given rectangular area.
185
- def initialize(left, bottom, width, height, shape: nil)
196
+ def initialize(left, bottom, width, height, shape: nil, context: nil)
186
197
  @left = left
187
198
  @bottom = bottom
188
199
  @width = width
189
200
  @height = height
190
- @shape = shape || Geom2D::PolygonSet.new(
191
- [create_rectangle(left, bottom, left + width, bottom + height)]
192
- )
201
+ @shape = shape || create_rectangle(left, bottom, left + width, bottom + height)
202
+ @context = context
203
+
193
204
  @x = left
194
205
  @y = bottom + height
195
206
  @available_width = width
@@ -199,6 +210,12 @@ module HexaPDF
199
210
  @region_selection = :max_height
200
211
  end
201
212
 
213
+ # Returns the HexaPDF::Document instance (through #context) that is associated with this Frame
214
+ # object or +nil+ if no context object has been set.
215
+ def document
216
+ @context&.document
217
+ end
218
+
202
219
  # Fits the given box into the current region of available space and returns a FitResult
203
220
  # object.
204
221
  #
@@ -319,8 +336,16 @@ module HexaPDF
319
336
  def find_next_region
320
337
  case @region_selection
321
338
  when :max_width
322
- find_max_width_region
323
- @region_selection = :max_height
339
+ if @shape.kind_of?(Geom2D::Rectangle)
340
+ @x = @shape.x
341
+ @y = @shape.y + @shape.height
342
+ @available_width = @shape.width
343
+ @available_height = @shape.height
344
+ @region_selection = :trim_shape
345
+ else
346
+ find_max_width_region
347
+ @region_selection = :max_height
348
+ end
324
349
  when :max_height
325
350
  x, y, aw, ah = @x, @y, @available_width, @available_height
326
351
  find_max_height_region
@@ -338,7 +363,17 @@ module HexaPDF
338
363
 
339
364
  # Removes the given *rectilinear* polygon from the frame's shape.
340
365
  def remove_area(polygon)
341
- @shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
366
+ @shape = if @shape.kind_of?(Geom2D::Rectangle) && polygon.kind_of?(Geom2D::Rectangle) &&
367
+ float_equal(@shape.x, polygon.x) && float_equal(@shape.width, polygon.width) &&
368
+ float_equal(@shape.y + @shape.height, polygon.y + polygon.height)
369
+ if float_equal(@shape.height, polygon.height)
370
+ Geom2D::PolygonSet()
371
+ else
372
+ Geom2D::Rectangle(@shape.x, @shape.y, @shape.width, @shape.height - polygon.height)
373
+ end
374
+ else
375
+ Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
376
+ end
342
377
  @region_selection = :max_width
343
378
  find_next_region
344
379
  end
@@ -369,8 +404,7 @@ module HexaPDF
369
404
  # Creates a Geom2D::Polygon object representing the rectangle with the bottom left corner
370
405
  # (blx, bly) and the top right corner (trx, try).
371
406
  def create_rectangle(blx, bly, trx, try)
372
- Geom2D::Polygon(Geom2D::Point(blx, bly), Geom2D::Point(trx, bly),
373
- Geom2D::Point(trx, try), Geom2D::Point(blx, try))
407
+ Geom2D::Rectangle(blx, bly, trx - blx, try - bly)
374
408
  end
375
409
 
376
410
  # Finds the region with the maximum width.
@@ -404,7 +438,8 @@ module HexaPDF
404
438
 
405
439
  # Trims the frame's shape so that the next starting point is different.
406
440
  def trim_shape
407
- return unless (segments = find_starting_point)
441
+ @x = @y = @available_width = @available_height = 0
442
+ return if @shape.kind_of?(Geom2D::Rectangle) || !(segments = find_starting_point)
408
443
 
409
444
  # Just use the second top-most segment
410
445
  # TODO: not the optimal solution!
@@ -74,6 +74,11 @@ module HexaPDF
74
74
  @image = image
75
75
  end
76
76
 
77
+ # Returns +false+ since the image is always drawn if it fits.
78
+ def empty?
79
+ false
80
+ end
81
+
77
82
  # Fits the image into the available space, taking the initially set width and height into
78
83
  # account (see the class description for details).
79
84
  def fit(available_width, available_height, _frame)
@@ -47,9 +47,10 @@ module HexaPDF
47
47
  # beforehand! This means the box *must* have at least its width set. The height may either also
48
48
  # be set or determined during fitting.
49
49
  #
50
- # Fitting of the wrapped box is done immediately after creating a InlineBox instance. For this,
51
- # a frame is used that has the width of the wrapped box and its height, or if not set, a
52
- # practically infinite height. In the latter case the height *must* be set during fitting.
50
+ # Fitting of the wrapped box via #fit_wrapped_box needs to be done before accessing any other
51
+ # method that uses the wrapped box. For fitting, a frame is used that has the width of the
52
+ # wrapped box and its height, or if not set, a practically infinite height. In the latter case
53
+ # the height *must* be set during fitting.
53
54
  class InlineBox
54
55
 
55
56
  # Creates an InlineBox that wraps a basic Box. All arguments (except +valign+) and the block
@@ -73,14 +74,14 @@ module HexaPDF
73
74
  # The +valign+ argument can be used to specify the vertical alignment of the box relative to
74
75
  # other items in the Line.
75
76
  def initialize(box, valign: :baseline)
77
+ raise HexaPDF::Error, "Width of box not set" if box.width == 0
76
78
  @box = box
77
79
  @valign = valign
78
- @fit_result = Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height).fit(box)
79
- if !@fit_result.success?
80
- raise HexaPDF::Error, "Box for inline use could not be fit"
81
- elsif box.height > 99_000
82
- raise HexaPDF::Error, "Box for inline use has no valid height set after fitting"
83
- end
80
+ end
81
+
82
+ # Returns the style of the wrapped box.
83
+ def style
84
+ box.style
84
85
  end
85
86
 
86
87
  # Returns +true+ if this inline box is just a placeholder without drawing operations.
@@ -125,6 +126,17 @@ module HexaPDF
125
126
  height
126
127
  end
127
128
 
129
+ # Fits the wrapped box, using the given context (see Frame#context).
130
+ def fit_wrapped_box(context)
131
+ @fit_result = Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height,
132
+ context: context).fit(box)
133
+ if !@fit_result.success?
134
+ raise HexaPDF::Error, "Box for inline use could not be fit"
135
+ elsif box.height > 99_000
136
+ raise HexaPDF::Error, "Box for inline use has no valid height set after fitting"
137
+ end
138
+ end
139
+
128
140
  end
129
141
 
130
142
  end
@@ -60,6 +60,9 @@ module HexaPDF
60
60
  # arguments are ignored.
61
61
  class ListBox < Box
62
62
 
63
+ # Stores the information when fitting an item of the list box.
64
+ ItemResult = Struct.new(:box_fitter, :height, :marker, :marker_pos_x)
65
+
63
66
  # The child boxes of this ListBox. They need to be finalized before #fit is called.
64
67
  attr_reader :children
65
68
 
@@ -177,6 +180,11 @@ module HexaPDF
177
180
  true
178
181
  end
179
182
 
183
+ # Returns +true+ if no box was fitted into the list box.
184
+ def empty?
185
+ super && (!@results || @results.all? {|result| result.box_fitter.fit_results.empty? })
186
+ end
187
+
180
188
  # Fits the list box into the available space.
181
189
  def fit(available_width, available_height, frame)
182
190
  @width = if @initial_width > 0
@@ -205,9 +213,10 @@ module HexaPDF
205
213
  end
206
214
 
207
215
  @results = []
208
- @results_item_marker_x = []
209
216
 
210
- @children.each do |child|
217
+ @children.each_with_index do |child, index|
218
+ item_result = ItemResult.new
219
+
211
220
  shape = Geom2D::Polygon([left, top - height],
212
221
  [left + width, top - height],
213
222
  [left + width, top],
@@ -217,24 +226,36 @@ module HexaPDF
217
226
  remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
218
227
  end
219
228
 
220
- item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height, shape: shape)
221
- @results_item_marker_x << item_frame.x - content_indentation
229
+ item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height,
230
+ shape: shape, context: frame.context)
231
+
232
+ if index != 0 || !split_box? || @split_box == :show_first_marker
233
+ box = item_marker_box(frame.document, index)
234
+ break unless box.fit(content_indentation, height, nil)
235
+ item_result.marker = box
236
+ item_result.marker_pos_x = item_frame.x - content_indentation
237
+ item_result.height = box.height
238
+ end
222
239
 
223
240
  box_fitter = BoxFitter.new([item_frame])
224
241
  Array(child).each {|box| box_fitter.fit(box) }
225
- @results << box_fitter
242
+ item_result.box_fitter = box_fitter
243
+ item_result.height = [item_result.height.to_i, box_fitter.content_heights[0]].max
244
+ @results << item_result
226
245
 
227
- top -= box_fitter.content_heights[0] + item_spacing
228
- height -= box_fitter.content_heights[0] + item_spacing
246
+ top -= item_result.height + item_spacing
247
+ height -= item_result.height + item_spacing
229
248
 
230
249
  break if !box_fitter.fit_successful? || height <= 0
231
250
  end
232
251
 
233
- @height = @results.sum {|box_fitter| box_fitter.content_heights[0] } +
252
+ @height = @results.sum {|item_result| item_result.height } +
234
253
  (@results.count - 1) * item_spacing +
235
254
  reserved_height
236
255
 
237
- @fit_successful = @results.all?(&:fit_successful?) && @results.size == @children.size
256
+ @draw_pos_x = frame.x + reserved_width_left
257
+ @draw_pos_y = frame.y - @height + reserved_height_bottom
258
+ @fit_successful = @results.all? {|r| r.box_fitter.fit_successful? } && @results.size == @children.size
238
259
  end
239
260
 
240
261
  private
@@ -285,7 +306,7 @@ module HexaPDF
285
306
 
286
307
  # Splits the content of the list box. This method is called from Box#split.
287
308
  def split_content(_available_width, _available_height, _frame)
288
- remaining_boxes = @results[-1].remaining_boxes
309
+ remaining_boxes = @results[-1].box_fitter.remaining_boxes
289
310
  first_is_split_box = remaining_boxes.first&.split_box?
290
311
  children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
291
312
 
@@ -294,7 +315,6 @@ module HexaPDF
294
315
  box.instance_variable_set(:@start_number,
295
316
  @start_number + @results.size + (first_is_split_box ? -1 : 0))
296
317
  box.instance_variable_set(:@results, [])
297
- box.instance_variable_set(:@results_item_marker_x, [])
298
318
 
299
319
  [self, box]
300
320
  end
@@ -308,20 +328,22 @@ module HexaPDF
308
328
  fragment = case @item_type
309
329
  when :disc
310
330
  TextFragment.create("•", font: document.fonts.add("Times"),
311
- font_size: style.font_size)
331
+ font_size: style.font_size, fill_color: style.fill_color)
312
332
  when :circle
313
333
  TextFragment.create("❍", font: document.fonts.add("ZapfDingbats"),
314
334
  font_size: style.font_size / 2.0,
335
+ fill_color: style.fill_color,
315
336
  text_rise: -style.font_size / 1.8)
316
337
  when :square
317
338
  TextFragment.create("■", font: document.fonts.add("ZapfDingbats"),
318
339
  font_size: style.font_size / 2.0,
340
+ fill_color: style.fill_color,
319
341
  text_rise: -style.font_size / 1.8)
320
342
  when :decimal
321
343
  text = (@start_number + index).to_s << "."
322
344
  decimal_style = {
323
345
  font: (style.font? ? style.font : document.fonts.add("Times")),
324
- font_size: style.font_size || 10,
346
+ font_size: style.font_size || 10, fill_color: style.fill_color
325
347
  }
326
348
  TextFragment.create(text, decimal_style)
327
349
  else
@@ -333,16 +355,24 @@ module HexaPDF
333
355
  end
334
356
 
335
357
  # Draws the list items onto the canvas at position [x, y].
336
- def draw_content(canvas, _x, _y)
337
- @results.each_with_index do |box_fitter, index|
338
- if index != 0 || !split_box? || @split_box == :show_first_marker
339
- box = item_marker_box(canvas.context.document, index)
340
- box.fit(content_indentation, box_fitter.content_heights[0], nil)
341
- box.draw(canvas, @results_item_marker_x[index],
342
- box_fitter.frames[0].bottom + box_fitter.frames[0].height - box.height)
358
+ def draw_content(canvas, x, y)
359
+ translate = (style.position != :flow && (x != @draw_pos_x || y != @draw_pos_y))
360
+
361
+ if translate
362
+ canvas.save_graphics_state
363
+ canvas.translate(x - @draw_pos_x, y - @draw_pos_y)
364
+ end
365
+
366
+ @results.each do |item_result|
367
+ box_fitter = item_result.box_fitter
368
+ if (marker = item_result.marker)
369
+ marker.draw(canvas, item_result.marker_pos_x,
370
+ box_fitter.frames[0].bottom + box_fitter.frames[0].height - marker.height)
343
371
  end
344
372
  box_fitter.fit_results.each {|result| result.draw(canvas) }
345
373
  end
374
+
375
+ canvas.restore_graphics_state if translate
346
376
  end
347
377
 
348
378
  end
@@ -95,8 +95,8 @@ module HexaPDF
95
95
  # If this attribute is +nil+ (the default), it means that this style should be used again.
96
96
  attr_accessor :next_style
97
97
 
98
- # Creates a new page style instance for the given page size and orientation. If a block is
99
- # given, it is used as template for defining the initial content.
98
+ # Creates a new page style instance for the given page size, orientation and next style
99
+ # values. If a block is given, it is used as template for defining the initial content.
100
100
  #
101
101
  # Example:
102
102
  #
@@ -105,12 +105,12 @@ module HexaPDF
105
105
  # style.next_style = :other
106
106
  # canvas.fill_color("fd0") { canvas.circle(100, 100, 50).fill }
107
107
  # end
108
- def initialize(page_size: :A4, orientation: :portrait, &block)
108
+ def initialize(page_size: :A4, orientation: :portrait, next_style: nil, &block)
109
109
  @page_size = page_size
110
110
  @orientation = orientation
111
111
  @template = block
112
112
  @frame = nil
113
- @next_style = nil
113
+ @next_style = next_style
114
114
  end
115
115
 
116
116
  # Creates a new page in the given document with this page style and returns it.
@@ -135,7 +135,8 @@ module HexaPDF
135
135
  Layout::Frame.new(box.left + margin.left,
136
136
  box.bottom + margin.bottom,
137
137
  box.width - margin.left - margin.right,
138
- box.height - margin.bottom - margin.top)
138
+ box.height - margin.bottom - margin.top,
139
+ context: page)
139
140
  end
140
141
 
141
142
  end
@@ -206,11 +206,16 @@ module HexaPDF
206
206
  # The styles of each edge. See Quad.
207
207
  attr_reader :style
208
208
 
209
+ # Specifies whether the border should be drawn inside the provided rectangle (+false+,
210
+ # default) or on it (+true+).
211
+ attr_accessor :draw_on_bounds
212
+
209
213
  # Creates a new border style. All arguments can be set to any value that a Quad can process.
210
- def initialize(width: 0, color: 0, style: :solid)
214
+ def initialize(width: 0, color: 0, style: :solid, draw_on_bounds: false)
211
215
  @width = Quad.new(width)
212
216
  @color = Quad.new(color)
213
217
  @style = Quad.new(style)
218
+ @draw_on_bounds = draw_on_bounds
214
219
  end
215
220
 
216
221
  # Duplicates a Border object's properties.
@@ -226,10 +231,20 @@ module HexaPDF
226
231
  width.simple? && width.top == 0
227
232
  end
228
233
 
229
- # Draws the border onto the canvas, inside the rectangle (x, y, w, h).
234
+ # Draws the border onto the canvas.
235
+ #
236
+ # Depending on #draw_on_bounds the border is drawn inside the rectangle (x, y, w, h) or on
237
+ # it.
230
238
  def draw(canvas, x, y, w, h)
231
239
  return if none?
232
240
 
241
+ if draw_on_bounds
242
+ x -= width.left / 2.0
243
+ y -= width.bottom / 2.0
244
+ w += (width.left + width.right) / 2.0
245
+ h += (width.top + width.bottom) / 2.0
246
+ end
247
+
233
248
  canvas.save_graphics_state do
234
249
  if width.simple? && color.simple? && style.simple?
235
250
  draw_simple_border(canvas, x, y, w, h)
@@ -444,17 +459,17 @@ module HexaPDF
444
459
  # Style objects using link annotations. Typical use cases would be linking to a (named)
445
460
  # destination on a different page or executing a URI action.
446
461
  #
447
- # See: PDF1.7 s12.5.6.6, Layers, HexaPDF::Type::Annotations::Link
462
+ # See: PDF2.0 s12.5.6.5, Layers, HexaPDF::Type::Annotations::Link
448
463
  class LinkLayer
449
464
 
450
465
  # Creates a new LinkLayer object.
451
466
  #
452
- # The following arguments are allowed (note that only *one* of +dest+, +uri+ or +file+ may
453
- # be specified):
467
+ # The following arguments are allowed (note that only *one* of +dest+, +uri+, +file+ or
468
+ # +action+ may be specified):
454
469
  #
455
470
  # +dest+::
456
471
  # The destination array or a name of a named destination for in-document links. If neither
457
- # +dest+ nor +uri+ nor +file+ is specified, it is assumed that the box has a custom
472
+ # +dest+, +uri+, +file+ nor +action+ is specified, it is assumed that the box has a custom
458
473
  # property named 'link' which is used for the destination.
459
474
  #
460
475
  # +uri+::
@@ -465,6 +480,9 @@ module HexaPDF
465
480
  # should be launched. Can either be a string or a Filespec object. Also see:
466
481
  # HexaPDF::Type::FileSpecification.
467
482
  #
483
+ # +action+::
484
+ # The PDF action that should be executed.
485
+ #
468
486
  # +border+::
469
487
  # If set to +true+, a standard border is used. Also accepts an array that adheres to the
470
488
  # rules for annotation borders.
@@ -477,15 +495,17 @@ module HexaPDF
477
495
  # LinkLayer.new(dest: [page, :XYZ, nil, nil, nil], border: true)
478
496
  # LinkLayer.new(uri: "https://my.example.com/path", border: [5 5 2])
479
497
  # LinkLayer.new # use 'link' custom box property for dest
480
- def initialize(dest: nil, uri: nil, file: nil, border: false, border_color: nil)
481
- if dest && (uri || file) || uri && file
482
- raise ArgumentError, "Only one of dest, uri and file is allowed"
498
+ def initialize(dest: nil, uri: nil, file: nil, action: nil, border: false, border_color: nil)
499
+ if dest && (uri || file || action) || uri && (file || action) || file && action
500
+ raise ArgumentError, "Only one of dest, uri, file or action is allowed"
483
501
  end
484
502
  @dest = dest
485
503
  @action = if uri
486
504
  {S: :URI, URI: uri}
487
505
  elsif file
488
506
  {S: :Launch, F: file, NewWindow: true}
507
+ elsif action
508
+ action
489
509
  end
490
510
  @border = case border
491
511
  when false then [0, 0, 0]
@@ -1030,6 +1050,40 @@ module HexaPDF
1030
1050
  # line_spacing: 1.5, last_line_gap: true)
1031
1051
  # composer.text("There is spacing above this line due to last_line_gap.")
1032
1052
 
1053
+ ##
1054
+ # :method: fill_horizontal
1055
+ # :call-seq:
1056
+ # fill_horizontal(factor = nil)
1057
+ #
1058
+ # If set to a positive number, it specifies that the content of the text item should be
1059
+ # repeated and appropriate spacing applied so that the remaining space of the line is
1060
+ # completely filled.
1061
+ #
1062
+ # If there are multiple text items with this property set for a single line, the remaining
1063
+ # space is split between those items using the set +factors+. For example, if item A has a
1064
+ # factor of 1 and item B a factor of 2, the remaining space will be split so that item
1065
+ # B will receive twice the space of A.
1066
+ #
1067
+ # Notes:
1068
+ #
1069
+ # * This property _must not_ be applied to inline boxes, it only works for text items.
1070
+ # * If the filling should be done with spaces, the non-breaking space character \u{00a0} has
1071
+ # to be used.
1072
+ #
1073
+ # Examples:
1074
+ #
1075
+ # #>pdf-composer100
1076
+ # composer.formatted_text(["Left", {text: "\u{00a0}", fill_horizontal: 1},
1077
+ # "Right"])
1078
+ # composer.formatted_text(["Typical table of contents entry",
1079
+ # {text: ".", fill_horizontal: 1}, "34"])
1080
+ # composer.formatted_text(["Factor 1", {text: "\u{00a0}", fill_horizontal: 1},
1081
+ # "Factor 3", {text: "\u{00a0}", fill_horizontal: 3}, "End"])
1082
+ # overlays = [proc {|c, b| c.line(0, b.height / 2.0, b.width, b.height / 2.0).stroke}]
1083
+ # composer.formatted_text([{text: "\u{00a0}", fill_horizontal: 1, overlays: overlays},
1084
+ # 'Centered',
1085
+ # {text: "\u{00a0}", fill_horizontal: 1, overlays: overlays}])
1086
+
1033
1087
  ##
1034
1088
  # :method: background_color
1035
1089
  # :call-seq:
@@ -1278,6 +1332,7 @@ module HexaPDF
1278
1332
  "{type: value, value: extra_arg} : value))",
1279
1333
  extra_args: ", extra_arg = nil"}],
1280
1334
  [:last_line_gap, false, {valid_values: [true, false]}],
1335
+ [:fill_horizontal, nil],
1281
1336
  [:background_color, nil],
1282
1337
  [:background_alpha, 1],
1283
1338
  [:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],