hexapdf 0.33.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/examples/026-optional_content.rb +55 -0
  4. data/examples/027-composer_optional_content.rb +83 -0
  5. data/lib/hexapdf/cli/command.rb +7 -1
  6. data/lib/hexapdf/cli/fonts.rb +1 -1
  7. data/lib/hexapdf/cli/inspect.rb +2 -4
  8. data/lib/hexapdf/composer.rb +2 -1
  9. data/lib/hexapdf/configuration.rb +21 -1
  10. data/lib/hexapdf/content/canvas.rb +52 -0
  11. data/lib/hexapdf/content/operator.rb +2 -0
  12. data/lib/hexapdf/dictionary.rb +1 -0
  13. data/lib/hexapdf/dictionary_fields.rb +1 -2
  14. data/lib/hexapdf/digital_signature/verification_result.rb +1 -2
  15. data/lib/hexapdf/document/layout.rb +3 -0
  16. data/lib/hexapdf/document/pages.rb +1 -1
  17. data/lib/hexapdf/document.rb +7 -0
  18. data/lib/hexapdf/encryption/ruby_aes.rb +10 -20
  19. data/lib/hexapdf/layout/box.rb +23 -3
  20. data/lib/hexapdf/layout/column_box.rb +2 -1
  21. data/lib/hexapdf/layout/frame.rb +23 -6
  22. data/lib/hexapdf/layout/inline_box.rb +20 -9
  23. data/lib/hexapdf/layout/list_box.rb +34 -20
  24. data/lib/hexapdf/layout/page_style.rb +2 -1
  25. data/lib/hexapdf/layout/style.rb +46 -6
  26. data/lib/hexapdf/layout/table_box.rb +9 -7
  27. data/lib/hexapdf/layout/text_box.rb +9 -2
  28. data/lib/hexapdf/layout/text_fragment.rb +28 -2
  29. data/lib/hexapdf/layout/text_layouter.rb +21 -5
  30. data/lib/hexapdf/stream.rb +1 -2
  31. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  32. data/lib/hexapdf/type/actions.rb +1 -0
  33. data/lib/hexapdf/type/annotations/text.rb +1 -2
  34. data/lib/hexapdf/type/catalog.rb +10 -1
  35. data/lib/hexapdf/type/cid_font.rb +15 -1
  36. data/lib/hexapdf/type/form.rb +75 -5
  37. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  38. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  39. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  40. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  41. data/lib/hexapdf/type/page.rb +27 -11
  42. data/lib/hexapdf/type/page_label.rb +4 -8
  43. data/lib/hexapdf/type.rb +4 -0
  44. data/lib/hexapdf/utils/pdf_doc_encoding.rb +0 -1
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/test/hexapdf/content/test_canvas.rb +49 -0
  47. data/test/hexapdf/document/test_layout.rb +7 -2
  48. data/test/hexapdf/document/test_pages.rb +6 -6
  49. data/test/hexapdf/layout/test_box.rb +13 -4
  50. data/test/hexapdf/layout/test_frame.rb +13 -1
  51. data/test/hexapdf/layout/test_inline_box.rb +17 -8
  52. data/test/hexapdf/layout/test_list_box.rb +48 -31
  53. data/test/hexapdf/layout/test_style.rb +10 -0
  54. data/test/hexapdf/layout/test_table_box.rb +32 -26
  55. data/test/hexapdf/layout/test_text_box.rb +8 -0
  56. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  57. data/test/hexapdf/layout/test_text_layouter.rb +32 -5
  58. data/test/hexapdf/test_composer.rb +10 -0
  59. data/test/hexapdf/test_dictionary.rb +10 -0
  60. data/test/hexapdf/test_document.rb +4 -0
  61. data/test/hexapdf/test_writer.rb +3 -3
  62. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  63. data/test/hexapdf/type/test_catalog.rb +11 -0
  64. data/test/hexapdf/type/test_form.rb +119 -0
  65. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  66. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  67. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  68. data/test/hexapdf/type/test_page.rb +2 -2
  69. metadata +14 -3
@@ -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
@@ -182,13 +188,18 @@ module HexaPDF
182
188
  # Also see the note in the #x documentation for further information.
183
189
  attr_reader :available_height
184
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
+
185
195
  # Creates a new Frame object for the given rectangular area.
186
- def initialize(left, bottom, width, height, shape: nil)
196
+ def initialize(left, bottom, width, height, shape: nil, context: nil)
187
197
  @left = left
188
198
  @bottom = bottom
189
199
  @width = width
190
200
  @height = height
191
201
  @shape = shape || create_rectangle(left, bottom, left + width, bottom + height)
202
+ @context = context
192
203
 
193
204
  @x = left
194
205
  @y = bottom + height
@@ -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
  #
@@ -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
@@ -76,12 +77,11 @@ module HexaPDF
76
77
  raise HexaPDF::Error, "Width of box not set" if box.width == 0
77
78
  @box = box
78
79
  @valign = valign
79
- @fit_result = Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height).fit(box)
80
- if !@fit_result.success?
81
- raise HexaPDF::Error, "Box for inline use could not be fit"
82
- elsif box.height > 99_000
83
- raise HexaPDF::Error, "Box for inline use has no valid height set after fitting"
84
- end
80
+ end
81
+
82
+ # Returns the style of the wrapped box.
83
+ def style
84
+ box.style
85
85
  end
86
86
 
87
87
  # Returns +true+ if this inline box is just a placeholder without drawing operations.
@@ -126,6 +126,17 @@ module HexaPDF
126
126
  height
127
127
  end
128
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
+
129
140
  end
130
141
 
131
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
 
@@ -179,7 +182,7 @@ module HexaPDF
179
182
 
180
183
  # Returns +true+ if no box was fitted into the list box.
181
184
  def empty?
182
- super && (!@results || @results.all? {|box_fitter| box_fitter.fit_results.empty? })
185
+ super && (!@results || @results.all? {|result| result.box_fitter.fit_results.empty? })
183
186
  end
184
187
 
185
188
  # Fits the list box into the available space.
@@ -210,9 +213,10 @@ module HexaPDF
210
213
  end
211
214
 
212
215
  @results = []
213
- @results_item_marker_x = []
214
216
 
215
- @children.each do |child|
217
+ @children.each_with_index do |child, index|
218
+ item_result = ItemResult.new
219
+
216
220
  shape = Geom2D::Polygon([left, top - height],
217
221
  [left + width, top - height],
218
222
  [left + width, top],
@@ -222,26 +226,36 @@ module HexaPDF
222
226
  remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
223
227
  end
224
228
 
225
- item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height, shape: shape)
226
- @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
227
239
 
228
240
  box_fitter = BoxFitter.new([item_frame])
229
241
  Array(child).each {|box| box_fitter.fit(box) }
230
- @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
231
245
 
232
- top -= box_fitter.content_heights[0] + item_spacing
233
- height -= box_fitter.content_heights[0] + item_spacing
246
+ top -= item_result.height + item_spacing
247
+ height -= item_result.height + item_spacing
234
248
 
235
249
  break if !box_fitter.fit_successful? || height <= 0
236
250
  end
237
251
 
238
- @height = @results.sum {|box_fitter| box_fitter.content_heights[0] } +
252
+ @height = @results.sum {|item_result| item_result.height } +
239
253
  (@results.count - 1) * item_spacing +
240
254
  reserved_height
241
255
 
242
256
  @draw_pos_x = frame.x + reserved_width_left
243
257
  @draw_pos_y = frame.y - @height + reserved_height_bottom
244
- @fit_successful = @results.all?(&:fit_successful?) && @results.size == @children.size
258
+ @fit_successful = @results.all? {|r| r.box_fitter.fit_successful? } && @results.size == @children.size
245
259
  end
246
260
 
247
261
  private
@@ -292,7 +306,7 @@ module HexaPDF
292
306
 
293
307
  # Splits the content of the list box. This method is called from Box#split.
294
308
  def split_content(_available_width, _available_height, _frame)
295
- remaining_boxes = @results[-1].remaining_boxes
309
+ remaining_boxes = @results[-1].box_fitter.remaining_boxes
296
310
  first_is_split_box = remaining_boxes.first&.split_box?
297
311
  children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
298
312
 
@@ -301,7 +315,6 @@ module HexaPDF
301
315
  box.instance_variable_set(:@start_number,
302
316
  @start_number + @results.size + (first_is_split_box ? -1 : 0))
303
317
  box.instance_variable_set(:@results, [])
304
- box.instance_variable_set(:@results_item_marker_x, [])
305
318
 
306
319
  [self, box]
307
320
  end
@@ -315,20 +328,22 @@ module HexaPDF
315
328
  fragment = case @item_type
316
329
  when :disc
317
330
  TextFragment.create("•", font: document.fonts.add("Times"),
318
- font_size: style.font_size)
331
+ font_size: style.font_size, fill_color: style.fill_color)
319
332
  when :circle
320
333
  TextFragment.create("❍", font: document.fonts.add("ZapfDingbats"),
321
334
  font_size: style.font_size / 2.0,
335
+ fill_color: style.fill_color,
322
336
  text_rise: -style.font_size / 1.8)
323
337
  when :square
324
338
  TextFragment.create("■", font: document.fonts.add("ZapfDingbats"),
325
339
  font_size: style.font_size / 2.0,
340
+ fill_color: style.fill_color,
326
341
  text_rise: -style.font_size / 1.8)
327
342
  when :decimal
328
343
  text = (@start_number + index).to_s << "."
329
344
  decimal_style = {
330
345
  font: (style.font? ? style.font : document.fonts.add("Times")),
331
- font_size: style.font_size || 10,
346
+ font_size: style.font_size || 10, fill_color: style.fill_color
332
347
  }
333
348
  TextFragment.create(text, decimal_style)
334
349
  else
@@ -348,12 +363,11 @@ module HexaPDF
348
363
  canvas.translate(x - @draw_pos_x, y - @draw_pos_y)
349
364
  end
350
365
 
351
- @results.each_with_index do |box_fitter, index|
352
- if index != 0 || !split_box? || @split_box == :show_first_marker
353
- box = item_marker_box(canvas.context.document, index)
354
- box.fit(content_indentation, box_fitter.content_heights[0], nil)
355
- box.draw(canvas, @results_item_marker_x[index],
356
- box_fitter.frames[0].bottom + box_fitter.frames[0].height - box.height)
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)
357
371
  end
358
372
  box_fitter.fit_results.each {|result| result.draw(canvas) }
359
373
  end
@@ -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
@@ -464,12 +464,12 @@ module HexaPDF
464
464
 
465
465
  # Creates a new LinkLayer object.
466
466
  #
467
- # The following arguments are allowed (note that only *one* of +dest+, +uri+ or +file+ may
468
- # be specified):
467
+ # The following arguments are allowed (note that only *one* of +dest+, +uri+, +file+ or
468
+ # +action+ may be specified):
469
469
  #
470
470
  # +dest+::
471
471
  # The destination array or a name of a named destination for in-document links. If neither
472
- # +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
473
473
  # property named 'link' which is used for the destination.
474
474
  #
475
475
  # +uri+::
@@ -480,6 +480,9 @@ module HexaPDF
480
480
  # should be launched. Can either be a string or a Filespec object. Also see:
481
481
  # HexaPDF::Type::FileSpecification.
482
482
  #
483
+ # +action+::
484
+ # The PDF action that should be executed.
485
+ #
483
486
  # +border+::
484
487
  # If set to +true+, a standard border is used. Also accepts an array that adheres to the
485
488
  # rules for annotation borders.
@@ -492,15 +495,17 @@ module HexaPDF
492
495
  # LinkLayer.new(dest: [page, :XYZ, nil, nil, nil], border: true)
493
496
  # LinkLayer.new(uri: "https://my.example.com/path", border: [5 5 2])
494
497
  # LinkLayer.new # use 'link' custom box property for dest
495
- def initialize(dest: nil, uri: nil, file: nil, border: false, border_color: nil)
496
- if dest && (uri || file) || uri && file
497
- 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"
498
501
  end
499
502
  @dest = dest
500
503
  @action = if uri
501
504
  {S: :URI, URI: uri}
502
505
  elsif file
503
506
  {S: :Launch, F: file, NewWindow: true}
507
+ elsif action
508
+ action
504
509
  end
505
510
  @border = case border
506
511
  when false then [0, 0, 0]
@@ -1045,6 +1050,40 @@ module HexaPDF
1045
1050
  # line_spacing: 1.5, last_line_gap: true)
1046
1051
  # composer.text("There is spacing above this line due to last_line_gap.")
1047
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
+
1048
1087
  ##
1049
1088
  # :method: background_color
1050
1089
  # :call-seq:
@@ -1293,6 +1332,7 @@ module HexaPDF
1293
1332
  "{type: value, value: extra_arg} : value))",
1294
1333
  extra_args: ", extra_arg = nil"}],
1295
1334
  [:last_line_gap, false, {valid_values: [true, false]}],
1335
+ [:fill_horizontal, nil],
1296
1336
  [:background_color, nil],
1297
1337
  [:background_alpha, 1],
1298
1338
  [:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
@@ -212,13 +212,13 @@ module HexaPDF
212
212
  end
213
213
 
214
214
  # Fits the children of the table cell into the given rectangular area.
215
- def fit(available_width, available_height, _frame)
215
+ def fit(available_width, available_height, frame)
216
216
  @width = available_width
217
217
  width = available_width - reserved_width
218
218
  height = available_height - reserved_height
219
219
  return false if width <= 0 || height <= 0
220
220
 
221
- frame = Frame.new(0, 0, width, height)
221
+ frame = Frame.new(0, 0, width, height, context: frame.context)
222
222
  case children
223
223
  when Box
224
224
  fit_result = frame.fit(children)
@@ -376,9 +376,11 @@ module HexaPDF
376
376
  # The +column_info+ argument needs to be an array of arrays of the form [x_pos, width]
377
377
  # containing the horizontal positions and widths of each column.
378
378
  #
379
+ # The +frame+ argument is further handed down to the Cell instances for fitting.
380
+ #
379
381
  # The fitting of a cell is done through the Cell#fit method which stores the result in the
380
382
  # cell itself. Furthermore, Cell#left and Cell#top are also assigned correctly.
381
- def fit_rows(start_row, available_height, column_info)
383
+ def fit_rows(start_row, available_height, column_info, frame)
382
384
  height = available_height
383
385
  last_fitted_row_index = -1
384
386
  @cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
@@ -391,7 +393,7 @@ module HexaPDF
391
393
  else
392
394
  column_info[cell.column].last
393
395
  end
394
- unless cell.fit(available_cell_width, available_height, nil)
396
+ unless cell.fit(available_cell_width, available_height, frame)
395
397
  row_fit = false
396
398
  break
397
399
  end
@@ -587,7 +589,7 @@ module HexaPDF
587
589
  end
588
590
 
589
591
  # Fits the table into the available space.
590
- def fit(available_width, available_height, _frame)
592
+ def fit(available_width, available_height, frame)
591
593
  return false if (@initial_width > 0 && @initial_width > available_width) ||
592
594
  (@initial_height > 0 && @initial_height > available_height)
593
595
 
@@ -608,14 +610,14 @@ module HexaPDF
608
610
  @special_cells_fit_not_successful = false
609
611
  [@header_cells, @footer_cells].each do |special_cells|
610
612
  next unless special_cells
611
- special_used_height, last_fitted_row_index = special_cells.fit_rows(0, height, columns)
613
+ special_used_height, last_fitted_row_index = special_cells.fit_rows(0, height, columns, frame)
612
614
  height -= special_used_height
613
615
  used_height += special_used_height
614
616
  @special_cells_fit_not_successful = (last_fitted_row_index != special_cells.number_of_rows - 1)
615
617
  return false if @special_cells_fit_not_successful
616
618
  end
617
619
 
618
- main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns)
620
+ main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns, frame)
619
621
  used_height += main_used_height
620
622
 
621
623
  @width = (@initial_width > 0 ? @initial_width : columns[-1].sum + rw)
@@ -54,6 +54,13 @@ module HexaPDF
54
54
  @result = nil
55
55
  end
56
56
 
57
+ # Returns the text that will be drawn.
58
+ #
59
+ # This will ignore any inline boxes or kerning values.
60
+ def text
61
+ @items.map {|item| item.kind_of?(TextFragment) ? item.text : '' }.join
62
+ end
63
+
57
64
  # Returns +true+ as the 'position' style property value :flow is supported.
58
65
  def supports_position_flow?
59
66
  true
@@ -75,13 +82,13 @@ module HexaPDF
75
82
  @width = @height = 0
76
83
  @result = if style.position == :flow
77
84
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
78
- apply_first_text_indent: !split_box?)
85
+ apply_first_text_indent: !split_box?, frame: frame)
79
86
  else
80
87
  @width = reserved_width
81
88
  @height = reserved_height
82
89
  width = (@initial_width > 0 ? @initial_width : available_width) - @width
83
90
  height = (@initial_height > 0 ? @initial_height : available_height) - @height
84
- @tl.fit(@items, width, height, apply_first_text_indent: !split_box?)
91
+ @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
85
92
  end
86
93
  @width += if @initial_width > 0 || style.align == :center || style.align == :right
87
94
  width
@@ -111,6 +111,11 @@ module HexaPDF
111
111
  @properties = properties
112
112
  end
113
113
 
114
+ # Returns the text of the fragment.
115
+ def text
116
+ items.reject {|i| i.kind_of?(Numeric) }.map(&:str).join
117
+ end
118
+
114
119
  # Creates a new TextFragment with the same style and custom properties as this one but with
115
120
  # the given +items+.
116
121
  def dup_attributes(items)
@@ -283,6 +288,28 @@ module HexaPDF
283
288
  :text
284
289
  end
285
290
 
291
+ # Creates a new text fragment that repeats this fragment's items and applies the necessary
292
+ # spacing so that the returned text fragment fills the given +width+ completely.
293
+ #
294
+ # If the given +width+ is less than the fragment's width, +self+ is returned.
295
+ def fill_horizontal!(width)
296
+ return self if width < self.width
297
+
298
+ factor, rest = width.divmod(self.width)
299
+ items = @items * factor
300
+ rest = @items.inject(rest) do |available_width, item|
301
+ new_available_width = available_width - style.scaled_item_width(item)
302
+ break available_width if new_available_width < 0
303
+ items << item
304
+ new_available_width
305
+ end
306
+
307
+ spacing = rest / (items.size - 1)
308
+ new_style = @style.dup.update(character_spacing: spacing)
309
+ items << spacing / new_style.scaled_font_size # correct spacing after last item
310
+ self.class.new(items, new_style, properties: @properties.dup)
311
+ end
312
+
286
313
  # Clears all cached values.
287
314
  #
288
315
  # This method needs to be called if the fragment's items or attributes are changed!
@@ -293,8 +320,7 @@ module HexaPDF
293
320
 
294
321
  # :nodoc:
295
322
  def inspect
296
- "#<#{self.class.name} #{items.reject {|i| i.kind_of?(Numeric) }.map(&:str).join.inspect} " \
297
- "#{items.inspect}>"
323
+ "#<#{self.class.name} #{text.inspect} #{items.inspect}>"
298
324
  end
299
325
 
300
326
  private
@@ -340,8 +340,8 @@ module HexaPDF
340
340
  # current start of the line index should be stored for later use.
341
341
  #
342
342
  # After the algorithm is finished, it returns the unused items.
343
- def self.call(items, width_block, &block)
344
- obj = new(items, width_block)
343
+ def self.call(items, width_block, frame, &block)
344
+ obj = new(items, width_block, frame)
345
345
  if width_block.arity == 1
346
346
  obj.variable_width_wrapping(&block)
347
347
  else
@@ -353,9 +353,10 @@ module HexaPDF
353
353
 
354
354
  # Creates a new line wrapping object that arranges the +items+ on lines with the given
355
355
  # width.
356
- def initialize(items, width_block)
356
+ def initialize(items, width_block, frame)
357
357
  @items = items
358
358
  @width_block = width_block
359
+ @frame = frame
359
360
  @line_items = []
360
361
  @width = 0
361
362
  @glue_items = []
@@ -363,6 +364,7 @@ module HexaPDF
363
364
  @last_breakpoint_index = 0
364
365
  @last_breakpoint_line_items_index = 0
365
366
  @break_prohibited_state = false
367
+ @fill_horizontal = false
366
368
 
367
369
  @height_calc = Line::HeightCalculator.new
368
370
  @line = DummyLine.new(0, 0)
@@ -505,9 +507,11 @@ module HexaPDF
505
507
  #
506
508
  # Returns +true+ if the item could be added and +false+ otherwise.
507
509
  def add_box_item(item)
510
+ item.fit_wrapped_box(@frame&.context) if item.kind_of?(InlineBox)
508
511
  return false unless @width + item.width <= @available_width
509
512
  @line_items.concat(@glue_items).push(item)
510
513
  @width += item.width
514
+ @fill_horizontal ||= item.style.fill_horizontal
511
515
  @glue_items.clear
512
516
  true
513
517
  end
@@ -547,6 +551,17 @@ module HexaPDF
547
551
 
548
552
  # Creates a Line object from the current line items.
549
553
  def create_line
554
+ if @fill_horizontal
555
+ rest_width = @available_width - @width
556
+ indices = []
557
+ @line_items.each_with_index do |item, index|
558
+ next unless item.style.fill_horizontal
559
+ indices << [index, item.style.fill_horizontal]
560
+ rest_width += item.width
561
+ end
562
+ unit_width = rest_width / indices.sum(&:last)
563
+ indices.each {|i, count| @line_items[i] = @line_items[i].fill_horizontal!(unit_width * count) }
564
+ end
550
565
  Line.new(@line_items)
551
566
  end
552
567
 
@@ -566,6 +581,7 @@ module HexaPDF
566
581
  @last_breakpoint_index = index
567
582
  @last_breakpoint_line_items_index = 0
568
583
  @break_prohibited_state = false
584
+ @fill_horizontal = false
569
585
  @available_width = @width_block.call(@line)
570
586
  end
571
587
 
@@ -701,7 +717,7 @@ module HexaPDF
701
717
  # Specifies whether style.text_indent should be applied to the first line. This should be
702
718
  # set to +false+ if the items start with a continuation of a paragraph instead of starting
703
719
  # a new paragraph (e.g. after a page break).
704
- def fit(items, width, height, apply_first_text_indent: true)
720
+ def fit(items, width, height, apply_first_text_indent: true, frame: nil)
705
721
  unless items.empty? || items[0].respond_to?(:type)
706
722
  items = style.text_segmentation_algorithm.call(items)
707
723
  end
@@ -765,7 +781,7 @@ module HexaPDF
765
781
  too_wide_box = nil
766
782
  line_height = 0
767
783
 
768
- rest = style.text_line_wrapping_algorithm.call(rest, width_block) do |line, item|
784
+ rest = style.text_line_wrapping_algorithm.call(rest, width_block, frame) do |line, item|
769
785
  # make sure empty lines broken by mandatory paragraph breaks are not empty
770
786
  line << TextFragment.new([], style) if item&.type != :box && line.items.empty?
771
787
 
@@ -278,9 +278,8 @@ module HexaPDF
278
278
  end
279
279
  end
280
280
 
281
- # :nodoc:
282
281
  # A mapping from short name to long name for filters.
283
- FILTER_MAP = {AHx: :ASCIIHexDecode, A85: :ASCII85Decode, LZW: :LZWDecode,
282
+ FILTER_MAP = {AHx: :ASCIIHexDecode, A85: :ASCII85Decode, LZW: :LZWDecode, # :nodoc:
284
283
  Fl: :FlateDecode, RL: :RunLengthDecode, CCF: :CCITTFaxDecode,
285
284
  DCT: :DCTDecode}.freeze
286
285