hexapdf 0.7.0 → 0.8.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/LICENSE +3 -0
  5. data/README.md +2 -1
  6. data/Rakefile +3 -1
  7. data/VERSION +1 -1
  8. data/examples/{hello_world.rb → 001-hello_world.rb} +0 -0
  9. data/examples/{graphics.rb → 002-graphics.rb} +1 -1
  10. data/examples/{arc.rb → 003-arcs.rb} +2 -2
  11. data/examples/{optimizing.rb → 004-optimizing.rb} +0 -0
  12. data/examples/{merging.rb → 005-merging.rb} +0 -0
  13. data/examples/{standard_pdf_fonts.rb → 006-standard_pdf_fonts.rb} +0 -0
  14. data/examples/{truetype.rb → 007-truetype.rb} +0 -0
  15. data/examples/{show_char_bboxes.rb → 008-show_char_bboxes.rb} +0 -0
  16. data/examples/{text_layouter_alignment.rb → 009-text_layouter_alignment.rb} +3 -3
  17. data/examples/{text_layouter_inline_boxes.rb → 010-text_layouter_inline_boxes.rb} +7 -9
  18. data/examples/{text_layouter_line_wrapping.rb → 011-text_layouter_line_wrapping.rb} +6 -5
  19. data/examples/{text_layouter_styling.rb → 012-text_layouter_styling.rb} +6 -8
  20. data/examples/013-text_layouter_shapes.rb +176 -0
  21. data/examples/014-text_in_polygon.rb +60 -0
  22. data/examples/{boxes.rb → 015-boxes.rb} +29 -21
  23. data/examples/016-frame_automatic_box_placement.rb +90 -0
  24. data/examples/017-frame_text_flow.rb +60 -0
  25. data/lib/hexapdf/cli/command.rb +4 -3
  26. data/lib/hexapdf/cli/files.rb +1 -1
  27. data/lib/hexapdf/cli/inspect.rb +0 -1
  28. data/lib/hexapdf/cli/merge.rb +1 -1
  29. data/lib/hexapdf/cli/modify.rb +1 -1
  30. data/lib/hexapdf/configuration.rb +2 -0
  31. data/lib/hexapdf/content/canvas.rb +3 -3
  32. data/lib/hexapdf/content/graphic_object.rb +1 -0
  33. data/lib/hexapdf/content/graphic_object/geom2d.rb +132 -0
  34. data/lib/hexapdf/dictionary.rb +7 -1
  35. data/lib/hexapdf/dictionary_fields.rb +35 -83
  36. data/lib/hexapdf/document.rb +9 -5
  37. data/lib/hexapdf/document/fonts.rb +1 -1
  38. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  39. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  40. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  41. data/lib/hexapdf/font/cmap/writer.rb +2 -2
  42. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  43. data/lib/hexapdf/font/true_type/table.rb +1 -1
  44. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  45. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  46. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  47. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  48. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  49. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  50. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  51. data/lib/hexapdf/image_loader/png.rb +2 -2
  52. data/lib/hexapdf/layout.rb +3 -0
  53. data/lib/hexapdf/layout/box.rb +64 -46
  54. data/lib/hexapdf/layout/frame.rb +348 -0
  55. data/lib/hexapdf/layout/inline_box.rb +2 -2
  56. data/lib/hexapdf/layout/line.rb +3 -3
  57. data/lib/hexapdf/layout/style.rb +81 -14
  58. data/lib/hexapdf/layout/text_box.rb +84 -0
  59. data/lib/hexapdf/layout/text_fragment.rb +8 -8
  60. data/lib/hexapdf/layout/text_layouter.rb +278 -169
  61. data/lib/hexapdf/layout/width_from_polygon.rb +246 -0
  62. data/lib/hexapdf/rectangle.rb +9 -9
  63. data/lib/hexapdf/stream.rb +2 -2
  64. data/lib/hexapdf/type.rb +1 -0
  65. data/lib/hexapdf/type/action.rb +1 -1
  66. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  67. data/lib/hexapdf/type/catalog.rb +1 -1
  68. data/lib/hexapdf/type/cid_font.rb +2 -1
  69. data/lib/hexapdf/type/font.rb +0 -1
  70. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  71. data/lib/hexapdf/type/font_simple.rb +3 -3
  72. data/lib/hexapdf/type/font_true_type.rb +8 -0
  73. data/lib/hexapdf/type/font_type0.rb +2 -1
  74. data/lib/hexapdf/type/font_type1.rb +7 -1
  75. data/lib/hexapdf/type/font_type3.rb +61 -0
  76. data/lib/hexapdf/type/graphics_state_parameter.rb +8 -8
  77. data/lib/hexapdf/type/image.rb +10 -0
  78. data/lib/hexapdf/type/page.rb +83 -10
  79. data/lib/hexapdf/version.rb +1 -1
  80. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  81. data/test/hexapdf/content/graphic_object/test_geom2d.rb +79 -0
  82. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  83. data/test/hexapdf/font/test_true_type_wrapper.rb +1 -1
  84. data/test/hexapdf/font/test_type1_wrapper.rb +1 -1
  85. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  86. data/test/hexapdf/font/true_type/table/test_directory.rb +1 -1
  87. data/test/hexapdf/font/true_type/table/test_head.rb +7 -3
  88. data/test/hexapdf/layout/test_box.rb +57 -15
  89. data/test/hexapdf/layout/test_frame.rb +313 -0
  90. data/test/hexapdf/layout/test_inline_box.rb +1 -1
  91. data/test/hexapdf/layout/test_style.rb +74 -0
  92. data/test/hexapdf/layout/test_text_box.rb +77 -0
  93. data/test/hexapdf/layout/test_text_layouter.rb +220 -239
  94. data/test/hexapdf/layout/test_width_from_polygon.rb +108 -0
  95. data/test/hexapdf/test_dictionary_fields.rb +22 -26
  96. data/test/hexapdf/test_document.rb +3 -3
  97. data/test/hexapdf/test_reference.rb +1 -0
  98. data/test/hexapdf/test_writer.rb +2 -2
  99. data/test/hexapdf/type/test_font_true_type.rb +25 -0
  100. data/test/hexapdf/type/test_font_type1.rb +6 -0
  101. data/test/hexapdf/type/test_font_type3.rb +26 -0
  102. data/test/hexapdf/type/test_image.rb +10 -0
  103. data/test/hexapdf/type/test_page.rb +114 -0
  104. data/test/test_helper.rb +1 -1
  105. metadata +65 -17
  106. data/examples/text_layouter_shapes.rb +0 -170
@@ -363,38 +363,30 @@ describe HexaPDF::Layout::TextLayouter do
363
363
  @style = HexaPDF::Layout::Style.new(font: @font)
364
364
  end
365
365
 
366
- it "creates an instance from text and options" do
367
- layouter = HexaPDF::Layout::TextLayouter.create("T", font: @font, width: 100, height: 100)
368
- assert_equal(1, layouter.items.length)
369
- assert_equal(@font.decode_utf8("T"), layouter.items[0].item.items)
370
- end
371
-
372
366
  describe "initialize" do
373
367
  it "can use a Style object" do
374
368
  style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
375
- layouter = HexaPDF::Layout::TextLayouter.new(items: [], width: 10, style: style)
369
+ layouter = HexaPDF::Layout::TextLayouter.new(style)
376
370
  assert_equal(20, layouter.style.font_size)
377
371
  end
378
372
 
379
373
  it "can use a style options" do
380
- layouter = HexaPDF::Layout::TextLayouter.new(items: [], width: 10, style:
381
- {font: @font, font_size: 20})
374
+ layouter = HexaPDF::Layout::TextLayouter.new(font: @font, font_size: 20)
382
375
  assert_equal(20, layouter.style.font_size)
383
376
  end
384
377
  end
385
378
 
386
- it "doesn't run the text segmentation algorithm on already segmented items" do
387
- item = HexaPDF::Layout::InlineBox.create(width: 20) {}
388
- layouter = HexaPDF::Layout::TextLayouter.new(items: [item], width: 100, height: 100)
389
- items = layouter.items
390
- assert_equal(1, items.length)
391
- assert_box(items[0], item)
379
+ describe "fit" do
380
+ before do
381
+ @layouter = HexaPDF::Layout::TextLayouter.new(@style)
382
+ end
392
383
 
393
- layouter.items = items
394
- assert_same(items, layouter.items)
395
- end
384
+ it "does nothing if there are no items" do
385
+ result = @layouter.fit([], 100, 100)
386
+ assert_equal(:success, result.status)
387
+ assert_equal(0, result.height)
388
+ end
396
389
 
397
- describe "fit" do
398
390
  it "handles text indentation" do
399
391
  items = boxes([20, 20], [20, 20], [20, 20]) +
400
392
  [HexaPDF::Layout::TextLayouter::Penalty::MandatoryParagraphBreak] +
@@ -404,93 +396,94 @@ describe HexaPDF::Layout::TextLayouter do
404
396
  @style.text_indent = 20
405
397
 
406
398
  [60, proc { 60 }].each do |width|
407
- layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: width, style: @style)
408
- rest, reason = layouter.fit
409
- assert_equal([40, 20, 40, 60, 20, 60, 20], layouter.lines.map(&:width))
410
- assert_equal([20, 0, 20, 0, 0, 0, 0], layouter.lines.map(&:x_offset))
411
- assert(rest.empty?)
412
- assert_equal(:success, reason)
399
+ result = @layouter.fit(items, width, 200)
400
+ assert_equal([40, 20, 40, 60, 20, 60, 20], result.lines.map(&:width))
401
+ assert_equal([20, 0, 20, 0, 0, 0, 0], result.lines.map(&:x_offset))
402
+ assert(result.remaining_items.empty?)
403
+ assert_equal(:success, result.status)
413
404
  end
414
405
  end
415
406
 
416
- it "fits using unlimited height" do
417
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 100)), width: 20,
418
- style: @style)
419
- rest, reason = layouter.fit
420
- assert(rest.empty?)
421
- assert_equal(:success, reason)
422
- assert_equal(20 * 100, layouter.actual_height)
423
- end
424
-
425
407
  it "fits using a limited height" do
426
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 100)), width: 20,
427
- height: 100, style: @style)
428
- rest, reason = layouter.fit
429
- assert_equal(95, rest.count)
430
- assert_equal(:height, reason)
431
- assert_equal(100, layouter.actual_height)
408
+ result = @layouter.fit(boxes(*([[20, 20]] * 100)), 20, 100)
409
+ assert_equal(95, result.remaining_items.count)
410
+ assert_equal(:height, result.status)
411
+ assert_equal(100, result.height)
432
412
  end
433
413
 
434
414
  it "takes line spacing into account when calculating the height" do
435
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 20]] * 5)), width: 20,
436
- style: @style)
437
- layouter.style.line_spacing = :double
438
- rest, reason = layouter.fit
439
- assert(rest.empty?)
440
- assert_equal(:success, reason)
441
- assert_equal(20 * (5 + 4), layouter.actual_height)
415
+ @style.line_spacing = :double
416
+ result = @layouter.fit(boxes(*([[20, 20]] * 5)), 20, 200)
417
+ assert(result.remaining_items.empty?)
418
+ assert_equal(:success, result.status)
419
+ assert_equal(20 * (5 + 4), result.height)
442
420
  end
443
421
 
444
422
  it "handles empty lines" do
445
423
  items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
446
424
  boxes([20, 20]) + [penalty(-5000)] * 2
447
- layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: 30, style: @style)
448
- rest, reason = layouter.fit
449
- assert(rest.empty?)
450
- assert_equal(:success, reason)
451
- assert_equal(5, layouter.lines.count)
452
- assert_equal(20 + 20 + 9 + 20 + 9, layouter.actual_height)
453
- end
454
-
455
- describe "fixed width" do
456
- it "stops if an item is wider than the available width, with unlimited height" do
457
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 20], [50, 20]), width: 30,
458
- style: @style)
459
- rest, reason = layouter.fit
460
- assert_equal(1, rest.count)
461
- assert_equal(:box, reason)
462
- assert_equal(20, layouter.actual_height)
425
+ result = @layouter.fit(items, 30, 100)
426
+ assert(result.remaining_items.empty?)
427
+ assert_equal(:success, result.status)
428
+ assert_equal(5, result.lines.count)
429
+ assert_equal(20 + 20 + 9 + 20 + 9, result.height)
430
+ end
431
+
432
+ it "handles line breaks in combination with multiple parts per line" do
433
+ items = boxes([20, 20]) + [penalty(-5000)] +
434
+ boxes([20, 20], [20, 20], [20, 20]) + [penalty(-5000)] +
435
+ [penalty(-5000)] +
436
+ boxes([20, 20])
437
+ result = @layouter.fit(items, [0, 40, 0, 40, 0, 40], 100)
438
+ assert_equal(:success, result.status)
439
+ assert_equal([20, 40, 20, 0, 20], result.lines.map(&:width))
440
+ assert_equal([20, 20, 0, 6.83, 22.17], result.lines.map(&:y_offset))
441
+ end
442
+
443
+ describe "fixed width and too wide item" do
444
+ it "single part per line" do
445
+ result = @layouter.fit(boxes([20, 20], [50, 20]), 30, 100)
446
+ assert_equal(1, result.remaining_items.count)
447
+ assert_equal(:box_too_wide, result.status)
448
+ assert_equal(20, result.height)
463
449
  end
464
450
 
465
- it "stops if a box item is wider than the available width, with limited height" do
466
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 20], [50, 20]), width: 30,
467
- height: 100, style: @style)
468
- rest, reason = layouter.fit
469
- assert_equal(1, rest.count)
470
- assert_equal(:box, reason)
471
- assert_equal(20, layouter.actual_height)
451
+ it "multiple parts per line, one fits" do
452
+ result = @layouter.fit(boxes([20, 20], [40, 20], [40, 20], [10, 20]),
453
+ [0, 30, 0, 50, 0, 30, 0, 30], 100)
454
+ assert_equal(0, result.remaining_items.count)
455
+ assert_equal([20, 40, 0, 0, 0, 50], result.lines.map(&:width))
456
+ assert_equal([0, 30, 80, 110, 0, 30], result.lines.map(&:x_offset))
457
+ assert_equal([20, 0, 0, 0, 20, 0], result.lines.map(&:y_offset))
458
+ assert_equal(:success, result.status)
459
+ assert_equal(40, result.height)
460
+ end
461
+
462
+ it "multiple parts per line, none fits" do
463
+ result = @layouter.fit(boxes([20, 20], [50, 20]), [0, 30, 0, 30, 0, 30], 100)
464
+ assert_equal(1, result.remaining_items.count)
465
+ assert_equal(:box_too_wide, result.status)
466
+ assert_equal(20, result.height)
472
467
  end
473
468
  end
474
469
 
475
- describe "variable width with limited height" do
476
- it "searches for a vertical offset if the first item is wider than the available width" do
470
+ describe "variable width" do
471
+ it "single part per line, searches for vertical offset if the first item is too wide" do
477
472
  width_block = lambda do |height, _|
478
473
  case height
479
474
  when 0..20 then 10
480
475
  else 40
481
476
  end
482
477
  end
483
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes([20, 18]), width: width_block,
484
- height: 100, style: @style)
485
- rest, reason = layouter.fit
486
- assert(rest.empty?)
487
- assert_equal(:success, reason)
488
- assert_equal(1, layouter.lines.count)
489
- assert_equal(24, layouter.lines[0].y_offset)
490
- assert_equal(42, layouter.actual_height)
478
+ result = @layouter.fit(boxes([20, 18]), width_block, 100)
479
+ assert(result.remaining_items.empty?)
480
+ assert_equal(:success, result.status)
481
+ assert_equal(1, result.lines.count)
482
+ assert_equal(42, result.lines[0].y_offset)
483
+ assert_equal(42, result.height)
491
484
  end
492
485
 
493
- it "searches for a vertical offset if an item is wider than the available width" do
486
+ it "single part per line, searches for vertical offset if an item is too wide" do
494
487
  width_block = lambda do |height, line_height|
495
488
  if (40..60).cover?(height) || (40..60).cover?(height + line_height)
496
489
  10
@@ -498,36 +491,118 @@ describe HexaPDF::Layout::TextLayouter do
498
491
  40
499
492
  end
500
493
  end
501
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 18]] * 7)),
502
- width: width_block, height: 100, style: @style)
503
- rest, reason = layouter.fit
504
- assert_equal(1, rest.count)
505
- assert_equal(:height, reason)
506
- assert_equal(3, layouter.lines.count)
507
- assert_equal(0, layouter.lines[0].y_offset)
508
- assert_equal(18, layouter.lines[1].y_offset)
509
- assert_equal(48, layouter.lines[2].y_offset)
510
- assert_equal(84, layouter.actual_height)
494
+ result = @layouter.fit(boxes(*([[20, 18]] * 7)), width_block, 100)
495
+ assert_equal(1, result.remaining_items.count)
496
+ assert_equal(:height, result.status)
497
+ assert_equal(3, result.lines.count)
498
+ assert_equal(18, result.lines[0].y_offset)
499
+ assert_equal(18, result.lines[1].y_offset)
500
+ assert_equal(48, result.lines[2].y_offset)
501
+ assert_equal(84, result.height)
502
+ end
503
+
504
+ it "multiple parts per line, reset line because of line height change during first part" do
505
+ width_block = lambda do |height, line_height|
506
+ if height == 0 && line_height <= 10
507
+ [0, 40, 0, 40]
508
+ elsif height == 0 && line_height > 10
509
+ [0, 30, 0, 40]
510
+ else
511
+ [0, 60]
512
+ end
513
+ end
514
+ result = @layouter.fit(boxes([20, 10], [20, 10], [20, 18], [20, 10]), width_block, 100)
515
+ assert_equal(:success, result.status)
516
+ assert_equal(0, result.remaining_items.count)
517
+ assert_equal(3, result.lines.count)
518
+ assert_equal([20, 40, 20], result.lines.map(&:width))
519
+ assert_equal([18, 0, 10], result.lines.map(&:y_offset))
520
+ assert_equal(28, result.height)
521
+ end
522
+
523
+ it "multiple parts per line, reset line because of line height change after first part" do
524
+ width_block = lambda do |height, line_height|
525
+ if height == 0 && line_height <= 10
526
+ [0, 40, 0, 40]
527
+ elsif height == 0 && line_height > 10
528
+ [0, 30, 0, 40]
529
+ else
530
+ [0, 60]
531
+ end
532
+ end
533
+ result = @layouter.fit(boxes([20, 10], [15, 10], [10, 10], [12, 18], [20, 10]),
534
+ width_block, 100)
535
+ assert_equal(:success, result.status)
536
+ assert_equal(0, result.remaining_items.count)
537
+ assert_equal(3, result.lines.count)
538
+ assert_equal([20, 37, 20], result.lines.map(&:width))
539
+ assert_equal([18, 0, 10], result.lines.map(&:y_offset))
540
+ assert_equal(28, result.height)
511
541
  end
512
542
  end
513
543
 
514
544
  it "breaks a text fragment into parts if it is wider than the available width" do
515
- [100, nil].each do |height|
516
- str = " Thisisaverylongstring"
517
- frag = HexaPDF::Layout::TextFragment.create(str, font: @font)
518
- layouter = HexaPDF::Layout::TextLayouter.new(items: [frag], width: 20, height: height,
519
- style: @style)
520
- rest, reason = layouter.fit
521
- assert(rest.empty?)
522
- assert_equal(:success, reason)
523
- assert_equal(str.strip.length, layouter.lines.sum {|l| l.items.sum {|i| i.items.count } })
524
- assert_equal(45, layouter.actual_height)
525
-
526
- layouter = HexaPDF::Layout::TextLayouter.new(items: [frag], width: 1, height: height,
527
- style: @style)
528
- rest, reason = layouter.fit
529
- assert_equal(str.strip.length, rest.count)
530
- assert_equal(:box, reason)
545
+ str = " Thisisaverylongstring"
546
+ frag = HexaPDF::Layout::TextFragment.create(str, font: @font)
547
+ result = @layouter.fit([frag], 20, 100)
548
+ assert(result.remaining_items.empty?)
549
+ assert_equal(:success, result.status)
550
+ assert_equal(str.strip.length, result.lines.sum {|l| l.items.sum {|i| i.items.count } })
551
+ assert_equal(45, result.height)
552
+
553
+ result = @layouter.fit([frag], 1, 100)
554
+ assert_equal(str.strip.length, result.remaining_items.count)
555
+ assert_equal(:box_too_wide, result.status)
556
+ end
557
+
558
+ describe "horizontal alignment" do
559
+ before do
560
+ @items = boxes(*[[20, 20]] * 4)
561
+ end
562
+
563
+ it "aligns the contents to the left" do
564
+ @style.align = :left
565
+ result = @layouter.fit(@items, 100, 100)
566
+ assert_equal(0, result.lines[0].x_offset)
567
+ end
568
+
569
+ it "aligns the contents to the center" do
570
+ @style.align = :center
571
+ result = @layouter.fit(@items, 100, 100)
572
+ assert_equal(10, result.lines[0].x_offset)
573
+ end
574
+
575
+ it "aligns the contents to the right" do
576
+ @style.align = :right
577
+ result = @layouter.fit(@items, 100, 100)
578
+ assert_equal(20, result.lines[0].x_offset)
579
+ end
580
+ end
581
+
582
+ describe "vertical alignment" do
583
+ before do
584
+ @items = boxes(*[[20, 20]] * 4)
585
+ end
586
+
587
+ it "aligns the contents to the top" do
588
+ @style.valign = :top
589
+ result = @layouter.fit(@items, 40, 100)
590
+ assert_equal(result.lines[0].y_max, result.lines[0].y_offset)
591
+ assert_equal(40, result.height)
592
+ end
593
+
594
+ it "aligns the contents to the center" do
595
+ @style.valign = :center
596
+ result = @layouter.fit(@items, 40, 100)
597
+ assert_equal((100 - 40) / 2 + 20, result.lines[0].y_offset)
598
+ assert_equal(70, result.height)
599
+ end
600
+
601
+ it "aligns the contents to the bottom" do
602
+ @style.valign = :bottom
603
+ result = @layouter.fit(@items, 40, 100)
604
+ assert_equal(100 - 20 * 2 + 20, result.lines[0].y_offset)
605
+ assert_equal(100, result.height)
531
606
  end
532
607
  end
533
608
 
@@ -543,34 +618,20 @@ describe HexaPDF::Layout::TextLayouter do
543
618
  # Missing width: 100 - 90 = 10
544
619
  # -> Each space must be doubled!
545
620
 
546
- layouter = HexaPDF::Layout::TextLayouter.new(items: items, width: 100)
547
- layouter.style.align = :justify
548
- rest, reason = layouter.fit
549
- assert(rest.empty?)
550
- assert_equal(:success, reason)
551
- assert_equal(9, layouter.lines[0].items.count)
552
- assert_in_delta(100, layouter.lines[0].width)
553
- assert_equal(-250, layouter.lines[0].items[1].items[0])
554
- assert_equal(-250, layouter.lines[0].items[4].items[0])
555
- assert_equal(-250, layouter.lines[0].items[6].items[0])
556
- assert_equal(30, layouter.lines[1].width)
557
- end
558
-
559
- it "applies the optional horizontal offsets if set" do
560
- x_offsets = lambda {|height, line_height| height + line_height }
561
- layouter = HexaPDF::Layout::TextLayouter.new(items: boxes(*([[20, 10]] * 7)), width: 60,
562
- x_offsets: x_offsets, height: 100, style: @style)
563
- rest, reason = layouter.fit
564
- assert(rest.empty?)
565
- assert_equal(:success, reason)
566
- assert_equal(30, layouter.actual_height)
567
- assert_equal(10, layouter.lines[0].x_offset)
568
- assert_equal(20, layouter.lines[1].x_offset)
569
- assert_equal(30, layouter.lines[2].x_offset)
621
+ @style.align = :justify
622
+ result = @layouter.fit(items, 100, 100)
623
+ assert(result.remaining_items.empty?)
624
+ assert_equal(:success, result.status)
625
+ assert_equal(9, result.lines[0].items.count)
626
+ assert_in_delta(100, result.lines[0].width)
627
+ assert_equal(-250, result.lines[0].items[1].items[0])
628
+ assert_equal(-250, result.lines[0].items[4].items[0])
629
+ assert_equal(-250, result.lines[0].items[6].items[0])
630
+ assert_equal(30, result.lines[1].width)
570
631
  end
571
632
  end
572
633
 
573
- describe "draw" do
634
+ describe "Result#draw" do
574
635
  def assert_positions(content, positions)
575
636
  processor = TestHelper::OperatorRecorder.new
576
637
  HexaPDF::Content::Parser.new.parse(content, processor)
@@ -596,133 +657,53 @@ describe HexaPDF::Layout::TextLayouter do
596
657
  @frag = HexaPDF::Layout::TextFragment.create("This is some more text.\n" \
597
658
  "This is some more text.", font: @font)
598
659
  @width = HexaPDF::Layout::TextFragment.create("This is some ", font: @font).width
599
- @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width)
660
+ @layouter = HexaPDF::Layout::TextLayouter.new
600
661
  @canvas = @doc.pages.add.canvas
601
662
 
602
663
  @line1w = HexaPDF::Layout::TextFragment.create("This is some", font: @font).width
603
664
  @line2w = HexaPDF::Layout::TextFragment.create("more text.", font: @font).width
604
665
  end
605
666
 
606
- it "returns the result of #fit if #fit needs to be run" do
607
- rest, reason = @layouter.draw(@canvas, 0, 0)
608
- assert(rest.empty?)
609
- assert_equal(:success, reason)
610
-
611
- @layouter.items = [HexaPDF::Layout::InlineBox.create(width: @width + 10) {}]
612
- rest, reason = @layouter.draw(@canvas, 0, 0)
613
- assert_equal(1, rest.size)
614
- assert_equal(:box, reason)
615
-
616
- assert_nil(@layouter.draw(@canvas, 0, 0))
617
- end
618
-
619
- it "can horizontally align the contents to the left" do
620
- top = 100
621
- @layouter.style.align = :left
622
- @layouter.draw(@canvas, 5, top)
623
- assert_positions(@canvas.contents,
624
- [[5, top - @frag.y_max],
625
- [5, top - @frag.y_max - @frag.height],
626
- [5, top - @frag.y_max - @frag.height * 2],
627
- [5, top - @frag.y_max - @frag.height * 3]])
628
- end
629
-
630
- it "can horizontally align the contents to the center" do
631
- top = 100
632
- @layouter.style.align = :center
633
- @layouter.draw(@canvas, 5, top)
634
- assert_positions(@canvas.contents,
635
- [[5 + (@width - @line1w) / 2, top - @frag.y_max],
636
- [5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height],
637
- [5 + (@width - @line1w) / 2, top - @frag.y_max - @frag.height * 2],
638
- [5 + (@width - @line2w) / 2, top - @frag.y_max - @frag.height * 3]])
639
- end
640
-
641
- it "can horizontally align the contents to the right" do
642
- top = 100
643
- @layouter.style.align = :right
644
- @layouter.draw(@canvas, 5, top)
645
- assert_positions(@canvas.contents,
646
- [[5 + @width - @line1w, top - @frag.y_max],
647
- [5 + @width - @line2w, top - @frag.y_max - @frag.height],
648
- [5 + @width - @line1w, top - @frag.y_max - @frag.height * 2],
649
- [5 + @width - @line2w, top - @frag.y_max - @frag.height * 3]])
650
- end
651
-
652
- it "can justify the contents" do
653
- top = 100
654
- @layouter.style.align = :justify
655
- @layouter.draw(@canvas, 5, top)
656
- assert_positions(@canvas.contents,
657
- [[5, top - @frag.y_max],
658
- [5, top - @frag.y_max - @frag.height],
659
- [5, top - @frag.y_max - @frag.height * 2],
660
- [5, top - @frag.y_max - @frag.height * 3]])
661
- assert_in_delta(@width, @layouter.lines[0].width, 0.0001)
662
- assert_in_delta(@width, @layouter.lines[2].width, 0.0001)
663
- end
664
-
665
- it "doesn't justify lines ending in a mandatory break or the last line" do
666
- @layouter.style.align = :justify
667
- @layouter.draw(@canvas, 5, 100)
668
- assert_equal(@line2w, @layouter.lines[1].width, 0.0001)
669
- assert_equal(@line2w, @layouter.lines[3].width, 0.0001)
670
- end
671
-
672
- it "can vertically align the contents in the center" do
667
+ it "respects the x- and y-offsets" do
673
668
  top = 100
674
- @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width, height: top)
675
669
  @layouter.style.valign = :center
670
+ @layouter.style.align = :center
676
671
 
677
- @layouter.fit
678
- initial_baseline = top - ((top - @layouter.actual_height) / 2) - @frag.y_max
679
- @layouter.draw(@canvas, 5, top)
680
- assert_positions(@canvas.contents,
681
- [[5, initial_baseline],
682
- [5, initial_baseline - @frag.height],
683
- [5, initial_baseline - @frag.height * 2],
684
- [5, initial_baseline - @frag.height * 3]])
685
- end
686
-
687
- it "can vertically align the contents to the bottom" do
688
- top = 100
689
- @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width, height: top)
690
- @layouter.style.valign = :bottom
672
+ result = @layouter.fit([@frag], @width, top)
673
+ result.draw(@canvas, 5, top)
691
674
 
692
- @layouter.fit
693
- initial_baseline = @layouter.actual_height - @frag.y_max
694
- @layouter.draw(@canvas, 5, top)
675
+ initial_baseline = top - result.lines.first.y_offset
695
676
  assert_positions(@canvas.contents,
696
- [[5, initial_baseline],
697
- [5, initial_baseline - @frag.height],
698
- [5, initial_baseline - @frag.height * 2],
699
- [5, initial_baseline - @frag.height * 3]])
700
- end
701
-
702
- it "raises an error if vertical alignment is :center/:bottom and an unlimited height is used" do
703
- @layouter = HexaPDF::Layout::TextLayouter.new(items: [@frag], width: @width)
704
- assert_raises(HexaPDF::Error) do
705
- @layouter.style.valign = :center
706
- @layouter.draw(@canvas, 0, 0)
707
- end
708
- assert_raises(HexaPDF::Error) do
709
- @layouter.style.valign = :bottom
710
- @layouter.draw(@canvas, 0, 0)
711
- end
677
+ [[5 + (@width - @line1w) / 2, initial_baseline],
678
+ [5 + (@width - @line2w) / 2, initial_baseline - @frag.height],
679
+ [5 + (@width - @line1w) / 2, initial_baseline - @frag.height * 2],
680
+ [5 + (@width - @line2w) / 2, initial_baseline - @frag.height * 3]])
712
681
  end
713
682
 
714
683
  it "makes sure that text fragments don't pollute the graphics state for inline boxes" do
715
- frag = HexaPDF::Layout::TextFragment.create("Demo", font: @font)
716
684
  inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {|c, _| c.text("A") }
717
- layouter = HexaPDF::Layout::TextLayouter.new(items: [frag, inline_box], width: 200)
718
- assert_raises(HexaPDF::Error) { layouter.draw(@canvas, 0, 0) }
685
+ result = @layouter.fit([@frag, inline_box], 200, 100)
686
+ assert_raises(HexaPDF::Error) { result.draw(@canvas, 0, 0) } # bc font should be reset to nil
687
+ end
688
+
689
+ it "doesn't do unnecessary work for consecutive text fragments with same style" do
690
+ @layouter.fit([@frag], 200, 100).draw(@canvas, 0, 0)
691
+ assert_operators(@canvas.contents, [[:save_graphics_state],
692
+ [:set_leading, [9.0]],
693
+ [:set_font_and_size, [:F1, 10]],
694
+ [:begin_text],
695
+ [:move_text, [0, -6.83]],
696
+ [:show_text, ["This is some more text."]],
697
+ [:move_text_next_line],
698
+ [:show_text, ["This is some more text."]],
699
+ [:end_text],
700
+ [:restore_graphics_state]])
719
701
  end
720
702
 
721
703
  it "doesn't do unnecessary work for placeholder boxes" do
722
704
  box1 = HexaPDF::Layout::InlineBox.create(width: 10, height: 20)
723
705
  box2 = HexaPDF::Layout::InlineBox.create(width: 30, height: 40) { @canvas.line_width(2) }
724
- layouter = HexaPDF::Layout::TextLayouter.new(items: [box1, box2], width: 200)
725
- layouter.draw(@canvas, 0, 0)
706
+ @layouter.fit([box1, box2], 200, 100).draw(@canvas, 0, 0)
726
707
  assert_operators(@canvas.contents, [[:save_graphics_state],
727
708
  [:restore_graphics_state],
728
709
  [:save_graphics_state],