hexapdf 0.32.2 → 0.34.0

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