hexapdf 0.8.0 → 0.9.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/018-composer.rb +44 -0
  7. data/lib/hexapdf/cli.rb +2 -0
  8. data/lib/hexapdf/cli/command.rb +2 -2
  9. data/lib/hexapdf/cli/optimize.rb +1 -1
  10. data/lib/hexapdf/cli/split.rb +82 -0
  11. data/lib/hexapdf/composer.rb +303 -0
  12. data/lib/hexapdf/configuration.rb +2 -2
  13. data/lib/hexapdf/content/canvas.rb +3 -6
  14. data/lib/hexapdf/dictionary.rb +0 -3
  15. data/lib/hexapdf/document.rb +30 -22
  16. data/lib/hexapdf/document/files.rb +1 -1
  17. data/lib/hexapdf/document/images.rb +1 -1
  18. data/lib/hexapdf/filter/predictor.rb +8 -8
  19. data/lib/hexapdf/layout.rb +1 -0
  20. data/lib/hexapdf/layout/box.rb +55 -12
  21. data/lib/hexapdf/layout/frame.rb +143 -46
  22. data/lib/hexapdf/layout/image_box.rb +96 -0
  23. data/lib/hexapdf/layout/inline_box.rb +10 -0
  24. data/lib/hexapdf/layout/line.rb +1 -1
  25. data/lib/hexapdf/layout/style.rb +55 -3
  26. data/lib/hexapdf/layout/text_box.rb +38 -8
  27. data/lib/hexapdf/layout/text_layouter.rb +66 -52
  28. data/lib/hexapdf/object.rb +3 -2
  29. data/lib/hexapdf/parser.rb +6 -1
  30. data/lib/hexapdf/rectangle.rb +6 -0
  31. data/lib/hexapdf/reference.rb +4 -6
  32. data/lib/hexapdf/revision.rb +34 -8
  33. data/lib/hexapdf/revisions.rb +12 -2
  34. data/lib/hexapdf/stream.rb +18 -0
  35. data/lib/hexapdf/task.rb +1 -1
  36. data/lib/hexapdf/task/dereference.rb +1 -1
  37. data/lib/hexapdf/task/optimize.rb +2 -2
  38. data/lib/hexapdf/type/form.rb +10 -0
  39. data/lib/hexapdf/version.rb +1 -1
  40. data/lib/hexapdf/writer.rb +25 -5
  41. data/man/man1/hexapdf.1 +17 -0
  42. data/test/hexapdf/layout/test_box.rb +7 -1
  43. data/test/hexapdf/layout/test_frame.rb +38 -1
  44. data/test/hexapdf/layout/test_image_box.rb +73 -0
  45. data/test/hexapdf/layout/test_inline_box.rb +8 -0
  46. data/test/hexapdf/layout/test_line.rb +1 -1
  47. data/test/hexapdf/layout/test_style.rb +58 -1
  48. data/test/hexapdf/layout/test_text_box.rb +57 -12
  49. data/test/hexapdf/layout/test_text_layouter.rb +14 -4
  50. data/test/hexapdf/task/test_optimize.rb +3 -3
  51. data/test/hexapdf/test_composer.rb +258 -0
  52. data/test/hexapdf/test_document.rb +29 -3
  53. data/test/hexapdf/test_importer.rb +8 -2
  54. data/test/hexapdf/test_object.rb +3 -1
  55. data/test/hexapdf/test_parser.rb +6 -0
  56. data/test/hexapdf/test_rectangle.rb +7 -0
  57. data/test/hexapdf/test_reference.rb +3 -1
  58. data/test/hexapdf/test_revision.rb +13 -0
  59. data/test/hexapdf/test_revisions.rb +1 -0
  60. data/test/hexapdf/test_stream.rb +13 -0
  61. data/test/hexapdf/test_writer.rb +13 -2
  62. data/test/hexapdf/type/test_annotation.rb +1 -1
  63. data/test/hexapdf/type/test_form.rb +13 -2
  64. metadata +10 -4
@@ -0,0 +1,96 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2018 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #++
33
+ require 'hexapdf/layout/box'
34
+
35
+ module HexaPDF
36
+ module Layout
37
+
38
+ # An Image box object is used for displaying an image.
39
+ #
40
+ # How an image is displayed inside an image box, depends on whether the +width+ and/or +height+
41
+ # of the box has been set:
42
+ #
43
+ # * If one of them has been set, the other is adjusted to retain the image ratio.
44
+ # * If both have been set, both are used as is.
45
+ # * If neither has been set, the image is scaled to fit the available space.
46
+ #
47
+ # Also see: HexaPDF::Content::Canvas#image
48
+ class ImageBox < Box
49
+
50
+ # The image that is shown in the box.
51
+ attr_reader :image
52
+
53
+ # Creates a new Image box object for the given +image+ argument which needs to be an image
54
+ # object (e.g. returned by HexaPDF::Document::Images#add).
55
+ def initialize(image, **kwargs)
56
+ super(kwargs, &:unused_draw_block)
57
+ @image = image
58
+ end
59
+
60
+ # Fits the image into the available space.
61
+ def fit(available_width, available_height, _)
62
+ image_width = @image.width.to_f
63
+ image_height = @image.height.to_f
64
+ image_ratio = image_width / image_height
65
+
66
+ if @initial_width > 0 && @initial_height > 0
67
+ @width = @initial_width
68
+ @height = @initial_height
69
+ elsif @initial_width > 0
70
+ @width = @initial_width
71
+ @height = (@width - reserved_width) / image_ratio + reserved_height
72
+ elsif @initial_height > 0
73
+ @height = @initial_height
74
+ @width = (@height - reserved_height) * image_ratio + reserved_width
75
+ else
76
+ rw = reserved_width
77
+ rh = reserved_height
78
+ ratio = [(available_width - rw) / image_width, (available_height - rh) / image_height].min
79
+ @width = image_width * ratio + rw
80
+ @height = image_height * ratio + rh
81
+ end
82
+
83
+ @width <= available_width && @height <= available_height
84
+ end
85
+
86
+ private
87
+
88
+ # Draws the image onto the canvas at position [x, y].
89
+ def draw_content(canvas, x, y)
90
+ canvas.image(@image, at: [x, y], width: content_width, height: content_height)
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+ end
@@ -98,6 +98,16 @@ module HexaPDF
98
98
  width
99
99
  end
100
100
 
101
+ # The minimum y-coordinate which is always 0.
102
+ def y_min
103
+ 0
104
+ end
105
+
106
+ # The maximum y-coordinate which is equivalent to the height of the inline box.
107
+ def y_max
108
+ height
109
+ end
110
+
101
111
  end
102
112
 
103
113
  end
@@ -153,7 +153,7 @@ module HexaPDF
153
153
  max_bottom_height = @max_bottom_height
154
154
  max_text_bottom_height = @max_text_bottom_height
155
155
  y_min, y_max, = add(item).result
156
- y_max - y_min
156
+ [y_min, y_max, y_max - y_min]
157
157
  ensure
158
158
  @text_y_min = text_y_min
159
159
  @text_y_max = text_y_max
@@ -78,7 +78,7 @@ module HexaPDF
78
78
 
79
79
  # Creates a new LineSpacing object for the given type which can be any valid line spacing
80
80
  # type or a LineSpacing object.
81
- def initialize(type, value: 1)
81
+ def initialize(type:, value: 1)
82
82
  case type
83
83
  when :single
84
84
  @type = :proportional
@@ -207,6 +207,14 @@ module HexaPDF
207
207
  @style = Quad.new(style)
208
208
  end
209
209
 
210
+ # Duplicates a Border object's properties.
211
+ def initialize_copy(other)
212
+ super
213
+ @width = @width.dup
214
+ @color = @color.dup
215
+ @style = @style.dup
216
+ end
217
+
210
218
  # Returns +true+ if there is no border.
211
219
  def none?
212
220
  width.simple? && width.top == 0
@@ -372,6 +380,12 @@ module HexaPDF
372
380
  @layers = layers
373
381
  end
374
382
 
383
+ # Duplicates the array holding the layers.
384
+ def initialize_copy(other)
385
+ super
386
+ @layers = @layers.dup
387
+ end
388
+
375
389
  # :call-seq:
376
390
  # layers.add {|canvas, box| block}
377
391
  # layers.add(name, **options)
@@ -510,6 +524,20 @@ module HexaPDF
510
524
  @scaled_item_widths = {}
511
525
  end
512
526
 
527
+ # Duplicates the complex properties that can be modified, as well as the cache.
528
+ def initialize_copy(other)
529
+ super
530
+ @scaled_item_widths = {}
531
+ clear_cache
532
+
533
+ @font_features = @font_features.dup if defined?(@font_features)
534
+ @padding = @padding.dup if defined?(@padding)
535
+ @margin = @margin.dup if defined?(@margin)
536
+ @border = @border.dup if defined?(@border)
537
+ @overlays = @overlays.dup if defined?(@overlays)
538
+ @underlays = @underlays.dup if defined?(@underlays)
539
+ end
540
+
513
541
  # :call-seq:
514
542
  # style.update(**properties) -> style
515
543
  #
@@ -724,6 +752,28 @@ module HexaPDF
724
752
  #
725
753
  # The indentation to be used for the first line of a sequence of text lines, defaults to 0.
726
754
 
755
+ ##
756
+ # :method: line_spacing
757
+ # :call-seq:
758
+ # line_spacing(type = nil, value = nil)
759
+ # line_spacing(type:, value: 1)
760
+ #
761
+ # The type of line spacing to be used for text lines, defaults to type :single.
762
+ #
763
+ # This method can set the line spacing in two ways:
764
+ #
765
+ # * Using two positional arguments +type+ and +value+.
766
+ # * Or a hash with the keys +type+ and +value+.
767
+ #
768
+ # See LineSpacing for supported types of line spacing.
769
+
770
+ ##
771
+ # :method: last_line_gap
772
+ # :call-seq:
773
+ # last_line_gap(enable = false)
774
+ #
775
+ # Add an appropriately sized gap after the last line of text if enabled, defaults to false.
776
+
727
777
  ##
728
778
  # :method: background_color
729
779
  # :call-seq:
@@ -842,8 +892,10 @@ module HexaPDF
842
892
  [:align, :left],
843
893
  [:valign, :top],
844
894
  [:text_indent, 0],
845
- [:line_spacing, "LineSpacing.new(:single)",
846
- "LineSpacing.new(value, value: extra_arg)", ", extra_arg = nil"],
895
+ [:line_spacing, "LineSpacing.new(type: :single)",
896
+ "LineSpacing.new(value.kind_of?(Symbol) ? {type: value, value: extra_arg} : value)",
897
+ ", extra_arg = nil"],
898
+ [:last_line_gap, false],
847
899
  [:background_color, nil],
848
900
  [:padding, "Quad.new(0)", "Quad.new(value)"],
849
901
  [:margin, "Quad.new(0)", "Quad.new(value)"],
@@ -56,26 +56,56 @@ module HexaPDF
56
56
  # Depending on the 'position' style property, the text is either fit into the rectangular area
57
57
  # given by +available_width+ and +available_height+, or fit to the outline of the frame
58
58
  # starting from the top.
59
+ #
60
+ # The spacing after the last line can be controlled via the style property +last_line_gap+.
61
+ #
62
+ # Also see TextLayouter#style for other style properties taken into account.
59
63
  def fit(available_width, available_height, frame)
64
+ @width = @height = 0
60
65
  @result = if style.position == :flow
61
66
  @tl.fit(@items, frame.width_specification, frame.contour_line.bbox.height)
62
67
  else
63
- @tl.fit(@items, available_width, available_height)
68
+ @width = reserved_width
69
+ @height = reserved_height
70
+ width = (@initial_width > 0 ? @initial_width : available_width) - @width
71
+ height = (@initial_height > 0 ? @initial_height : available_height) - @height
72
+ @tl.fit(@items, width, height)
64
73
  end
65
- @height = @result.height
66
- @width = @result.lines.max_by(&:width)&.width || 0
74
+ @width += @result.lines.max_by(&:width)&.width || 0
75
+ @height += @result.height
76
+ if style.last_line_gap && @result.lines.last
77
+ @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
78
+ end
79
+
80
+ @result.status == :success
81
+ end
82
+
83
+ # Splits the text box into two boxes if necessary and possible.
84
+ def split(available_width, available_height, frame)
85
+ fit(available_width, available_height, frame) unless @result
86
+ if @result.remaining_items.empty?
87
+ [self]
88
+ elsif @result.lines.empty?
89
+ [nil, self]
90
+ else
91
+ box = clone
92
+ box.instance_variable_set(:@result, nil)
93
+ box.instance_variable_set(:@items, @result.remaining_items)
94
+ [self, box]
95
+ end
96
+ end
67
97
 
68
- success = (@result.status == :success)
69
- @draw_block = success ? method(:draw_text) : nil
70
- success
98
+ # :nodoc:
99
+ def empty?
100
+ super && (!@result || @result.lines.empty?)
71
101
  end
72
102
 
73
103
  private
74
104
 
75
105
  # Draws the text into the box.
76
- def draw_text(canvas, _self)
106
+ def draw_content(canvas, x, y)
77
107
  return unless @result && !@result.lines.empty?
78
- @result.draw(canvas, 0, @result.height)
108
+ @result.draw(canvas, x, y + content_height)
79
109
  end
80
110
 
81
111
  end
@@ -276,6 +276,19 @@ module HexaPDF
276
276
  end
277
277
  end
278
278
 
279
+ # A dummy line class for use with variable width wrapping, and Style#line_spacing methods in
280
+ # case a line actually consists of multiple line fragments.
281
+ DummyLine = Struct.new(:y_min, :y_max) do
282
+ def update(y_min, y_max)
283
+ self.y_min = y_min
284
+ self.y_max = y_max
285
+ end
286
+
287
+ def height
288
+ y_max - y_min
289
+ end
290
+ end
291
+
279
292
  # Implementation of a simple line wrapping algorithm.
280
293
  #
281
294
  # The algorithm arranges the given items so that the maximum number is put onto each line,
@@ -297,10 +310,11 @@ module HexaPDF
297
310
  # is the indentation of the first line). This is the general case.
298
311
  #
299
312
  # * However, if lines should have varying widths (e.g. for flowing text around shapes), the
300
- # +width_block+ argument should be an object responding to #call(line_height) where
301
- # +line_height+ is the height of the currently layed out line. The caller is responsible
302
- # for tracking the height of the already layed out lines. This method involves more work
303
- # and is therefore slower.
313
+ # +width_block+ argument should be an object responding to #call(line_like) where
314
+ # +line_like+ is a Line-like object responding to #y_min, #y_max and #height holding the
315
+ # values for the currently layed out line. The caller is responsible for tracking the
316
+ # height of the already layed out lines. This method involves more work and is therefore
317
+ # slower.
304
318
  #
305
319
  # Regardless of whether varying line widths are used or not, each time a line is finished,
306
320
  # it is yielded to the caller. The second argument +item+ is the item that caused the line
@@ -332,7 +346,6 @@ module HexaPDF
332
346
  def initialize(items, width_block)
333
347
  @items = items
334
348
  @width_block = width_block
335
- @available_width = @width_block.call(0)
336
349
  @line_items = []
337
350
  @width = 0
338
351
  @glue_items = []
@@ -342,7 +355,9 @@ module HexaPDF
342
355
  @break_prohibited_state = false
343
356
 
344
357
  @height_calc = Line::HeightCalculator.new
345
- @line_height = 0
358
+ @line = DummyLine.new(0, 0)
359
+
360
+ @available_width = @width_block.call(@line)
346
361
  end
347
362
 
348
363
  # Peforms line wrapping with a fixed width per line, with line height playing no role.
@@ -408,14 +423,14 @@ module HexaPDF
408
423
  while (item = @items[index])
409
424
  case item.type
410
425
  when :box
411
- new_height = @height_calc.simulate_height(item.item)
412
- if new_height > @line_height
413
- @line_height = new_height
414
- @available_width = @width_block.call(@line_height)
426
+ y_min, y_max, new_height = @height_calc.simulate_height(item.item)
427
+ if new_height > @line.height
428
+ @line.update(y_min, y_max)
429
+ @available_width = @width_block.call(@line)
415
430
  if !@available_width || @width > @available_width
416
431
  index = (@available_width ? @beginning_of_line_index : @stored_index)
417
432
  item = @items[index]
418
- reset_after_line_break_variable_width(index, @line_height)
433
+ reset_after_line_break_variable_width(index)
419
434
  redo
420
435
  end
421
436
  end
@@ -427,18 +442,18 @@ module HexaPDF
427
442
  item = @items[index]
428
443
  end
429
444
  break unless (action = yield(create_line, item))
430
- reset_after_line_break_variable_width(index, 0, action)
445
+ reset_after_line_break_variable_width(index, true, action)
431
446
  redo
432
447
  end
433
448
  when :glue
434
449
  unless add_glue_item(item.item, index)
435
450
  break unless (action = yield(create_line, item))
436
- reset_after_line_break_variable_width(index + 1, 0, action)
451
+ reset_after_line_break_variable_width(index + 1, true, action)
437
452
  end
438
453
  when :penalty
439
454
  if item.penalty <= -Penalty::INFINITY
440
455
  break unless (action = yield(create_unjustified_line, item))
441
- reset_after_line_break_variable_width(index + 1, 0, action)
456
+ reset_after_line_break_variable_width(index + 1, true, action)
442
457
  elsif item.penalty >= Penalty::INFINITY
443
458
  @break_prohibited_state = true
444
459
  add_box_item(item.item) if item.width > 0
@@ -447,8 +462,8 @@ module HexaPDF
447
462
  next_index = index + 1
448
463
  next_item = @items[next_index]
449
464
  next_item = @items[next_index += 1] while next_item&.type == :penalty
450
- new_height = @height_calc.simulate_height(next_item.item)
451
- if next_item && @width + next_item.width > @width_block.call(new_height)
465
+ y_min, y_max, new_height = @height_calc.simulate_height(next_item.item)
466
+ if next_item && @width + next_item.width > @width_block.call(DummyLine.new(y_min, y_max))
452
467
  @line_items.concat(@glue_items).push(item.item)
453
468
  @width += item.width
454
469
  # No need to clean up, since in the next iteration a line break occurs
@@ -531,7 +546,7 @@ module HexaPDF
531
546
  # Resets the line state variables to their initial values. The +index+ specifies the items
532
547
  # index of the first item on the new line. The +line_height+ specifies the line height to
533
548
  # use for getting the available width.
534
- def reset_after_line_break(index, line_height = 0)
549
+ def reset_after_line_break(index)
535
550
  @beginning_of_line_index = index
536
551
  @line_items.clear
537
552
  @width = 0
@@ -539,7 +554,7 @@ module HexaPDF
539
554
  @last_breakpoint_index = index
540
555
  @last_breakpoint_line_items_index = 0
541
556
  @break_prohibited_state = false
542
- @available_width = @width_block.call(line_height)
557
+ @available_width = @width_block.call(@line)
543
558
  end
544
559
 
545
560
  # Specialized reset method for variable width wrapping.
@@ -548,11 +563,11 @@ module HexaPDF
548
563
  #
549
564
  # * If the +action+ argument is +:store_start_of_line+, the stored item index is reset to
550
565
  # the index of the first item of the line.
551
- def reset_after_line_break_variable_width(index, line_height, action = :none)
566
+ def reset_after_line_break_variable_width(index, reset_line = false, action = :none)
552
567
  @stored_index = @beginning_of_line_index if action == :store_start_of_line
553
- @line_height = line_height
568
+ @line.update(0, 0) if reset_line
554
569
  @height_calc.reset
555
- reset_after_line_break(index, line_height)
570
+ reset_after_line_break(index)
556
571
  end
557
572
 
558
573
  end
@@ -592,23 +607,23 @@ module HexaPDF
592
607
 
593
608
  # Draws the layed out lines onto the canvas with the top-left corner being at [x, y].
594
609
  def draw(canvas, x, y)
595
- last_item = nil
610
+ last_text_fragment = nil
596
611
  canvas.save_graphics_state do
597
612
  # Best effort for leading in case we have an evenly spaced paragraph
598
613
  canvas.leading(@lines[1].y_offset) if @lines.size > 1
599
- @lines.each_with_index do |line, index|
600
- y -= @lines[index].y_offset
614
+ @lines.each do |line|
615
+ y -= line.y_offset
601
616
  line_x = x + line.x_offset
602
617
  line.each do |item, item_x, item_y|
603
618
  if item.kind_of?(TextFragment)
604
619
  item.draw(canvas, line_x + item_x, y + item_y,
605
- ignore_text_properties: last_item&.style == item.style)
606
- last_item = item
620
+ ignore_text_properties: last_text_fragment&.style == item.style)
621
+ last_text_fragment = item
607
622
  elsif !item.empty?
608
623
  canvas.restore_graphics_state
609
624
  item.draw(canvas, line_x + item_x, y + item_y)
610
625
  canvas.save_graphics_state
611
- last_item = nil
626
+ last_text_fragment = nil
612
627
  end
613
628
  end
614
629
  end
@@ -681,21 +696,26 @@ module HexaPDF
681
696
  indent = style.text_indent
682
697
  line_fragments = []
683
698
  line_height = 0
684
- last_line = nil
699
+ previous_line = nil
685
700
  y_offset = 0
686
701
  width_spec = nil
687
702
  width_spec_index = 0
688
703
  width_block =
689
704
  if width.respond_to?(:call)
690
705
  last_actual_height = nil
691
- last_line_height = nil
692
- proc do |h|
693
- line_height = [line_height, h || 0].max
694
- if last_actual_height != actual_height || last_line_height != line_height
695
- spec = width.call(actual_height, line_height)
706
+ previous_line_height = nil
707
+ proc do |cur_line|
708
+ line_height = [line_height, cur_line.height || 0].max
709
+ if last_actual_height != actual_height || previous_line_height != line_height
710
+ gap = if previous_line
711
+ style.line_spacing.gap(previous_line, cur_line)
712
+ else
713
+ 0
714
+ end
715
+ spec = width.call(actual_height + gap, cur_line.height)
696
716
  spec = [0, spec] unless spec.kind_of?(Array)
697
717
  last_actual_height = actual_height
698
- last_line_height = line_height
718
+ previous_line_height = line_height
699
719
  else
700
720
  spec = width_spec
701
721
  end
@@ -732,7 +752,7 @@ module HexaPDF
732
752
  # item didn't fit into first part, find next available part
733
753
  if line.items.empty? && line_fragments.empty?
734
754
  old_height = actual_height
735
- while item.width > width_block.call(item.height) && actual_height <= height
755
+ while item.width > width_block.call(item.item) && actual_height <= height
736
756
  width_spec_index += 1
737
757
  if width_spec_index >= width_spec.size / 2
738
758
  actual_height += item.height / 3
@@ -761,11 +781,11 @@ module HexaPDF
761
781
 
762
782
  combined_line = create_combined_line(line_fragments)
763
783
  new_height = actual_height + combined_line.height +
764
- (last_line ? style.line_spacing.gap(last_line, combined_line) : 0)
784
+ (previous_line ? style.line_spacing.gap(previous_line, combined_line) : 0)
765
785
 
766
786
  if new_height <= height
767
787
  # valid line found, use it
768
- apply_offsets(line_fragments, width_spec, indent, last_line, combined_line, y_offset)
788
+ apply_offsets(line_fragments, width_spec, indent, previous_line, combined_line, y_offset)
769
789
  lines.concat(line_fragments)
770
790
  line_fragments.clear
771
791
  width_spec_index = 0
@@ -774,7 +794,7 @@ module HexaPDF
774
794
  else
775
795
  0
776
796
  end
777
- last_line = combined_line
797
+ previous_line = combined_line
778
798
  actual_height = new_height
779
799
  line_height = 0
780
800
  y_offset = nil
@@ -805,16 +825,6 @@ module HexaPDF
805
825
 
806
826
  private
807
827
 
808
- # :nodoc:
809
- #
810
- # A dummy line class for use with Style#line_spacing methods in case a line actually consists
811
- # of multiple line fragments.
812
- DummyLine = Struct.new(:y_min, :y_max) do
813
- def height
814
- y_max - y_min
815
- end
816
- end
817
-
818
828
  # Creates a line combining all items from the given line fragments for height calculations.
819
829
  def create_combined_line(line_frags)
820
830
  if line_frags.size == 1
@@ -828,7 +838,11 @@ module HexaPDF
828
838
  end
829
839
 
830
840
  # Applies the necessary x- and y-offsets to the line fragments.
831
- def apply_offsets(line_frags, width_spec, indent, last_line, combined_line, y_offset)
841
+ #
842
+ # Note that the offset for the first fragment of the first line is the top of the line since
843
+ # the #initial_baseline_offset method applies the correct offset to it once layouting is
844
+ # completely done.
845
+ def apply_offsets(line_frags, width_spec, indent, previous_line, combined_line, y_offset)
832
846
  cumulated_width = 0
833
847
  line_frags.each_with_index do |line, index|
834
848
  line.x_offset = cumulated_width + indent
@@ -838,12 +852,12 @@ module HexaPDF
838
852
  if index == 0
839
853
  line.y_offset = if y_offset
840
854
  y_offset + combined_line.y_max -
841
- (last_line ? last_line.y_min : line.y_max)
855
+ (previous_line ? previous_line.y_min : line.y_max)
842
856
  else
843
- style.line_spacing.baseline_distance(last_line, combined_line)
857
+ style.line_spacing.baseline_distance(previous_line, combined_line)
844
858
  end
859
+ indent = 0
845
860
  end
846
- indent = 0
847
861
  end
848
862
  end
849
863