hexapdf 0.5.0 → 0.6.0

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