hexapdf 0.4.0 → 0.5.0

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