hexapdf 0.8.0 → 0.9.0

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