hexapdf 0.4.0 → 0.5.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/CONTRIBUTERS +1 -1
  4. data/README.md +5 -5
  5. data/VERSION +1 -1
  6. data/examples/emoji-smile.png +0 -0
  7. data/examples/emoji-wink.png +0 -0
  8. data/examples/graphics.rb +9 -8
  9. data/examples/standard_pdf_fonts.rb +2 -1
  10. data/examples/text_box_alignment.rb +47 -0
  11. data/examples/text_box_inline_boxes.rb +56 -0
  12. data/examples/text_box_line_wrapping.rb +57 -0
  13. data/examples/text_box_shapes.rb +166 -0
  14. data/examples/text_box_styling.rb +72 -0
  15. data/examples/truetype.rb +3 -4
  16. data/lib/hexapdf/cli/optimize.rb +2 -2
  17. data/lib/hexapdf/configuration.rb +8 -6
  18. data/lib/hexapdf/content/canvas.rb +8 -5
  19. data/lib/hexapdf/content/parser.rb +3 -2
  20. data/lib/hexapdf/content/processor.rb +14 -3
  21. data/lib/hexapdf/document.rb +1 -0
  22. data/lib/hexapdf/document/fonts.rb +2 -1
  23. data/lib/hexapdf/document/pages.rb +23 -0
  24. data/lib/hexapdf/font/invalid_glyph.rb +78 -0
  25. data/lib/hexapdf/font/true_type/font.rb +14 -3
  26. data/lib/hexapdf/font/true_type/table.rb +1 -0
  27. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  28. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -0
  29. data/lib/hexapdf/font/true_type/table/glyf.rb +4 -0
  30. data/lib/hexapdf/font/true_type/table/kern.rb +170 -0
  31. data/lib/hexapdf/font/true_type/table/post.rb +5 -1
  32. data/lib/hexapdf/font/true_type_wrapper.rb +71 -24
  33. data/lib/hexapdf/font/type1/afm_parser.rb +3 -2
  34. data/lib/hexapdf/font/type1/character_metrics.rb +0 -9
  35. data/lib/hexapdf/font/type1/font.rb +11 -0
  36. data/lib/hexapdf/font/type1/font_metrics.rb +6 -1
  37. data/lib/hexapdf/font/type1_wrapper.rb +51 -7
  38. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  39. data/lib/hexapdf/layout.rb +51 -0
  40. data/lib/hexapdf/layout/inline_box.rb +95 -0
  41. data/lib/hexapdf/layout/line_fragment.rb +333 -0
  42. data/lib/hexapdf/layout/numeric_refinements.rb +56 -0
  43. data/lib/hexapdf/layout/style.rb +365 -0
  44. data/lib/hexapdf/layout/text_box.rb +727 -0
  45. data/lib/hexapdf/layout/text_fragment.rb +206 -0
  46. data/lib/hexapdf/layout/text_shaper.rb +155 -0
  47. data/lib/hexapdf/task.rb +0 -1
  48. data/lib/hexapdf/task/dereference.rb +1 -1
  49. data/lib/hexapdf/tokenizer.rb +3 -2
  50. data/lib/hexapdf/type/font_descriptor.rb +2 -1
  51. data/lib/hexapdf/type/font_type0.rb +3 -1
  52. data/lib/hexapdf/type/form.rb +12 -4
  53. data/lib/hexapdf/version.rb +1 -1
  54. data/test/hexapdf/common_tokenizer_tests.rb +7 -0
  55. data/test/hexapdf/content/common.rb +8 -0
  56. data/test/hexapdf/content/test_canvas.rb +10 -22
  57. data/test/hexapdf/content/test_processor.rb +4 -1
  58. data/test/hexapdf/document/test_pages.rb +16 -0
  59. data/test/hexapdf/font/test_invalid_glyph.rb +34 -0
  60. data/test/hexapdf/font/test_true_type_wrapper.rb +25 -11
  61. data/test/hexapdf/font/test_type1_wrapper.rb +26 -10
  62. data/test/hexapdf/font/true_type/table/common.rb +27 -0
  63. data/test/hexapdf/font/true_type/table/test_cmap.rb +14 -20
  64. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +7 -0
  65. data/test/hexapdf/font/true_type/table/test_glyf.rb +8 -6
  66. data/test/hexapdf/font/true_type/table/test_head.rb +9 -13
  67. data/test/hexapdf/font/true_type/table/test_hhea.rb +16 -23
  68. data/test/hexapdf/font/true_type/table/test_hmtx.rb +4 -7
  69. data/test/hexapdf/font/true_type/table/test_kern.rb +61 -0
  70. data/test/hexapdf/font/true_type/table/test_loca.rb +7 -13
  71. data/test/hexapdf/font/true_type/table/test_maxp.rb +4 -9
  72. data/test/hexapdf/font/true_type/table/test_name.rb +14 -17
  73. data/test/hexapdf/font/true_type/table/test_os2.rb +3 -5
  74. data/test/hexapdf/font/true_type/table/test_post.rb +21 -19
  75. data/test/hexapdf/font/true_type/test_font.rb +4 -0
  76. data/test/hexapdf/font/type1/common.rb +6 -0
  77. data/test/hexapdf/font/type1/test_afm_parser.rb +9 -0
  78. data/test/hexapdf/font/type1/test_font.rb +6 -0
  79. data/test/hexapdf/layout/test_inline_box.rb +40 -0
  80. data/test/hexapdf/layout/test_line_fragment.rb +206 -0
  81. data/test/hexapdf/layout/test_style.rb +143 -0
  82. data/test/hexapdf/layout/test_text_box.rb +640 -0
  83. data/test/hexapdf/layout/test_text_fragment.rb +208 -0
  84. data/test/hexapdf/layout/test_text_shaper.rb +64 -0
  85. data/test/hexapdf/task/test_dereference.rb +1 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/test_font_descriptor.rb +4 -2
  88. data/test/hexapdf/type/test_font_type0.rb +7 -0
  89. data/test/hexapdf/type/test_form.rb +12 -0
  90. metadata +29 -2
@@ -0,0 +1,640 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/layout'
5
+ require 'hexapdf/document'
6
+ require_relative "../content/common"
7
+
8
+ module TestTextBoxHelpers
9
+ def boxes(*dims)
10
+ dims.map do |width, height|
11
+ box = HexaPDF::Layout::InlineBox.new(width, height || 0) {}
12
+ HexaPDF::Layout::TextBox::Box.new(box)
13
+ end
14
+ end
15
+
16
+ def glue(width)
17
+ HexaPDF::Layout::TextBox::Glue.new(HexaPDF::Layout::InlineBox.new(width, 0) {})
18
+ end
19
+
20
+ def penalty(penalty, item = nil)
21
+ if item
22
+ HexaPDF::Layout::TextBox::Penalty.new(penalty, item.width, item: item)
23
+ else
24
+ HexaPDF::Layout::TextBox::Penalty.new(penalty)
25
+ end
26
+ end
27
+
28
+ def assert_box(obj, item)
29
+ assert_kind_of(HexaPDF::Layout::TextBox::Box, obj)
30
+ if obj.item.kind_of?(HexaPDF::Layout::InlineBox)
31
+ assert_same(item, obj.item)
32
+ else
33
+ assert_same(item.style, obj.item.style)
34
+ assert_equal(item.items, obj.item.items)
35
+ end
36
+ end
37
+
38
+ def assert_glue(obj, fragment)
39
+ assert_kind_of(HexaPDF::Layout::TextBox::Glue, obj)
40
+ assert_same(fragment.style, obj.item.style)
41
+ end
42
+
43
+ def assert_penalty(obj, penalty, item = nil)
44
+ assert_kind_of(HexaPDF::Layout::TextBox::Penalty, obj)
45
+ assert_equal(penalty, obj.penalty)
46
+ if item
47
+ assert_same(item.style, obj.item.style)
48
+ assert_equal(item.items, obj.item.items)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe HexaPDF::Layout::TextBox::SimpleTextSegmentation do
54
+ include TestTextBoxHelpers
55
+
56
+ before do
57
+ @doc = HexaPDF::Document.new
58
+ @font = @doc.fonts.load("Times")
59
+ @obj = HexaPDF::Layout::TextBox::SimpleTextSegmentation
60
+ end
61
+
62
+ def setup_fragment(text, style = nil)
63
+ if style
64
+ HexaPDF::Layout::TextFragment.new(items: style.font.decode_utf8(text), style: style)
65
+ else
66
+ HexaPDF::Layout::TextFragment.create(text, font: @font)
67
+ end
68
+ end
69
+
70
+ it "handles InlineBox objects" do
71
+ input = HexaPDF::Layout::InlineBox.new(10, 10) { }
72
+ result = @obj.call([input, input])
73
+ assert_equal(2, result.size)
74
+ assert_box(result[0], input)
75
+ assert_box(result[1], input)
76
+ end
77
+
78
+ it "handles plain text" do
79
+ frag = setup_fragment("Testtext")
80
+ result = @obj.call([frag])
81
+ assert_equal(1, result.size)
82
+ assert_box(result[0], frag)
83
+ end
84
+
85
+ it "inserts a glue in places where spaces are" do
86
+ frag = setup_fragment("This is a test")
87
+ space = setup_fragment(" ", frag.style)
88
+
89
+ result = @obj.call([frag])
90
+ assert_equal(7, result.size)
91
+ assert_glue(result[1], space)
92
+ assert_glue(result[3], space)
93
+ assert_glue(result[5], space)
94
+ end
95
+
96
+ it "inserts a glue representing 8 spaces when a tab is encountered" do
97
+ frag = setup_fragment("This\ttest")
98
+ tab = setup_fragment(" " * 8, frag.style)
99
+
100
+ result = @obj.call([frag])
101
+ assert_equal(3, result.size)
102
+ assert_glue(result[1], tab)
103
+ end
104
+
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")
107
+
108
+ result = @obj.call([frag])
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)
112
+ end
113
+ end
114
+
115
+ it "insert a standard penalty after a hyphen" do
116
+ frag = setup_fragment("hy-phen-a-tion - cool!")
117
+
118
+ result = @obj.call([frag])
119
+ assert_equal(12, result.size)
120
+ [1, 3, 5, 9].each do |index|
121
+ assert_penalty(result[index], HexaPDF::Layout::TextBox::Penalty::Standard.penalty)
122
+ end
123
+ end
124
+
125
+ it "insert a neutral penalty in places where zero-width-spaces are" do
126
+ frag = setup_fragment("zero\u{200B}width\u{200B}space")
127
+
128
+ result = @obj.call([frag])
129
+ assert_equal(5, result.size)
130
+ assert_penalty(result[1], 0)
131
+ assert_penalty(result[3], 0)
132
+ end
133
+
134
+ it "insert a special penalty for soft-hyphens" do
135
+ frag = setup_fragment("soft\u{00AD}hyphened")
136
+ hyphen = setup_fragment("-", frag.style)
137
+
138
+ result = @obj.call([frag])
139
+ assert_equal(3, result.size)
140
+ assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::Standard.penalty, hyphen)
141
+ end
142
+
143
+ it "insert a prohibited break penalty for non-breaking spaces" do
144
+ frag = setup_fragment("soft\u{00A0}hyphened")
145
+ space = setup_fragment(" ", frag.style)
146
+
147
+ result = @obj.call([frag])
148
+ assert_equal(3, result.size)
149
+ assert_penalty(result[1], HexaPDF::Layout::TextBox::Penalty::ProhibitedBreak.penalty, space)
150
+ end
151
+ end
152
+
153
+ # 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.
155
+ module CommonLineWrappingTests
156
+ extend Minitest::Spec::DSL
157
+
158
+ include TestTextBoxHelpers
159
+
160
+ it "breaks before a box if it doesn't fit onto the line anymore" do
161
+ rest, lines = call(boxes(25, 50, 25, 10))
162
+ assert(rest.empty?)
163
+ assert_equal(2, lines.count)
164
+ lines.each {|line| line.items.each {|item| assert_kind_of(HexaPDF::Layout::InlineBox, item)}}
165
+ assert_equal(100, lines[0].width)
166
+ assert_equal(10, lines[1].width)
167
+ end
168
+
169
+ it "breaks at a glue and ignores it if it doesn't fit onto the line anymore" do
170
+ rest, lines = call(boxes(90, 20).insert(-2, glue(20)))
171
+ assert(rest.empty?)
172
+ assert_equal(2, lines.count)
173
+ assert_equal(90, lines[0].width)
174
+ assert_equal(20, lines[1].width)
175
+ end
176
+
177
+ it "handles spaces at the start of a line" do
178
+ rest, lines = call(boxes(25, 50).unshift(glue(15)))
179
+ assert(rest.empty?)
180
+ assert_equal(1, lines.count)
181
+ assert_equal(75, lines[0].width)
182
+ assert_equal(25, lines[0].items[0].width)
183
+ end
184
+
185
+ it "handles spaces at the end of a line" do
186
+ rest, lines = call(boxes(20, 50, 20).insert(-2, glue(10)).insert(-2, glue(10)))
187
+ assert(rest.empty?)
188
+ assert_equal(2, lines.count)
189
+ assert_equal(70, lines[0].width)
190
+ assert_equal(20, lines[1].width)
191
+ assert_equal(50, lines[0].items[-1].width)
192
+ end
193
+
194
+ it "handles spaces at the end of a line before a mandatory break" do
195
+ rest, lines = call(boxes(20, 50, 20).insert(-2, glue(10)).insert(-2, penalty(-5000)))
196
+ assert(rest.empty?)
197
+ assert_equal(2, lines.count)
198
+ assert_equal(70, lines[0].width)
199
+ assert_equal(20, lines[1].width)
200
+ assert_equal(50, lines[0].items[-1].width)
201
+ end
202
+
203
+ it "handles multiple glue items after another" do
204
+ rest, lines = call(boxes(20, 20, 50, 20).insert(1, glue(20)).insert(1, glue(20)))
205
+ assert(rest.empty?)
206
+ assert_equal(2, lines.count)
207
+ assert_equal(80, lines[0].width)
208
+ assert_equal(70, lines[1].width)
209
+ end
210
+
211
+ it "handles mandatory line breaks" do
212
+ rest, lines = call(boxes(20, 20).insert(-2, penalty(-5000)))
213
+ assert(rest.empty?)
214
+ assert_equal(2, lines.count)
215
+ assert_equal(20, lines[0].width)
216
+ assert_equal(20, lines[1].width)
217
+ assert(lines[0].ignore_justification?)
218
+ end
219
+
220
+ it "handles breaking at penalties with zero width" do
221
+ rest, lines = call(boxes(80, 10, 20).insert(1, penalty(0)).insert(-2, penalty(0)))
222
+ assert(rest.empty?)
223
+ assert_equal(2, lines.count)
224
+ assert_equal(90, lines[0].width)
225
+ assert_equal(20, lines[1].width)
226
+ end
227
+
228
+ it "handles breaking at penalties with non-zero width if they fit on the line" do
229
+ item = HexaPDF::Layout::InlineBox.new(20, 0) {}
230
+ rest, lines = call(boxes(20, 60, 30).insert(1, penalty(0, item)).insert(-2, penalty(0, item)))
231
+ assert(rest.empty?)
232
+ assert_equal(2, lines.count)
233
+ assert_equal(100, lines[0].width)
234
+ assert_equal(30, lines[1].width)
235
+ assert_same(item, lines[0].items[-1])
236
+ end
237
+
238
+ it "handles penalties with non-zero width if they don't fit on the line" do
239
+ item = HexaPDF::Layout::InlineBox.new(20, 0) {}
240
+ rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(0, item)] + boxes(30))
241
+ assert(rest.empty?)
242
+ assert_equal(2, lines.count)
243
+ assert_equal(70, lines[0].width)
244
+ assert_equal(40, lines[1].width)
245
+ end
246
+
247
+ it "handles breaking at prohibited breakpoints by back-tracking to the last valid breakpoint " do
248
+ item = HexaPDF::Layout::InlineBox.new(20, 0) {}
249
+ rest, lines = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
250
+ assert(rest.empty?)
251
+ assert_equal(2, lines.count)
252
+ assert_equal(70, lines[0].width)
253
+ assert_equal(60, lines[1].width)
254
+ end
255
+
256
+ 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}
259
+ assert_equal(2, rest.count)
260
+ assert_equal(2, lines.count)
261
+ end
262
+
263
+ it "stops when nil is returned by the block: last item is a glue" do
264
+ done = false
265
+ items = boxes(20, 15, 20).insert(-2, glue(10))
266
+ rest = @obj.call(items, 20) { done ? nil : (done = true; 20) }
267
+ assert_equal(3, rest.count)
268
+ assert_equal(15, rest[0].width)
269
+ end
270
+
271
+ it "stops when nil is returned by the block: last item is a mandatory break penalty" do
272
+ items = boxes(20, 20).insert(-2, penalty(-5000))
273
+ rest = @obj.call(items, 20) { nil }
274
+ assert_equal(3, rest.count)
275
+ end
276
+
277
+ 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}
280
+ assert_equal(1, rest.count)
281
+ assert_equal(2, lines.count)
282
+ end
283
+
284
+ end
285
+
286
+ describe HexaPDF::Layout::TextBox::SimpleLineWrapping do
287
+ before do
288
+ @obj = HexaPDF::Layout::TextBox::SimpleLineWrapping
289
+ end
290
+
291
+ describe "fixed width wrapping" do
292
+ include CommonLineWrappingTests
293
+
294
+ def call(items, width = 100)
295
+ lines = []
296
+ rest = @obj.call(items, width) {|line, _| lines << line; true }
297
+ [rest, lines]
298
+ end
299
+ end
300
+
301
+ describe "variable width wrapping" do
302
+ include CommonLineWrappingTests
303
+
304
+ def call(items, width = proc { 100 })
305
+ lines = []
306
+ rest = @obj.call(items, width) {|line, _| lines << line; true }
307
+ [rest, lines]
308
+ end
309
+
310
+ it "handles changing widths" do
311
+ height = 0
312
+ width_block = lambda do |line_height|
313
+ case height + line_height
314
+ when 0..10 then 60
315
+ when 11..20 then 40
316
+ when 21..30 then 20
317
+ else 60
318
+ end
319
+ end
320
+ lines = []
321
+ rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block) do |line|
322
+ height += line.height
323
+ lines << line
324
+ true
325
+ end
326
+ assert(rest.empty?)
327
+ assert_equal(3, lines.size)
328
+ assert_equal(30, lines[0].width)
329
+ assert_equal(20, lines[1].width)
330
+ assert_equal(40, lines[2].width)
331
+ end
332
+
333
+ it "handles changing widths when breaking on a penalty" do
334
+ height = 0
335
+ width_block = lambda do |line_height|
336
+ case height + line_height
337
+ when 0..10 then 80
338
+ else 50
339
+ end
340
+ end
341
+ lines = []
342
+ item = HexaPDF::Layout::InlineBox.new(20, 10) {}
343
+ items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
344
+ rest = @obj.call(items, width_block) do |line|
345
+ height += line.height
346
+ lines << line
347
+ true
348
+ end
349
+ assert(rest.empty?)
350
+ assert_equal(2, lines.size)
351
+ assert_equal(40, lines[0].width)
352
+ assert_equal(40, lines[1].width)
353
+ assert_equal(25, height)
354
+ end
355
+ end
356
+ end
357
+
358
+ describe HexaPDF::Layout::TextBox do
359
+ include TestTextBoxHelpers
360
+
361
+ before do
362
+ @doc = HexaPDF::Document.new
363
+ @font = @doc.fonts.load("Times")
364
+ @style = HexaPDF::Layout::Style.new(font: @font)
365
+ end
366
+
367
+ 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)
371
+ end
372
+
373
+ 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
377
+ assert_equal(1, items.length)
378
+ assert_box(items[0], item)
379
+
380
+ box.items = items
381
+ assert_same(items, box.items)
382
+ end
383
+
384
+ describe "fit" do
385
+ 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)
394
+ end
395
+
396
+ 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
400
+ assert(rest.empty?)
401
+ assert_equal(20 * 100, height)
402
+ end
403
+
404
+ 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
408
+ assert_equal(95, rest.count)
409
+ assert_equal(100, height)
410
+ end
411
+
412
+ 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
416
+ assert(rest.empty?)
417
+ assert_equal(20 * (5 + 4), height)
418
+ end
419
+
420
+ it "handles empty lines" do
421
+ items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
422
+ boxes([20, 20]) + [penalty(-5000)] * 2
423
+ box = HexaPDF::Layout::TextBox.new(items: items, width: 30, style: @style)
424
+ rest, height = box.fit
425
+ assert(rest.empty?)
426
+ assert_equal(5, box.lines.count)
427
+ assert_equal(20 + 20 + 9 + 20 + 9, height)
428
+ end
429
+
430
+ describe "fixed width" do
431
+ 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
435
+ assert_equal(1, rest.count)
436
+ assert_equal(20, height)
437
+ end
438
+
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
443
+ assert_equal(1, rest.count)
444
+ assert_equal(20, height)
445
+ end
446
+ end
447
+
448
+ describe "variable width with limited height" do
449
+ it "searches for a vertical offset if the first item is wider than the available width" do
450
+ width_block = lambda do |height, _|
451
+ case height
452
+ when 0..20 then 10
453
+ else 40
454
+ end
455
+ end
456
+ box = HexaPDF::Layout::TextBox.new(items: boxes([20, 18]), width: width_block,
457
+ height: 100, style: @style)
458
+ rest, height = box.fit
459
+ assert(rest.empty?)
460
+ assert_equal(1, box.lines.count)
461
+ assert_equal(24, box.lines[0].y_offset)
462
+ assert_equal(42, height)
463
+ end
464
+
465
+ it "searches for a vertical offset if an item is wider than the available width" do
466
+ width_block = lambda do |height, line_height|
467
+ if (40..60).cover?(height) || (40..60).cover?(height + line_height)
468
+ 10
469
+ else
470
+ 40
471
+ end
472
+ 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
476
+ 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)
482
+ end
483
+ end
484
+
485
+ it "post-processes lines for justification if needed" do
486
+ frag10 = HexaPDF::Layout::TextFragment.create(" ", font: @font)
487
+ frag10.items.freeze
488
+ frag10b = HexaPDF::Layout::TextBox::Box.new(frag10)
489
+ frag20 = HexaPDF::Layout::TextFragment.create(" ", font: @font, font_size: 20)
490
+ frag20b = HexaPDF::Layout::TextBox::Box.new(frag20)
491
+ items = boxes(20, 20, 20, 20, 30).insert(1, frag10b).insert(3, frag20b).insert(5, frag10b)
492
+ # Width of spaces: 2.5 * 2 + 5 = 10 (from AFM file, adjusted for font size)
493
+ # Line width: 20 * 4 + width_of_spaces = 90
494
+ # Missing width: 100 - 90 = 10
495
+ # -> Each space must be doubled!
496
+
497
+ box = HexaPDF::Layout::TextBox.new(items: items, width: 100)
498
+ box.style.align = :justify
499
+ rest, _height = box.fit
500
+ 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)
507
+ end
508
+
509
+ it "applies the optional horizontal offsets if set" do
510
+ 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
514
+ 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)
519
+ end
520
+ end
521
+
522
+ describe "draw" do
523
+ def assert_positions(content, positions)
524
+ processor = TestHelper::OperatorRecorder.new
525
+ HexaPDF::Content::Parser.new.parse(content, processor)
526
+ result = processor.recorded_ops
527
+ result.select! {|name, _| name == :set_text_matrix}.map! {|_, ops| ops[-2, 2]}
528
+ positions.each_with_index do |(x, y), index|
529
+ assert_in_delta(x, result[index][0], 0.00001)
530
+ assert_in_delta(y, result[index][1], 0.00001)
531
+ end
532
+ end
533
+
534
+ before do
535
+ @frag = HexaPDF::Layout::TextFragment.create("This is some more text.\n" \
536
+ "This is some more text.", font: @font)
537
+ @width = HexaPDF::Layout::TextFragment.create("This is some ", font: @font).width
538
+ @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width)
539
+ @canvas = @doc.pages.add.canvas
540
+
541
+ @line1w = HexaPDF::Layout::TextFragment.create("This is some", font: @font).width
542
+ @line2w = HexaPDF::Layout::TextFragment.create("more text.", font: @font).width
543
+ end
544
+
545
+ it "can horizontally align the contents to the left" do
546
+ top = 100
547
+ @box.style.align = :left
548
+ @box.draw(@canvas, 5, top)
549
+ assert_positions(@canvas.contents,
550
+ [[5, top - @frag.y_max],
551
+ [5, top - @frag.y_max - @frag.height],
552
+ [5, top - @frag.y_max - @frag.height * 2],
553
+ [5, top - @frag.y_max - @frag.height * 3]])
554
+ end
555
+
556
+ it "can horizontally align the contents to the center" do
557
+ top = 100
558
+ @box.style.align = :center
559
+ @box.draw(@canvas, 5, top)
560
+ assert_positions(@canvas.contents,
561
+ [[5 + (@width - @line1w) / 2, top - @frag.y_max],
562
+ [5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height],
563
+ [5 + (@width - @line1w) / 2, top - @frag.y_max - @frag.height * 2],
564
+ [5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height * 3]])
565
+ end
566
+
567
+ it "can horizontally align the contents to the right" do
568
+ top = 100
569
+ @box.style.align = :right
570
+ @box.draw(@canvas, 5, top)
571
+ assert_positions(@canvas.contents,
572
+ [[5 + @width - @line1w, top - @frag.y_max],
573
+ [5 + @width - @line2w, top - @frag.y_max - @frag.height],
574
+ [5 + @width - @line1w, top - @frag.y_max - @frag.height * 2],
575
+ [5 + @width - @line2w, top - @frag.y_max - @frag.height * 3]])
576
+ end
577
+
578
+ it "can justify the contents" do
579
+ top = 100
580
+ @box.style.align = :justify
581
+ @box.draw(@canvas, 5, top)
582
+ assert_positions(@canvas.contents,
583
+ [[5, top - @frag.y_max],
584
+ [5, top - @frag.y_max - @frag.height],
585
+ [5, top - @frag.y_max - @frag.height * 2],
586
+ [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)
589
+ end
590
+
591
+ 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)
596
+ end
597
+
598
+ it "can vertically align the contents in the center" do
599
+ top = 100
600
+ @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
601
+ @box.style.valign = :center
602
+
603
+ _, height = @box.fit
604
+ initial_baseline = top - ((top - height) / 2) - @frag.y_max
605
+ @box.draw(@canvas, 5, top)
606
+ assert_positions(@canvas.contents,
607
+ [[5, initial_baseline],
608
+ [5, initial_baseline - @frag.height],
609
+ [5, initial_baseline - @frag.height * 2],
610
+ [5, initial_baseline - @frag.height * 3]])
611
+ end
612
+
613
+ it "can vertically align the contents to the bottom" do
614
+ top = 100
615
+ @box = HexaPDF::Layout::TextBox.new(items: [@frag], width: @width, height: top)
616
+ @box.style.valign = :bottom
617
+
618
+ _, height = @box.fit
619
+ initial_baseline = height - @frag.y_max
620
+ @box.draw(@canvas, 5, top)
621
+ assert_positions(@canvas.contents,
622
+ [[5, initial_baseline],
623
+ [5, initial_baseline - @frag.height],
624
+ [5, initial_baseline - @frag.height * 2],
625
+ [5, initial_baseline - @frag.height * 3]])
626
+ end
627
+
628
+ 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)
630
+ assert_raises(HexaPDF::Error) do
631
+ @box.style.valign = :center
632
+ @box.draw(@canvas, 0, 0)
633
+ end
634
+ assert_raises(HexaPDF::Error) do
635
+ @box.style.valign = :bottom
636
+ @box.draw(@canvas, 0, 0)
637
+ end
638
+ end
639
+ end
640
+ end