hexapdf 0.5.0 → 0.6.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -2
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/boxes.rb +68 -0
  7. data/examples/graphics.rb +12 -12
  8. data/examples/{text_box_alignment.rb → text_layouter_alignment.rb} +14 -14
  9. data/examples/text_layouter_inline_boxes.rb +66 -0
  10. data/examples/{text_box_line_wrapping.rb → text_layouter_line_wrapping.rb} +9 -10
  11. data/examples/{text_box_shapes.rb → text_layouter_shapes.rb} +58 -54
  12. data/examples/text_layouter_styling.rb +125 -0
  13. data/examples/truetype.rb +5 -7
  14. data/lib/hexapdf/cli/command.rb +1 -0
  15. data/lib/hexapdf/configuration.rb +170 -106
  16. data/lib/hexapdf/content/canvas.rb +41 -36
  17. data/lib/hexapdf/content/graphics_state.rb +15 -0
  18. data/lib/hexapdf/content/operator.rb +1 -1
  19. data/lib/hexapdf/dictionary.rb +20 -8
  20. data/lib/hexapdf/dictionary_fields.rb +8 -6
  21. data/lib/hexapdf/document.rb +25 -26
  22. data/lib/hexapdf/document/fonts.rb +4 -4
  23. data/lib/hexapdf/document/images.rb +2 -2
  24. data/lib/hexapdf/document/pages.rb +16 -16
  25. data/lib/hexapdf/encryption/security_handler.rb +41 -9
  26. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  27. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  28. data/lib/hexapdf/filter/predictor.rb +7 -1
  29. data/lib/hexapdf/font/true_type/font.rb +20 -0
  30. data/lib/hexapdf/font/type1/font.rb +23 -0
  31. data/lib/hexapdf/font_loader.rb +1 -0
  32. data/lib/hexapdf/font_loader/from_configuration.rb +2 -3
  33. data/lib/hexapdf/font_loader/from_file.rb +65 -0
  34. data/lib/hexapdf/image_loader/png.rb +2 -2
  35. data/lib/hexapdf/layout.rb +3 -2
  36. data/lib/hexapdf/layout/box.rb +146 -0
  37. data/lib/hexapdf/layout/inline_box.rb +40 -31
  38. data/lib/hexapdf/layout/{line_fragment.rb → line.rb} +12 -13
  39. data/lib/hexapdf/layout/style.rb +630 -41
  40. data/lib/hexapdf/layout/text_fragment.rb +80 -12
  41. data/lib/hexapdf/layout/{text_box.rb → text_layouter.rb} +164 -109
  42. data/lib/hexapdf/number_tree_node.rb +1 -1
  43. data/lib/hexapdf/parser.rb +4 -1
  44. data/lib/hexapdf/revisions.rb +11 -4
  45. data/lib/hexapdf/stream.rb +8 -9
  46. data/lib/hexapdf/tokenizer.rb +5 -3
  47. data/lib/hexapdf/type.rb +3 -0
  48. data/lib/hexapdf/type/action.rb +56 -0
  49. data/lib/hexapdf/type/actions.rb +52 -0
  50. data/lib/hexapdf/type/actions/go_to.rb +52 -0
  51. data/lib/hexapdf/type/actions/go_to_r.rb +54 -0
  52. data/lib/hexapdf/type/actions/launch.rb +73 -0
  53. data/lib/hexapdf/type/actions/uri.rb +65 -0
  54. data/lib/hexapdf/type/annotation.rb +85 -0
  55. data/lib/hexapdf/type/annotations.rb +51 -0
  56. data/lib/hexapdf/type/annotations/link.rb +70 -0
  57. data/lib/hexapdf/type/annotations/markup_annotation.rb +70 -0
  58. data/lib/hexapdf/type/annotations/text.rb +81 -0
  59. data/lib/hexapdf/type/catalog.rb +3 -1
  60. data/lib/hexapdf/type/embedded_file.rb +6 -11
  61. data/lib/hexapdf/type/file_specification.rb +4 -6
  62. data/lib/hexapdf/type/font.rb +3 -1
  63. data/lib/hexapdf/type/font_descriptor.rb +18 -16
  64. data/lib/hexapdf/type/form.rb +3 -1
  65. data/lib/hexapdf/type/graphics_state_parameter.rb +3 -1
  66. data/lib/hexapdf/type/image.rb +4 -2
  67. data/lib/hexapdf/type/info.rb +2 -5
  68. data/lib/hexapdf/type/names.rb +2 -5
  69. data/lib/hexapdf/type/object_stream.rb +2 -1
  70. data/lib/hexapdf/type/page.rb +14 -1
  71. data/lib/hexapdf/type/page_tree_node.rb +9 -6
  72. data/lib/hexapdf/type/resources.rb +2 -5
  73. data/lib/hexapdf/type/trailer.rb +2 -5
  74. data/lib/hexapdf/type/viewer_preferences.rb +2 -5
  75. data/lib/hexapdf/type/xref_stream.rb +3 -1
  76. data/lib/hexapdf/version.rb +1 -1
  77. data/test/hexapdf/common_tokenizer_tests.rb +3 -1
  78. data/test/hexapdf/content/test_canvas.rb +29 -3
  79. data/test/hexapdf/content/test_graphics_state.rb +11 -0
  80. data/test/hexapdf/content/test_operator.rb +3 -2
  81. data/test/hexapdf/document/test_fonts.rb +8 -8
  82. data/test/hexapdf/document/test_images.rb +4 -12
  83. data/test/hexapdf/document/test_pages.rb +7 -7
  84. data/test/hexapdf/encryption/test_security_handler.rb +1 -5
  85. data/test/hexapdf/filter/test_predictor.rb +40 -12
  86. data/test/hexapdf/font/true_type/test_font.rb +16 -0
  87. data/test/hexapdf/font/type1/test_font.rb +30 -0
  88. data/test/hexapdf/font_loader/test_from_file.rb +29 -0
  89. data/test/hexapdf/font_loader/test_standard14.rb +4 -3
  90. data/test/hexapdf/layout/test_box.rb +104 -0
  91. data/test/hexapdf/layout/test_inline_box.rb +24 -10
  92. data/test/hexapdf/layout/{test_line_fragment.rb → test_line.rb} +9 -9
  93. data/test/hexapdf/layout/test_style.rb +519 -31
  94. data/test/hexapdf/layout/test_text_fragment.rb +136 -15
  95. data/test/hexapdf/layout/{test_text_box.rb → test_text_layouter.rb} +224 -144
  96. data/test/hexapdf/layout/test_text_shaper.rb +1 -1
  97. data/test/hexapdf/test_configuration.rb +12 -6
  98. data/test/hexapdf/test_dictionary.rb +27 -2
  99. data/test/hexapdf/test_dictionary_fields.rb +10 -1
  100. data/test/hexapdf/test_document.rb +14 -13
  101. data/test/hexapdf/test_parser.rb +12 -0
  102. data/test/hexapdf/test_revisions.rb +34 -0
  103. data/test/hexapdf/test_stream.rb +1 -1
  104. data/test/hexapdf/test_type.rb +18 -0
  105. data/test/hexapdf/test_writer.rb +2 -2
  106. data/test/hexapdf/type/actions/test_launch.rb +24 -0
  107. data/test/hexapdf/type/actions/test_uri.rb +23 -0
  108. data/test/hexapdf/type/annotations/test_link.rb +19 -0
  109. data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
  110. data/test/hexapdf/type/annotations/test_text.rb +38 -0
  111. data/test/hexapdf/type/test_annotation.rb +38 -0
  112. data/test/hexapdf/type/test_file_specification.rb +0 -7
  113. data/test/hexapdf/type/test_info.rb +0 -5
  114. data/test/hexapdf/type/test_page.rb +14 -0
  115. data/test/hexapdf/type/test_page_tree_node.rb +4 -1
  116. data/test/hexapdf/type/test_trailer.rb +0 -4
  117. data/test/test_helper.rb +6 -3
  118. metadata +36 -15
  119. data/examples/text_box_inline_boxes.rb +0 -56
  120. data/examples/text_box_styling.rb +0 -72
  121. data/test/hexapdf/type/test_embedded_file.rb +0 -16
  122. data/test/hexapdf/type/test_names.rb +0 -9
@@ -9,7 +9,7 @@ require 'hexapdf/layout/text_fragment'
9
9
  describe HexaPDF::Layout::TextFragment do
10
10
  before do
11
11
  @doc = HexaPDF::Document.new
12
- @font = @doc.fonts.load("Times", custom_encoding: true)
12
+ @font = @doc.fonts.add("Times", custom_encoding: true)
13
13
  end
14
14
 
15
15
  def setup_fragment(items, text_rise = 0)
@@ -27,24 +27,139 @@ describe HexaPDF::Layout::TextFragment do
27
27
  assert_equal(13.66 + 4.34, frag.height)
28
28
  end
29
29
 
30
+ describe "initialize" do
31
+ before do
32
+ @items = @font.decode_utf8("Tom")
33
+ end
34
+
35
+ it "can use a Style object" do
36
+ style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
37
+ frag = HexaPDF::Layout::TextFragment.new(items: @items, style: style)
38
+ assert_equal(20, frag.style.font_size)
39
+ end
40
+
41
+ it "can use a style options" do
42
+ frag = HexaPDF::Layout::TextFragment.new(items: @items, style: {font: @font, font_size: 20})
43
+ assert_equal(20, frag.style.font_size)
44
+ end
45
+ end
46
+
30
47
  it "returns :text for valign" do
31
48
  assert_equal(:text, setup_fragment([]).valign)
32
49
  end
33
50
 
34
- it "draws the text onto the canvas" do
35
- setup_fragment(@font.decode_utf8('H'), 2)
36
- canvas = @doc.pages.add.canvas
37
- @fragment.draw(canvas, 10, 15)
38
- assert_operators(canvas.contents,
39
- [[:begin_text],
40
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]],
41
- [:set_font_and_size, [:F1, 20]],
42
- [:set_leading, [24.0]],
43
- [:set_horizontal_scaling, [200]],
44
- [:set_character_spacing, [1]],
45
- [:set_word_spacing, [2]],
46
- [:set_text_rise, [2]],
47
- [:show_text_with_positioning, [['!']]]])
51
+ describe "draw" do
52
+ def setup_with_style(**styles)
53
+ setup_fragment(@font.decode_utf8('H'), 2)
54
+ styles.each {|name, value| @fragment.style.send(name, value)}
55
+ @canvas = @doc.pages.add.canvas
56
+ @fragment.draw(@canvas, 10, 15)
57
+ end
58
+
59
+ def assert_draw_operators(*args, front: [], middle: args, back: [])
60
+ ops = [
61
+ *front,
62
+ [:begin_text],
63
+ [:set_text_matrix, [1, 0, 0, 1, 10, 15]],
64
+ [:set_font_and_size, [:F1, 20]],
65
+ [:set_leading, [24.0]],
66
+ [:set_horizontal_scaling, [200]],
67
+ [:set_character_spacing, [1]],
68
+ [:set_word_spacing, [2]],
69
+ [:set_text_rise, [2]],
70
+ *middle,
71
+ [:show_text_with_positioning, [['!']]],
72
+ *back,
73
+ ].compact
74
+ assert_operators(@canvas.contents, ops)
75
+ end
76
+
77
+ it "draws text onto the canvas" do
78
+ setup_with_style
79
+ assert_draw_operators
80
+ end
81
+
82
+ it "draws styled filled text" do
83
+ setup_with_style(fill_color: 0.5, fill_alpha: 0.5)
84
+ assert_draw_operators([:set_graphics_state_parameters, [:GS1]],
85
+ [:set_device_gray_non_stroking_color, [0.5]])
86
+ assert_equal({Type: :ExtGState, CA: 1, ca: 0.5}, @canvas.resources[:ExtGState][:GS1])
87
+ end
88
+
89
+ it "draws style stroked text" do
90
+ setup_with_style(text_rendering_mode: :stroke,
91
+ stroke_color: [1.0, 0, 0], stroke_alpha: 0.5, stroke_width: 2,
92
+ stroke_cap_style: :round, stroke_join_style: :round, stroke_miter_limit: 5,
93
+ stroke_dash_pattern: [1, 2, 3])
94
+ assert_draw_operators([:set_text_rendering_mode, [1]],
95
+ [:set_graphics_state_parameters, [:GS1]],
96
+ [:set_device_rgb_stroking_color, [1, 0, 0]],
97
+ [:set_line_width, [2]],
98
+ [:set_line_cap_style, [1]],
99
+ [:set_line_join_style, [1]],
100
+ [:set_miter_limit, [5]],
101
+ [:set_line_dash_pattern, [[1, 2, 3], 0]])
102
+ assert_equal({Type: :ExtGState, CA: 0.5, ca: 1}, @canvas.resources[:ExtGState][:GS1])
103
+ end
104
+
105
+ it "invokes the underlays" do
106
+ setup_with_style(underlays: [proc { @canvas.stroke_color(0.5) }])
107
+ assert_draw_operators(front: [[:save_graphics_state],
108
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 15 + @fragment.y_min]],
109
+ [:save_graphics_state],
110
+ [:set_device_gray_stroking_color, [0.5]],
111
+ [:restore_graphics_state],
112
+ [:restore_graphics_state]])
113
+ end
114
+
115
+ it "invokes the overlays" do
116
+ setup_with_style(overlays: [proc { @canvas.stroke_color(0.5) }])
117
+ assert_draw_operators(back: [[:end_text],
118
+ [:save_graphics_state],
119
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 15 + @fragment.y_min]],
120
+ [:save_graphics_state],
121
+ [:set_device_gray_stroking_color, [0.5]],
122
+ [:restore_graphics_state],
123
+ [:restore_graphics_state]])
124
+ end
125
+
126
+ it "draws the underline" do
127
+ setup_with_style(underline: true, text_rendering_mode: :stroke,
128
+ stroke_width: 5, stroke_color: [0.5], stroke_cap_style: :round,
129
+ stroke_dash_pattern: 5)
130
+ assert_draw_operators(middle: [[:set_text_rendering_mode, [1]],
131
+ [:set_device_gray_stroking_color, [0.5]],
132
+ [:set_line_width, [5]],
133
+ [:set_line_cap_style, [1]],
134
+ [:set_line_dash_pattern, [[5], 0]]],
135
+ back: [[:set_device_gray_stroking_color, [0]],
136
+ [:set_line_width, [@fragment.style.calculated_underline_thickness]],
137
+ [:set_line_cap_style, [0]],
138
+ [:set_line_dash_pattern, [[], 0]],
139
+ [:end_text],
140
+ [:move_to, [10, 15]],
141
+ [:line_to, [40.88, 15]],
142
+ [:stroke_path]])
143
+ end
144
+
145
+ it "draws the strikeout line" do
146
+ setup_with_style(strikeout: true, text_rendering_mode: :stroke,
147
+ stroke_width: 5, stroke_color: [0.5], stroke_cap_style: :round,
148
+ stroke_dash_pattern: 5)
149
+ assert_draw_operators(middle: [[:set_text_rendering_mode, [1]],
150
+ [:set_device_gray_stroking_color, [0.5]],
151
+ [:set_line_width, [5]],
152
+ [:set_line_cap_style, [1]],
153
+ [:set_line_dash_pattern, [[5], 0]]],
154
+ back: [[:set_device_gray_stroking_color, [0]],
155
+ [:set_line_width, [@fragment.style.calculated_strikeout_thickness]],
156
+ [:set_line_cap_style, [0]],
157
+ [:set_line_dash_pattern, [[], 0]],
158
+ [:end_text],
159
+ [:move_to, [10, 21.01]],
160
+ [:line_to, [40.88, 21.01]],
161
+ [:stroke_path]])
162
+ end
48
163
  end
49
164
 
50
165
  describe "empty fragment" do
@@ -75,6 +190,12 @@ describe HexaPDF::Layout::TextFragment do
75
190
  it "calculates the height" do
76
191
  assert_equal(13.66 + 4.34, @fragment.height)
77
192
  end
193
+
194
+ it "draws nothing" do
195
+ canvas = @doc.pages.add.canvas
196
+ @fragment.draw(canvas, 10, 15)
197
+ assert_operators(canvas.contents, [])
198
+ end
78
199
  end
79
200
 
80
201
  describe "normal text" do
@@ -5,28 +5,28 @@ require 'hexapdf/layout'
5
5
  require 'hexapdf/document'
6
6
  require_relative "../content/common"
7
7
 
8
- module TestTextBoxHelpers
8
+ module TestTextLayouterHelpers
9
9
  def boxes(*dims)
10
10
  dims.map do |width, height|
11
- box = HexaPDF::Layout::InlineBox.new(width, height || 0) {}
12
- HexaPDF::Layout::TextBox::Box.new(box)
11
+ box = HexaPDF::Layout::InlineBox.create(width: width, height: height || 0) {}
12
+ HexaPDF::Layout::TextLayouter::Box.new(box)
13
13
  end
14
14
  end
15
15
 
16
16
  def glue(width)
17
- HexaPDF::Layout::TextBox::Glue.new(HexaPDF::Layout::InlineBox.new(width, 0) {})
17
+ HexaPDF::Layout::TextLayouter::Glue.new(HexaPDF::Layout::InlineBox.create(width: width) {})
18
18
  end
19
19
 
20
20
  def penalty(penalty, item = nil)
21
21
  if item
22
- HexaPDF::Layout::TextBox::Penalty.new(penalty, item.width, item: item)
22
+ HexaPDF::Layout::TextLayouter::Penalty.new(penalty, item.width, item: item)
23
23
  else
24
- HexaPDF::Layout::TextBox::Penalty.new(penalty)
24
+ HexaPDF::Layout::TextLayouter::Penalty.new(penalty)
25
25
  end
26
26
  end
27
27
 
28
28
  def assert_box(obj, item)
29
- assert_kind_of(HexaPDF::Layout::TextBox::Box, obj)
29
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Box, obj)
30
30
  if obj.item.kind_of?(HexaPDF::Layout::InlineBox)
31
31
  assert_same(item, obj.item)
32
32
  else
@@ -36,12 +36,12 @@ module TestTextBoxHelpers
36
36
  end
37
37
 
38
38
  def assert_glue(obj, fragment)
39
- assert_kind_of(HexaPDF::Layout::TextBox::Glue, obj)
39
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Glue, obj)
40
40
  assert_same(fragment.style, obj.item.style)
41
41
  end
42
42
 
43
43
  def assert_penalty(obj, penalty, item = nil)
44
- assert_kind_of(HexaPDF::Layout::TextBox::Penalty, obj)
44
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Penalty, obj)
45
45
  assert_equal(penalty, obj.penalty)
46
46
  if item
47
47
  assert_same(item.style, obj.item.style)
@@ -50,13 +50,13 @@ module TestTextBoxHelpers
50
50
  end
51
51
  end
52
52
 
53
- describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
54
- include TestTextBoxHelpers
53
+ describe HexaPDF::Layout::TextLayouter::SimpleTextSegmentation do
54
+ include TestTextLayouterHelpers
55
55
 
56
56
  before do
57
57
  @doc = HexaPDF::Document.new
58
- @font = @doc.fonts.load("Times")
59
- @obj = HexaPDF::Layout::TextBox::SimpleTextSegmentation
58
+ @font = @doc.fonts.add("Times")
59
+ @obj = HexaPDF::Layout::TextLayouter::SimpleTextSegmentation
60
60
  end
61
61
 
62
62
  def setup_fragment(text, style = nil)
@@ -68,7 +68,7 @@ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
68
68
  end
69
69
 
70
70
  it "handles InlineBox objects" do
71
- input = HexaPDF::Layout::InlineBox.new(10, 10) { }
71
+ input = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) { }
72
72
  result = @obj.call([input, input])
73
73
  assert_equal(2, result.size)
74
74
  assert_box(result[0], input)
@@ -103,13 +103,16 @@ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
103
103
  end
104
104
 
105
105
  it "insert a mandatory break when an Unicode line boundary characters is encountered" do
106
- frag = setup_fragment("A\rB\r\nC\nD\vE\fF\u{85}G\u{2028}H\u{2029}I")
106
+ frag = setup_fragment("A\rB\r\nC\nD\vE\fF\u{85}G\u{2029}H\u{2028}I")
107
107
 
108
108
  result = @obj.call([frag])
109
109
  assert_equal(17, result.size)
110
- [1, 3, 5, 7, 9, 11, 13, 15].each do |index|
111
- assert_penalty(result[index], HexaPDF::Layout::TextBox::Penalty::MandatoryBreak.penalty)
110
+ [1, 3, 5, 7, 9, 11, 13].each do |index|
111
+ assert_penalty(result[index],
112
+ HexaPDF::Layout::TextLayouter::Penalty::MandatoryParagraphBreak.penalty)
112
113
  end
114
+ assert_penalty(result[15],
115
+ HexaPDF::Layout::TextLayouter::Penalty::MandatoryLineBreak.penalty)
113
116
  end
114
117
 
115
118
  it "insert a standard penalty after a hyphen" do
@@ -118,7 +121,7 @@ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
118
121
  result = @obj.call([frag])
119
122
  assert_equal(12, result.size)
120
123
  [1, 3, 5, 9].each do |index|
121
- assert_penalty(result[index], HexaPDF::Layout::TextBox::Penalty::Standard.penalty)
124
+ assert_penalty(result[index], HexaPDF::Layout::TextLayouter::Penalty::Standard.penalty)
122
125
  end
123
126
  end
124
127
 
@@ -137,7 +140,7 @@ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
137
140
 
138
141
  result = @obj.call([frag])
139
142
  assert_equal(3, result.size)
140
- assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::Standard.penalty, hyphen)
143
+ assert_penalty(result[1], HexaPDF::Layout::TextLayouter::Penalty::Standard.penalty, hyphen)
141
144
  end
142
145
 
143
146
  it "insert a prohibited break penalty for non-breaking spaces" do
@@ -146,16 +149,17 @@ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
146
149
 
147
150
  result = @obj.call([frag])
148
151
  assert_equal(3, result.size)
149
- assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::ProhibitedBreak.penalty, space)
152
+ assert_penalty(result[1], HexaPDF::Layout::TextLayouter::Penalty::ProhibitedBreak.penalty, space)
150
153
  end
151
154
  end
152
155
 
153
156
  # Common tests for fixed and variable width line wrapping. The including class needs to define a
154
- # #call(items, width = 100) method with a default with of 100.
157
+ # #call(items, width = 100) method with a default with of 100. The optional block is called after a
158
+ # line has been yielded by the line wrapping algorithm.
155
159
  module CommonLineWrappingTests
156
160
  extend Minitest::Spec::DSL
157
161
 
158
- include TestTextBoxHelpers
162
+ include TestTextLayouterHelpers
159
163
 
160
164
  it "breaks before a box if it doesn't fit onto the line anymore" do
161
165
  rest, lines = call(boxes(25, 50, 25, 10))
@@ -226,7 +230,7 @@ module CommonLineWrappingTests
226
230
  end
227
231
 
228
232
  it "handles breaking at penalties with non-zero width if they fit on the line" do
229
- item = HexaPDF::Layout::InlineBox.new(20, 0) {}
233
+ item = HexaPDF::Layout::InlineBox.create(width: 20) {}
230
234
  rest, lines = call(boxes(20, 60, 30).insert(1, penalty(0, item)).insert(-2, penalty(0, item)))
231
235
  assert(rest.empty?)
232
236
  assert_equal(2, lines.count)
@@ -236,7 +240,7 @@ module CommonLineWrappingTests
236
240
  end
237
241
 
238
242
  it "handles penalties with non-zero width if they don't fit on the line" do
239
- item = HexaPDF::Layout::InlineBox.new(20, 0) {}
243
+ item = HexaPDF::Layout::InlineBox.create(width: 20) {}
240
244
  rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(0, item)] + boxes(30))
241
245
  assert(rest.empty?)
242
246
  assert_equal(2, lines.count)
@@ -245,7 +249,7 @@ module CommonLineWrappingTests
245
249
  end
246
250
 
247
251
  it "handles breaking at prohibited breakpoints by back-tracking to the last valid breakpoint " do
248
- item = HexaPDF::Layout::InlineBox.new(20, 0) {}
252
+ item = HexaPDF::Layout::InlineBox.create(width: 20) {}
249
253
  rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
250
254
  assert(rest.empty?)
251
255
  assert_equal(2, lines.count)
@@ -254,8 +258,8 @@ module CommonLineWrappingTests
254
258
  end
255
259
 
256
260
  it "stops when nil is returned by the block: last item is a box" do
257
- lines = []
258
- rest = @obj.call(boxes(20, 20, 20), 20) {|line| lines << line; lines.count > 1 ? nil : true}
261
+ done = false
262
+ rest, lines = call(boxes(20, 20, 20), 20) { done ? nil : done = true }
259
263
  assert_equal(2, rest.count)
260
264
  assert_equal(2, lines.count)
261
265
  end
@@ -263,37 +267,38 @@ module CommonLineWrappingTests
263
267
  it "stops when nil is returned by the block: last item is a glue" do
264
268
  done = false
265
269
  items = boxes(20, 15, 20).insert(-2, glue(10))
266
- rest = @obj.call(items, 20) { done ? nil : (done = true; 20) }
270
+ rest, = call(items, 20) { done ? nil : done = true }
267
271
  assert_equal(3, rest.count)
268
272
  assert_equal(15, rest[0].width)
269
273
  end
270
274
 
271
275
  it "stops when nil is returned by the block: last item is a mandatory break penalty" do
272
276
  items = boxes(20, 20).insert(-2, penalty(-5000))
273
- rest = @obj.call(items, 20) { nil }
277
+ rest, = call(items, 20) { nil }
274
278
  assert_equal(3, rest.count)
275
279
  end
276
280
 
277
281
  it "stops when nil is returned by the block: works for the last line" do
278
- lines = []
279
- rest = @obj.call(boxes(20, 20), 20) {|line| lines << line; lines.count > 1 ? nil : true}
282
+ done = false
283
+ rest, lines = call(boxes(20, 20), 20) { done ? nil : done = true }
280
284
  assert_equal(1, rest.count)
281
285
  assert_equal(2, lines.count)
282
286
  end
283
287
 
284
288
  end
285
289
 
286
- describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
290
+ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
287
291
  before do
288
- @obj = HexaPDF::Layout::TextBox::SimpleLineWrapping
292
+ @obj = HexaPDF::Layout::TextLayouter::SimpleLineWrapping
289
293
  end
290
294
 
291
295
  describe "fixed width wrapping" do
292
296
  include CommonLineWrappingTests
293
297
 
294
- def call(items, width = 100)
298
+ def call(items, width = 100, &block)
295
299
  lines = []
296
- rest = @obj.call(items, width) {|line, _| lines << line; true }
300
+ block ||= proc { true }
301
+ rest = @obj.call(items, proc { width }) {|line, item| lines << line; block.call(line, item) }
297
302
  [rest, lines]
298
303
  end
299
304
  end
@@ -301,9 +306,10 @@ describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
301
306
  describe "variable width wrapping" do
302
307
  include CommonLineWrappingTests
303
308
 
304
- def call(items, width = proc { 100 })
309
+ def call(items, width = 100, &block)
305
310
  lines = []
306
- rest = @obj.call(items, width) {|line, _| lines << line; true }
311
+ block ||= proc { true }
312
+ rest = @obj.call(items, proc {|_| width }) {|line, i| lines << line; block.call(line, i) }
307
313
  [rest, lines]
308
314
  end
309
315
 
@@ -339,7 +345,7 @@ describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
339
345
  end
340
346
  end
341
347
  lines = []
342
- item = HexaPDF::Layout::InlineBox.new(20, 10) {}
348
+ item = HexaPDF::Layout::InlineBox.create(width: 20, height: 10) {}
343
349
  items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
344
350
  rest = @obj.call(items, width_block) do |line|
345
351
  height += line.height
@@ -355,93 +361,121 @@ describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
355
361
  end
356
362
  end
357
363
 
358
- describe HexaPDF::Layout::TextBox do
359
- include TestTextBoxHelpers
364
+ describe HexaPDF::Layout::TextLayouter do
365
+ include TestTextLayouterHelpers
360
366
 
361
367
  before do
362
368
  @doc = HexaPDF::Document.new
363
- @font = @doc.fonts.load("Times")
369
+ @font = @doc.fonts.add("Times")
364
370
  @style = HexaPDF::Layout::Style.new(font: @font)
365
371
  end
366
372
 
367
373
  it "creates an instance from text and options" do
368
- box = HexaPDF::Layout::TextBox.create("T", font: @font, width: 100, height: 100)
369
- assert_equal(1, box.items.length)
370
- assert_equal(@font.decode_utf8("T"), box.items[0].item.items)
374
+ layouter = HexaPDF::Layout::TextLayouter.create("T", font: @font, width: 100, height: 100)
375
+ assert_equal(1, layouter.items.length)
376
+ assert_equal(@font.decode_utf8("T"), layouter.items[0].item.items)
377
+ end
378
+
379
+ describe "initialize" do
380
+ it "can use a Style object" do
381
+ style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
382
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [], width: 10, style: style)
383
+ assert_equal(20, layouter.style.font_size)
384
+ end
385
+
386
+ it "can use a style options" do
387
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [], width: 10, style:
388
+ {font: @font, font_size: 20})
389
+ assert_equal(20, layouter.style.font_size)
390
+ end
371
391
  end
372
392
 
373
393
  it "doesn't run the text segmentation algorithm on already segmented items" do
374
- item = HexaPDF::Layout::InlineBox.new(20, 0) {}
375
- box = HexaPDF::Layout::TextBox.new(items: [item], width: 100, height: 100)
376
- items = box.items
394
+ item = HexaPDF::Layout::InlineBox.create(width: 20) {}
395
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [item], width: 100, height: 100)
396
+ items = layouter.items
377
397
  assert_equal(1, items.length)
378
398
  assert_box(items[0], item)
379
399
 
380
- box.items = items
381
- assert_same(items, box.items)
400
+ layouter.items = items
401
+ assert_same(items, layouter.items)
382
402
  end
383
403
 
384
404
  describe "fit" do
385
405
  it "handles text indentation" do
386
- box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [20, 20], [20, 20]), width: 60,
387
- style: @style)
388
- box.style.text_indent = 20
389
- rest, height = box.fit
390
- assert_equal(60, box.lines[0].width)
391
- assert_equal(20, box.lines[1].width)
392
- assert(rest.empty?)
393
- assert_equal(40, height)
406
+ items = boxes([20, 20], [20, 20], [20, 20]) +
407
+ [HexaPDF::Layout::TextLayouter::Penalty::MandatoryParagraphBreak] +
408
+ boxes([40, 20]) + [glue(20)] +
409
+ boxes(*([[20, 20]] * 4)) + [HexaPDF::Layout::TextLayouter::Penalty::MandatoryLineBreak] +
410
+ boxes(*([[20, 20]] * 4))
411
+ @style.text_indent = 20
412
+
413
+ [60, proc { 60 }].each do |width|
414
+ layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: width, style: @style)
415
+ rest, reason = layouter.fit
416
+ assert_equal([40, 20, 40, 60, 20, 60, 20], layouter.lines.map(&:width))
417
+ assert_equal([20, 0, 20, 0, 0, 0, 0], layouter.lines.map(&:x_offset))
418
+ assert(rest.empty?)
419
+ assert_equal(:success, reason)
420
+ end
394
421
  end
395
422
 
396
423
  it "fits using unlimited height" do
397
- box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 100)), width: 20,
398
- style: @style)
399
- rest, height = box.fit
424
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 100)), width: 20,
425
+ style: @style)
426
+ rest, reason = layouter.fit
400
427
  assert(rest.empty?)
401
- assert_equal(20 * 100, height)
428
+ assert_equal(:success, reason)
429
+ assert_equal(20 * 100, layouter.actual_height)
402
430
  end
403
431
 
404
432
  it "fits using a limited height" do
405
- box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 100)), width: 20, height: 100,
406
- style: @style)
407
- rest, height = box.fit
433
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 100)), width: 20,
434
+ height: 100, style: @style)
435
+ rest, reason = layouter.fit
408
436
  assert_equal(95, rest.count)
409
- assert_equal(100, height)
437
+ assert_equal(:height, reason)
438
+ assert_equal(100, layouter.actual_height)
410
439
  end
411
440
 
412
441
  it "takes line spacing into account when calculating the height" do
413
- box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 20]] * 5)), width: 20, style: @style)
414
- box.style.line_spacing = :double
415
- rest, height = box.fit
442
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 5)), width: 20,
443
+ style: @style)
444
+ layouter.style.line_spacing = :double
445
+ rest, reason = layouter.fit
416
446
  assert(rest.empty?)
417
- assert_equal(20 * (5 + 4), height)
447
+ assert_equal(:success, reason)
448
+ assert_equal(20 * (5 + 4), layouter.actual_height)
418
449
  end
419
450
 
420
451
  it "handles empty lines" do
421
452
  items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
422
453
  boxes([20, 20]) + [penalty(-5000)] * 2
423
- box = HexaPDF::Layout::TextBox.new(items: items, width: 30, style: @style)
424
- rest, height = box.fit
454
+ layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: 30, style: @style)
455
+ rest, reason = layouter.fit
425
456
  assert(rest.empty?)
426
- assert_equal(5, box.lines.count)
427
- assert_equal(20 + 20 + 9 + 20 + 9, height)
457
+ assert_equal(:success, reason)
458
+ assert_equal(5, layouter.lines.count)
459
+ assert_equal(20 + 20 + 9 + 20 + 9, layouter.actual_height)
428
460
  end
429
461
 
430
462
  describe "fixed width" do
431
463
  it "stops if an item is wider than the available width, with unlimited height" do
432
- box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [50, 20]), width: 30,
433
- style: @style)
434
- rest, height = box.fit
464
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 20], [50, 20]), width: 30,
465
+ style: @style)
466
+ rest, reason = layouter.fit
435
467
  assert_equal(1, rest.count)
436
- assert_equal(20, height)
468
+ assert_equal(:box, reason)
469
+ assert_equal(20, layouter.actual_height)
437
470
  end
438
471
 
439
- it "stops if an item is wider than the available width, with limited height" do
440
- box = HexaPDF::Layout::TextBox.new(items: boxes([20, 20], [50, 20]), width: 30, height: 100,
441
- style: @style)
442
- rest, height = box.fit
472
+ it "stops if a box item is wider than the available width, with limited height" do
473
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 20], [50, 20]), width: 30,
474
+ height: 100, style: @style)
475
+ rest, reason = layouter.fit
443
476
  assert_equal(1, rest.count)
444
- assert_equal(20, height)
477
+ assert_equal(:box, reason)
478
+ assert_equal(20, layouter.actual_height)
445
479
  end
446
480
  end
447
481
 
@@ -453,13 +487,14 @@ describe HexaPDF::Layout::TextBox do
453
487
  else 40
454
488
  end
455
489
  end
456
- box = HexaPDF::Layout::TextBox.new(items: boxes([20, 18]), width: width_block,
457
- height: 100, style: @style)
458
- rest, height = box.fit
490
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 18]), width: width_block,
491
+ height: 100, style: @style)
492
+ rest, reason = layouter.fit
459
493
  assert(rest.empty?)
460
- assert_equal(1, box.lines.count)
461
- assert_equal(24, box.lines[0].y_offset)
462
- assert_equal(42, height)
494
+ assert_equal(:success, reason)
495
+ assert_equal(1, layouter.lines.count)
496
+ assert_equal(24, layouter.lines[0].y_offset)
497
+ assert_equal(42, layouter.actual_height)
463
498
  end
464
499
 
465
500
  it "searches for a vertical offset if an item is wider than the available width" do
@@ -470,52 +505,75 @@ describe HexaPDF::Layout::TextBox do
470
505
  40
471
506
  end
472
507
  end
473
- box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 18]] * 7)), width: width_block,
474
- height: 100, style: @style)
475
- rest, height = box.fit
508
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 18]] * 7)),
509
+ width: width_block, height: 100, style: @style)
510
+ rest, reason = layouter.fit
476
511
  assert_equal(1, rest.count)
477
- assert_equal(3, box.lines.count)
478
- assert_equal(0, box.lines[0].y_offset)
479
- assert_equal(18, box.lines[1].y_offset)
480
- assert_equal(48, box.lines[2].y_offset)
481
- assert_equal(84, height)
512
+ assert_equal(:height, reason)
513
+ assert_equal(3, layouter.lines.count)
514
+ assert_equal(0, layouter.lines[0].y_offset)
515
+ assert_equal(18, layouter.lines[1].y_offset)
516
+ assert_equal(48, layouter.lines[2].y_offset)
517
+ assert_equal(84, layouter.actual_height)
518
+ end
519
+ end
520
+
521
+ it "breaks a text fragment into parts if it is wider than the available width" do
522
+ [100, nil].each do |height|
523
+ str = " Thisisaverylongstring"
524
+ frag = HexaPDF::Layout::TextFragment.create(str, font: @font)
525
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [frag], width: 20, height: height,
526
+ style: @style)
527
+ rest, reason = layouter.fit
528
+ assert(rest.empty?)
529
+ assert_equal(:success, reason)
530
+ assert_equal(str.strip.length, layouter.lines.sum {|l| l.items.sum {|i| i.items.count}})
531
+ assert_equal(45, layouter.actual_height)
532
+
533
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [frag], width: 1, height: height,
534
+ style: @style)
535
+ rest, reason = layouter.fit
536
+ assert_equal(str.strip.length, rest.count)
537
+ assert_equal(:box, reason)
482
538
  end
483
539
  end
484
540
 
485
541
  it "post-processes lines for justification if needed" do
486
542
  frag10 = HexaPDF::Layout::TextFragment.create(" ", font: @font)
487
543
  frag10.items.freeze
488
- frag10b = HexaPDF::Layout::TextBox::Box.new(frag10)
544
+ frag10b = HexaPDF::Layout::TextLayouter::Box.new(frag10)
489
545
  frag20 = HexaPDF::Layout::TextFragment.create(" ", font: @font, font_size: 20)
490
- frag20b = HexaPDF::Layout::TextBox::Box.new(frag20)
546
+ frag20b = HexaPDF::Layout::TextLayouter::Box.new(frag20)
491
547
  items = boxes(20, 20, 20, 20, 30).insert(1, frag10b).insert(3, frag20b).insert(5, frag10b)
492
548
  # Width of spaces: 2.5 * 2 + 5 = 10 (from AFM file, adjusted for font size)
493
549
  # Line width: 20 * 4 + width_of_spaces = 90
494
550
  # Missing width: 100 - 90 = 10
495
551
  # -> Each space must be doubled!
496
552
 
497
- box = HexaPDF::Layout::TextBox.new(items: items, width: 100)
498
- box.style.align = :justify
499
- rest, _height = box.fit
553
+ layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: 100)
554
+ layouter.style.align = :justify
555
+ rest, reason = layouter.fit
500
556
  assert(rest.empty?)
501
- assert_equal(9, box.lines[0].items.count)
502
- assert_in_delta(100, box.lines[0].width)
503
- assert_equal(-250, box.lines[0].items[1].items[0])
504
- assert_equal(-250, box.lines[0].items[4].items[0])
505
- assert_equal(-250, box.lines[0].items[6].items[0])
506
- assert_equal(30, box.lines[1].width)
557
+ assert_equal(:success, reason)
558
+ assert_equal(9, layouter.lines[0].items.count)
559
+ assert_in_delta(100, layouter.lines[0].width)
560
+ assert_equal(-250, layouter.lines[0].items[1].items[0])
561
+ assert_equal(-250, layouter.lines[0].items[4].items[0])
562
+ assert_equal(-250, layouter.lines[0].items[6].items[0])
563
+ assert_equal(30, layouter.lines[1].width)
507
564
  end
508
565
 
509
566
  it "applies the optional horizontal offsets if set" do
510
567
  x_offsets = lambda {|height, line_height| height + line_height}
511
- box = HexaPDF::Layout::TextBox.new(items: boxes(*([[20, 10]] * 7)), width: 60,
512
- x_offsets: x_offsets, height: 100, style: @style)
513
- rest, height = box.fit
568
+ layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 10]] * 7)), width: 60,
569
+ x_offsets: x_offsets, height: 100, style: @style)
570
+ rest, reason = layouter.fit
514
571
  assert(rest.empty?)
515
- assert_equal(30, height)
516
- assert_equal(10, box.lines[0].x_offset)
517
- assert_equal(20, box.lines[1].x_offset)
518
- assert_equal(30, box.lines[2].x_offset)
572
+ assert_equal(:success, reason)
573
+ assert_equal(30, layouter.actual_height)
574
+ assert_equal(10, layouter.lines[0].x_offset)
575
+ assert_equal(20, layouter.lines[1].x_offset)
576
+ assert_equal(30, layouter.lines[2].x_offset)
519
577
  end
520
578
  end
521
579
 
@@ -535,7 +593,7 @@ describe HexaPDF::Layout::TextBox do
535
593
  @frag = HexaPDF::Layout::TextFragment.create("This is some more text.\n" \
536
594
  "This is some more text.", font: @font)
537
595
  @width = HexaPDF::Layout::TextFragment.create("This is some ", font: @font).width
538
- @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width)
596
+ @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width)
539
597
  @canvas = @doc.pages.add.canvas
540
598
 
541
599
  @line1w = HexaPDF::Layout::TextFragment.create("This is some", font: @font).width
@@ -544,8 +602,8 @@ describe HexaPDF::Layout::TextBox do
544
602
 
545
603
  it "can horizontally align the contents to the left" do
546
604
  top = 100
547
- @box.style.align = :left
548
- @box.draw(@canvas, 5, top)
605
+ @layouter.style.align = :left
606
+ @layouter.draw(@canvas, 5, top)
549
607
  assert_positions(@canvas.contents,
550
608
  [[5, top - @frag.y_max],
551
609
  [5, top - @frag.y_max - @frag.height],
@@ -555,8 +613,8 @@ describe HexaPDF::Layout::TextBox do
555
613
 
556
614
  it "can horizontally align the contents to the center" do
557
615
  top = 100
558
- @box.style.align = :center
559
- @box.draw(@canvas, 5, top)
616
+ @layouter.style.align = :center
617
+ @layouter.draw(@canvas, 5, top)
560
618
  assert_positions(@canvas.contents,
561
619
  [[5 + (@width - @line1w) / 2, top - @frag.y_max],
562
620
  [5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height],
@@ -566,8 +624,8 @@ describe HexaPDF::Layout::TextBox do
566
624
 
567
625
  it "can horizontally align the contents to the right" do
568
626
  top = 100
569
- @box.style.align = :right
570
- @box.draw(@canvas, 5, top)
627
+ @layouter.style.align = :right
628
+ @layouter.draw(@canvas, 5, top)
571
629
  assert_positions(@canvas.contents,
572
630
  [[5 + @width - @line1w, top - @frag.y_max],
573
631
  [5 + @width - @line2w, top - @frag.y_max - @frag.height],
@@ -577,32 +635,32 @@ describe HexaPDF::Layout::TextBox do
577
635
 
578
636
  it "can justify the contents" do
579
637
  top = 100
580
- @box.style.align = :justify
581
- @box.draw(@canvas, 5, top)
638
+ @layouter.style.align = :justify
639
+ @layouter.draw(@canvas, 5, top)
582
640
  assert_positions(@canvas.contents,
583
641
  [[5, top - @frag.y_max],
584
642
  [5, top - @frag.y_max - @frag.height],
585
643
  [5, top - @frag.y_max - @frag.height * 2],
586
644
  [5, top - @frag.y_max - @frag.height * 3]])
587
- assert_in_delta(@width, @box.lines[0].width, 0.0001)
588
- assert_in_delta(@width, @box.lines[2].width, 0.0001)
645
+ assert_in_delta(@width, @layouter.lines[0].width, 0.0001)
646
+ assert_in_delta(@width, @layouter.lines[2].width, 0.0001)
589
647
  end
590
648
 
591
649
  it "doesn't justify lines ending in a mandatory break or the last line" do
592
- @box.style.align = :justify
593
- @box.draw(@canvas, 5, 100)
594
- assert_equal(@line2w, @box.lines[1].width, 0.0001)
595
- assert_equal(@line2w, @box.lines[3].width, 0.0001)
650
+ @layouter.style.align = :justify
651
+ @layouter.draw(@canvas, 5, 100)
652
+ assert_equal(@line2w, @layouter.lines[1].width, 0.0001)
653
+ assert_equal(@line2w, @layouter.lines[3].width, 0.0001)
596
654
  end
597
655
 
598
656
  it "can vertically align the contents in the center" do
599
657
  top = 100
600
- @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
601
- @box.style.valign = :center
658
+ @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width, height: top)
659
+ @layouter.style.valign = :center
602
660
 
603
- _, height = @box.fit
604
- initial_baseline = top - ((top - height) / 2) - @frag.y_max
605
- @box.draw(@canvas, 5, top)
661
+ @layouter.fit
662
+ initial_baseline = top - ((top - @layouter.actual_height) / 2) - @frag.y_max
663
+ @layouter.draw(@canvas, 5, top)
606
664
  assert_positions(@canvas.contents,
607
665
  [[5, initial_baseline],
608
666
  [5, initial_baseline - @frag.height],
@@ -612,12 +670,12 @@ describe HexaPDF::Layout::TextBox do
612
670
 
613
671
  it "can vertically align the contents to the bottom" do
614
672
  top = 100
615
- @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
616
- @box.style.valign = :bottom
673
+ @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width, height: top)
674
+ @layouter.style.valign = :bottom
617
675
 
618
- _, height = @box.fit
619
- initial_baseline = height - @frag.y_max
620
- @box.draw(@canvas, 5, top)
676
+ @layouter.fit
677
+ initial_baseline = @layouter.actual_height - @frag.y_max
678
+ @layouter.draw(@canvas, 5, top)
621
679
  assert_positions(@canvas.contents,
622
680
  [[5, initial_baseline],
623
681
  [5, initial_baseline - @frag.height],
@@ -626,15 +684,37 @@ describe HexaPDF::Layout::TextBox do
626
684
  end
627
685
 
628
686
  it "raises an error if vertical alignment is :center/:bottom and an unlimited height is used" do
629
- @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width)
687
+ @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width)
630
688
  assert_raises(HexaPDF::Error) do
631
- @box.style.valign = :center
632
- @box.draw(@canvas, 0, 0)
689
+ @layouter.style.valign = :center
690
+ @layouter.draw(@canvas, 0, 0)
633
691
  end
634
692
  assert_raises(HexaPDF::Error) do
635
- @box.style.valign = :bottom
636
- @box.draw(@canvas, 0, 0)
693
+ @layouter.style.valign = :bottom
694
+ @layouter.draw(@canvas, 0, 0)
637
695
  end
638
696
  end
697
+
698
+ it "makes sure that text fragments don't pollute the graphics state for inline boxes" do
699
+ frag = HexaPDF::Layout::TextFragment.create("Demo", font: @font)
700
+ inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {|c, _| c.text("A")}
701
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [frag, inline_box], width: 200)
702
+ assert_raises(HexaPDF::Error) { layouter.draw(@canvas, 0, 0) }
703
+ end
704
+
705
+ it "doesn't do unnecessary work for placeholder boxes" do
706
+ box1 = HexaPDF::Layout::InlineBox.create(width: 10, height: 20)
707
+ box2 = HexaPDF::Layout::InlineBox.create(width: 30, height: 40) { @canvas.line_width(2) }
708
+ layouter = HexaPDF::Layout::TextLayouter.new(items: [box1, box2], width: 200)
709
+ layouter.draw(@canvas, 0, 0)
710
+ assert_operators(@canvas.contents, [[:save_graphics_state],
711
+ [:restore_graphics_state],
712
+ [:save_graphics_state],
713
+ [:concatenate_matrix, [1, 0, 0, 1, 10, -40]],
714
+ [:set_line_width, [2]],
715
+ [:restore_graphics_state],
716
+ [:save_graphics_state],
717
+ [:restore_graphics_state]])
718
+ end
639
719
  end
640
720
  end