hexapdf 0.33.0 → 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 (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