hexapdf 0.8.0 → 0.9.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/018-composer.rb +44 -0
  7. data/lib/hexapdf/cli.rb +2 -0
  8. data/lib/hexapdf/cli/command.rb +2 -2
  9. data/lib/hexapdf/cli/optimize.rb +1 -1
  10. data/lib/hexapdf/cli/split.rb +82 -0
  11. data/lib/hexapdf/composer.rb +303 -0
  12. data/lib/hexapdf/configuration.rb +2 -2
  13. data/lib/hexapdf/content/canvas.rb +3 -6
  14. data/lib/hexapdf/dictionary.rb +0 -3
  15. data/lib/hexapdf/document.rb +30 -22
  16. data/lib/hexapdf/document/files.rb +1 -1
  17. data/lib/hexapdf/document/images.rb +1 -1
  18. data/lib/hexapdf/filter/predictor.rb +8 -8
  19. data/lib/hexapdf/layout.rb +1 -0
  20. data/lib/hexapdf/layout/box.rb +55 -12
  21. data/lib/hexapdf/layout/frame.rb +143 -46
  22. data/lib/hexapdf/layout/image_box.rb +96 -0
  23. data/lib/hexapdf/layout/inline_box.rb +10 -0
  24. data/lib/hexapdf/layout/line.rb +1 -1
  25. data/lib/hexapdf/layout/style.rb +55 -3
  26. data/lib/hexapdf/layout/text_box.rb +38 -8
  27. data/lib/hexapdf/layout/text_layouter.rb +66 -52
  28. data/lib/hexapdf/object.rb +3 -2
  29. data/lib/hexapdf/parser.rb +6 -1
  30. data/lib/hexapdf/rectangle.rb +6 -0
  31. data/lib/hexapdf/reference.rb +4 -6
  32. data/lib/hexapdf/revision.rb +34 -8
  33. data/lib/hexapdf/revisions.rb +12 -2
  34. data/lib/hexapdf/stream.rb +18 -0
  35. data/lib/hexapdf/task.rb +1 -1
  36. data/lib/hexapdf/task/dereference.rb +1 -1
  37. data/lib/hexapdf/task/optimize.rb +2 -2
  38. data/lib/hexapdf/type/form.rb +10 -0
  39. data/lib/hexapdf/version.rb +1 -1
  40. data/lib/hexapdf/writer.rb +25 -5
  41. data/man/man1/hexapdf.1 +17 -0
  42. data/test/hexapdf/layout/test_box.rb +7 -1
  43. data/test/hexapdf/layout/test_frame.rb +38 -1
  44. data/test/hexapdf/layout/test_image_box.rb +73 -0
  45. data/test/hexapdf/layout/test_inline_box.rb +8 -0
  46. data/test/hexapdf/layout/test_line.rb +1 -1
  47. data/test/hexapdf/layout/test_style.rb +58 -1
  48. data/test/hexapdf/layout/test_text_box.rb +57 -12
  49. data/test/hexapdf/layout/test_text_layouter.rb +14 -4
  50. data/test/hexapdf/task/test_optimize.rb +3 -3
  51. data/test/hexapdf/test_composer.rb +258 -0
  52. data/test/hexapdf/test_document.rb +29 -3
  53. data/test/hexapdf/test_importer.rb +8 -2
  54. data/test/hexapdf/test_object.rb +3 -1
  55. data/test/hexapdf/test_parser.rb +6 -0
  56. data/test/hexapdf/test_rectangle.rb +7 -0
  57. data/test/hexapdf/test_reference.rb +3 -1
  58. data/test/hexapdf/test_revision.rb +13 -0
  59. data/test/hexapdf/test_revisions.rb +1 -0
  60. data/test/hexapdf/test_stream.rb +13 -0
  61. data/test/hexapdf/test_writer.rb +13 -2
  62. data/test/hexapdf/type/test_annotation.rb +1 -1
  63. data/test/hexapdf/type/test_form.rb +13 -2
  64. metadata +10 -4
@@ -60,4 +60,12 @@ describe HexaPDF::Layout::InlineBox do
60
60
  it "returns width for x_max" do
61
61
  assert_equal(@box.width, @box.x_max)
62
62
  end
63
+
64
+ it "returns 0 for y_min" do
65
+ assert_equal(0, @box.y_min)
66
+ end
67
+
68
+ it "returns height for y_max" do
69
+ assert_equal(@box.height, @box.y_max)
70
+ end
63
71
  end
@@ -12,7 +12,7 @@ describe HexaPDF::Layout::Line::HeightCalculator do
12
12
  @calc << HexaPDF::Layout::InlineBox.create(width: 10, height: 20, valign: :baseline) {}
13
13
  assert_equal([0, 20, 0, 0], @calc.result)
14
14
  new_item = HexaPDF::Layout::InlineBox.create(width: 10, height: 30, valign: :top) {}
15
- assert_equal(30, @calc.simulate_height(new_item))
15
+ assert_equal([-10, 20, 30], @calc.simulate_height(new_item))
16
16
  assert_equal([0, 20, 0, 0], @calc.result)
17
17
  end
18
18
  end
@@ -18,7 +18,7 @@ describe HexaPDF::Layout::Style::LineSpacing do
18
18
  end
19
19
 
20
20
  def line_spacing(type, value = nil)
21
- HexaPDF::Layout::Style::LineSpacing.new(type, value: value)
21
+ HexaPDF::Layout::Style::LineSpacing.new(type: type, value: value)
22
22
  end
23
23
 
24
24
  it "allows single line spacing" do
@@ -147,6 +147,17 @@ describe HexaPDF::Layout::Style::Border do
147
147
  assert_kind_of(HexaPDF::Layout::Style::Quad, border.style)
148
148
  end
149
149
 
150
+ it "can be duplicated" do
151
+ border = create_border
152
+ copy = border.dup
153
+ border.width.top = 10
154
+ border.color.top = :red
155
+ border.style.top = :dotted
156
+ assert_equal(0, copy.width.top)
157
+ assert_equal(0, copy.color.top)
158
+ assert_equal(:solid, copy.style.top)
159
+ end
160
+
150
161
  it "can be asked whether a border is defined" do
151
162
  assert(create_border.none?)
152
163
  refute(create_border(width: 5).none?)
@@ -436,6 +447,12 @@ describe HexaPDF::Layout::Style::Layers do
436
447
  assert_equal(data, layers.enum_for(:each, {}).to_a)
437
448
  end
438
449
 
450
+ it "can be duplicated" do
451
+ copy = @layers.dup
452
+ @layers.add(lambda {})
453
+ assert(copy.none?)
454
+ end
455
+
439
456
  describe "add and each" do
440
457
  it "can use a given block" do
441
458
  block = proc { true }
@@ -577,6 +594,45 @@ describe HexaPDF::Layout::Style do
577
594
  assert_equal(10, style.font_size)
578
595
  end
579
596
 
597
+ describe "initialize_copy" do
598
+ it "can be duplicated" do
599
+ @style.font_features[:kerning] = true
600
+ @style.padding.top = 10
601
+ @style.margin.top = 10
602
+ @style.border.width.top = 10
603
+ @style.overlays.add(lambda {})
604
+ @style.underlays.add(lambda {})
605
+
606
+ copy = @style.dup
607
+ @style.font_features[:kerning] = false
608
+ @style.padding.top = 5
609
+ @style.margin.top = 5
610
+ @style.border.width.top = 5
611
+ @style.overlays.add(lambda {})
612
+ @style.underlays.add(lambda {})
613
+
614
+ assert_equal({kerning: true}, copy.font_features)
615
+ assert_equal(10, copy.padding.top)
616
+ assert_equal(10, copy.margin.top)
617
+ assert_equal(10, copy.border.width.top)
618
+ assert_equal(1, copy.underlays.instance_variable_get(:@layers).size)
619
+ assert_equal(1, copy.overlays.instance_variable_get(:@layers).size)
620
+ end
621
+
622
+ it "resets the cache" do
623
+ @style.horizontal_scaling(200)
624
+ assert_equal(2.0, @style.scaled_horizontal_scaling)
625
+ assert_equal(-1.06, @style.scaled_item_width(53))
626
+
627
+ style = @style.dup
628
+ style.horizontal_scaling(100)
629
+ assert_equal(2.0, @style.scaled_horizontal_scaling)
630
+ assert_equal(-1.06, @style.scaled_item_width(53))
631
+ assert_equal(1.0, style.scaled_horizontal_scaling)
632
+ assert_equal(-0.53, style.scaled_item_width(53))
633
+ end
634
+ end
635
+
580
636
  it "has several simple and dynamically generated properties with default values" do
581
637
  assert_raises(HexaPDF::Error) { @style.font }
582
638
  assert_equal(10, @style.font_size)
@@ -607,6 +663,7 @@ describe HexaPDF::Layout::Style do
607
663
  assert_equal([:proportional, 1], [@style.line_spacing.type, @style.line_spacing.value])
608
664
  refute(@style.subscript)
609
665
  refute(@style.superscript)
666
+ refute(@style.last_line_gap)
610
667
  assert_kind_of(HexaPDF::Layout::Style::Layers, @style.underlays)
611
668
  assert_kind_of(HexaPDF::Layout::Style::Layers, @style.overlays)
612
669
  end
@@ -8,6 +8,7 @@ require 'hexapdf/layout/text_box'
8
8
  describe HexaPDF::Layout::TextBox do
9
9
  before do
10
10
  @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
11
+ @inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {}
11
12
  end
12
13
 
13
14
  def create_box(items, **kwargs)
@@ -22,15 +23,19 @@ describe HexaPDF::Layout::TextBox do
22
23
  end
23
24
 
24
25
  describe "fit" do
25
- before do
26
- @inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {}
26
+ it "fits into a rectangular area" do
27
+ box = create_box([@inline_box] * 5, style: {padding: 10})
28
+ assert(box.fit(100, 100, @frame))
29
+ assert_equal(70, box.width)
30
+ assert_equal(30, box.height)
27
31
  end
28
32
 
29
- it "fits into a rectangular area" do
30
- box = create_box([@inline_box] * 5)
33
+ it "respects the set width and height" do
34
+ box = create_box([@inline_box] * 5, width: 40, height: 50, style: {padding: 10})
31
35
  assert(box.fit(100, 100, @frame))
32
- assert_equal(50, box.width)
33
- assert_equal(10, box.height)
36
+ assert_equal(40, box.width)
37
+ assert_equal(50, box.height)
38
+ assert_equal([20, 20, 10], box.instance_variable_get(:@result).lines.map(&:width))
34
39
  end
35
40
 
36
41
  it "fits into the frame's outline" do
@@ -40,30 +45,59 @@ describe HexaPDF::Layout::TextBox do
40
45
  assert_equal(100, box.width)
41
46
  assert_equal(20, box.height)
42
47
  end
48
+
49
+ it "takes the style option last_line_gap into account" do
50
+ box = create_box([@inline_box] * 5, style: {last_line_gap: true, line_spacing: :double})
51
+ assert(box.fit(100, 100, @frame))
52
+ assert_equal(50, box.width)
53
+ assert_equal(20, box.height)
54
+ end
55
+ end
56
+
57
+ describe "split" do
58
+ it "works for an empty text box" do
59
+ box = create_box([])
60
+ assert_equal([box], box.split(100, 100, @frame))
61
+ end
62
+
63
+ it "doesn't need to split the box if it completely fits" do
64
+ box = create_box([@inline_box] * 5)
65
+ assert_equal([box], box.split(50, 100, @frame))
66
+ end
67
+
68
+ it "works if no item of the text box fits" do
69
+ box = create_box([@inline_box])
70
+ assert_equal([nil, box], box.split(5, 20, @frame))
71
+ end
72
+
73
+ it "splits the box if necessary" do
74
+ box = create_box([@inline_box] * 10)
75
+ boxes = box.split(50, 10, @frame)
76
+ assert_equal(2, boxes.length)
77
+ assert_equal(box, boxes[0])
78
+ assert_equal(5, boxes[1].instance_variable_get(:@items).length)
79
+ end
43
80
  end
44
81
 
45
82
  describe "draw" do
46
83
  it "draws the layed out inline items onto the canvas" do
47
84
  inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10,
48
85
  border: {width: 1})
49
- box = create_box([inline_box], width: 100, height: 10)
86
+ box = create_box([inline_box], width: 100, height: 30, style: {padding: [10, 5]})
50
87
  box.fit(100, 100, nil)
51
88
 
52
89
  @canvas = HexaPDF::Document.new.pages.add.canvas
53
90
  box.draw(@canvas, 0, 0)
54
91
  assert_operators(@canvas.contents, [[:save_graphics_state],
55
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
56
- [:save_graphics_state],
57
92
  [:restore_graphics_state],
58
93
  [:save_graphics_state],
59
- [:append_rectangle, [0, 0, 10, 10]],
94
+ [:append_rectangle, [5, 10, 10, 10]],
60
95
  [:clip_path_non_zero],
61
96
  [:end_path],
62
- [:append_rectangle, [0.5, 0.5, 9, 9]],
97
+ [:append_rectangle, [5.5, 10.5, 9, 9]],
63
98
  [:stroke_path],
64
99
  [:restore_graphics_state],
65
100
  [:save_graphics_state],
66
- [:restore_graphics_state],
67
101
  [:restore_graphics_state]])
68
102
  end
69
103
 
@@ -74,4 +108,15 @@ describe HexaPDF::Layout::TextBox do
74
108
  assert_operators(@canvas.contents, [])
75
109
  end
76
110
  end
111
+
112
+ it "is empty if there is a result without any text lines" do
113
+ box = create_box([])
114
+ assert(box.empty?)
115
+ box.fit(100, 100, @frame)
116
+ assert(box.empty?)
117
+
118
+ box = create_box([@inline_box])
119
+ box.fit(100, 100, @frame)
120
+ refute(box.empty?)
121
+ end
77
122
  end
@@ -308,8 +308,8 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
308
308
 
309
309
  it "handles changing widths" do
310
310
  height = 0
311
- width_block = lambda do |line_height|
312
- case height + line_height
311
+ width_block = lambda do |line|
312
+ case height + line.height
313
313
  when 0..10 then 60
314
314
  when 11..20 then 40
315
315
  when 21..30 then 20
@@ -331,8 +331,8 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
331
331
 
332
332
  it "handles changing widths when breaking on a penalty" do
333
333
  height = 0
334
- width_block = lambda do |line_height|
335
- case height + line_height
334
+ width_block = lambda do |line|
335
+ case height + line.height
336
336
  when 0..10 then 80
337
337
  else 50
338
338
  end
@@ -419,6 +419,16 @@ describe HexaPDF::Layout::TextLayouter do
419
419
  assert_equal(20 * (5 + 4), result.height)
420
420
  end
421
421
 
422
+ it "takes line spacing into account with variable width" do
423
+ @style.line_spacing = :double
424
+ width_block = lambda {|l, h| l + h <= 90 ? 40 : 20 }
425
+ result = @layouter.fit(boxes(*([[20, 20]] * 6)), width_block, 170)
426
+ assert(result.remaining_items.empty?)
427
+ assert_equal(:success, result.status)
428
+ assert_line_wrapping([[], result.lines], [40, 40, 20, 20])
429
+ assert_equal(140, result.height)
430
+ end
431
+
422
432
  it "handles empty lines" do
423
433
  items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
424
434
  boxes([20, 20]) + [penalty(-5000)] * 2
@@ -32,11 +32,11 @@ describe HexaPDF::Task::Optimize do
32
32
  end
33
33
 
34
34
  def assert_no_objstms
35
- assert(@doc.each(current: false).all? {|obj| obj.type != :ObjStm })
35
+ assert(@doc.each(only_current: false).all? {|obj| obj.type != :ObjStm })
36
36
  end
37
37
 
38
38
  def assert_no_xrefstms
39
- assert(@doc.each(current: false).all? {|obj| obj.type != :XRef })
39
+ assert(@doc.each(only_current: false).all? {|obj| obj.type != :XRef })
40
40
  end
41
41
 
42
42
  def assert_default_deleted
@@ -47,7 +47,7 @@ describe HexaPDF::Task::Optimize do
47
47
  it "compacts the document" do
48
48
  @doc.task(:optimize, compact: true)
49
49
  assert_equal(1, @doc.revisions.size)
50
- assert_equal(2, @doc.each(current: false).to_a.size)
50
+ assert_equal(2, @doc.each(only_current: false).to_a.size)
51
51
  refute_equal(@obj2, @doc.object(@obj2))
52
52
  refute_equal(@obj3, @doc.object(@obj3))
53
53
  assert_default_deleted
@@ -0,0 +1,258 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative 'content/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/composer'
7
+ require 'stringio'
8
+
9
+ describe HexaPDF::Composer do
10
+ before do
11
+ @composer = HexaPDF::Composer.new
12
+ end
13
+
14
+ describe "initialize" do
15
+ it "creates a composer object with default values" do
16
+ assert_kind_of(HexaPDF::Document, @composer.document)
17
+ assert_kind_of(HexaPDF::Type::Page, @composer.page)
18
+ assert_equal(36, @composer.frame.left)
19
+ assert_equal(36, @composer.frame.bottom)
20
+ assert_equal(523, @composer.frame.width)
21
+ assert_equal(770, @composer.frame.height)
22
+ assert_equal("Times", @composer.base_style.font)
23
+ end
24
+
25
+ it "allows the customization of the page size" do
26
+ composer = HexaPDF::Composer.new(page_size: [0, 0, 100, 100])
27
+ assert_equal([0, 0, 100, 100], composer.page.box.value)
28
+ end
29
+
30
+ it "allows the customization of the page orientation" do
31
+ composer = HexaPDF::Composer.new(page_orientation: :landscape)
32
+ assert_equal([0, 0, 842, 595], composer.page.box.value)
33
+ end
34
+
35
+ it "allows the customization of the margin" do
36
+ composer = HexaPDF::Composer.new(margin: [100, 80, 60, 40])
37
+ assert_equal(40, composer.frame.left)
38
+ assert_equal(60, composer.frame.bottom)
39
+ assert_equal(475, composer.frame.width)
40
+ assert_equal(682, composer.frame.height)
41
+ end
42
+
43
+ it "yields itself" do
44
+ yielded = nil
45
+ composer = HexaPDF::Composer.new {|c| yielded = c }
46
+ assert_same(composer, yielded)
47
+ end
48
+ end
49
+
50
+ describe "::create" do
51
+ it "creates, yields, and writes a document" do
52
+ io = StringIO.new
53
+ HexaPDF::Composer.create(io, &:new_page)
54
+ io.rewind
55
+ assert_equal(2, HexaPDF::Document.new(io: io).pages.count)
56
+ end
57
+ end
58
+
59
+ describe "new_page" do
60
+ it "creates a new page with the stored information" do
61
+ c = HexaPDF::Composer.new(page_size: [0, 0, 50, 100], margin: 10)
62
+ c.new_page
63
+ assert_equal([0, 0, 50, 100], c.page.box.value)
64
+ assert_equal(10, c.frame.left)
65
+ assert_equal(10, c.frame.bottom)
66
+ end
67
+
68
+ it "uses the provided information for the new and all following pages" do
69
+ @composer.new_page(page_size: [0, 0, 50, 100], margin: 10)
70
+ assert_equal([0, 0, 50, 100], @composer.page.box.value)
71
+ assert_equal(10, @composer.frame.left)
72
+ assert_equal(10, @composer.frame.bottom)
73
+ @composer.new_page
74
+ assert_same(@composer.document.pages[2], @composer.page)
75
+ assert_equal([0, 0, 50, 100], @composer.page.box.value)
76
+ assert_equal(10, @composer.frame.left)
77
+ assert_equal(10, @composer.frame.bottom)
78
+ end
79
+ end
80
+
81
+ it "returns the current x-position" do
82
+ assert_equal(36, @composer.x)
83
+ end
84
+
85
+ it "returns the current y-position" do
86
+ assert_equal(806, @composer.y)
87
+ end
88
+
89
+ describe "text" do
90
+ it "creates a text box and draws it on the canvas" do
91
+ box = nil
92
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
93
+
94
+ @composer.text("Test", width: 10, height: 15)
95
+ assert_equal(10, box.width)
96
+ assert_equal(15, box.height)
97
+ assert_same(@composer.document.fonts.add("Times"), box.style.font)
98
+ items = box.instance_variable_get(:@items)
99
+ assert_equal(1, items.length)
100
+ assert_same(box.style, items.first.style)
101
+ end
102
+
103
+ it "allows setting of a custom style" do
104
+ box = nil
105
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
106
+
107
+ @composer.text("Test", style: HexaPDF::Layout::Style.new(font_size: 20))
108
+ assert_same(@composer.document.fonts.add("Times"), box.style.font)
109
+ assert_equal(20, box.style.font_size)
110
+ end
111
+
112
+ it "updates the used style with the provided options" do
113
+ box = nil
114
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
115
+
116
+ @composer.text("Test", style: HexaPDF::Layout::Style.new, font_size: 20)
117
+ assert_equal(20, box.style.font_size)
118
+ end
119
+ end
120
+
121
+ describe "formatted_text" do
122
+ it "creates a text box with the formatted text and draws it on the canvas" do
123
+ box = nil
124
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
125
+
126
+ @composer.formatted_text(["Test"], width: 10, height: 15)
127
+ assert_equal(10, box.width)
128
+ assert_equal(15, box.height)
129
+ assert_equal(1, box.instance_variable_get(:@items).length)
130
+ end
131
+
132
+ it "a hash can be used for custom style properties" do
133
+ box = nil
134
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
135
+
136
+ @composer.formatted_text([{text: "Test", font_size: 20}], align: :center)
137
+ items = box.instance_variable_get(:@items)
138
+ assert_equal(1, items.length)
139
+ assert_equal(20, items.first.style.font_size)
140
+ assert_equal(:center, items.first.style.align)
141
+ assert_equal(10, box.style.font_size)
142
+ end
143
+
144
+ it "a hash can be used to provide a custom style" do
145
+ box = nil
146
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
147
+
148
+ @composer.formatted_text([{text: "Test", style: HexaPDF::Layout::Style.new(fill_color: 128),
149
+ font_size: 20}], align: :center)
150
+ items = box.instance_variable_get(:@items)
151
+ assert_equal(20, items.first.style.font_size)
152
+ assert_equal(128, items.first.style.fill_color)
153
+ assert_equal(:center, items.first.style.align)
154
+ end
155
+
156
+ it "a hash can be used to link to an URL" do
157
+ box = nil
158
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
159
+
160
+ @composer.formatted_text([{text: "Test", link: "URI"}, {link: "URI"}])
161
+ items = box.instance_variable_get(:@items)
162
+ assert_equal(2, items.length)
163
+ assert_equal(4, items[0].items.length)
164
+ assert_equal(3, items[1].items.length)
165
+ assert_equal([:link, {uri: 'URI'}], items[0].style.overlays.instance_variable_get(:@layers)[0])
166
+ end
167
+ end
168
+
169
+ describe "image" do
170
+ it "creates an image box and draws it on the canvas" do
171
+ box = nil
172
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
173
+ image_path = File.join(TEST_DATA_DIR, 'images', 'gray.jpg')
174
+
175
+ @composer.image(image_path, width: 10, height: 15)
176
+ assert_equal(10, box.width)
177
+ assert_equal(15, box.height)
178
+ assert_same(@composer.document.images.add(image_path), box.image)
179
+ end
180
+ end
181
+
182
+ describe "draw_box" do
183
+ def create_box(**kwargs)
184
+ HexaPDF::Layout::Box.new(kwargs) {}
185
+ end
186
+
187
+ it "draws the box if it completely fits" do
188
+ @composer.draw_box(create_box(height: 100))
189
+ @composer.draw_box(create_box)
190
+ assert_operators(@composer.canvas.contents,
191
+ [[:save_graphics_state],
192
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 706]],
193
+ [:restore_graphics_state],
194
+ [:save_graphics_state],
195
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 36]],
196
+ [:restore_graphics_state]])
197
+ end
198
+
199
+ it "draws the box on a new page if the frame is already full" do
200
+ first_page_canvas = @composer.canvas
201
+ @composer.draw_box(create_box)
202
+ @composer.draw_box(create_box)
203
+ refute_same(first_page_canvas, @composer.canvas)
204
+ assert_operators(@composer.canvas.contents,
205
+ [[:save_graphics_state],
206
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 36]],
207
+ [:restore_graphics_state]])
208
+ end
209
+
210
+ it "splits the box across two pages" do
211
+ first_page_contents = @composer.canvas.contents
212
+ @composer.draw_box(create_box(height: 400))
213
+
214
+ box = create_box(height: 400)
215
+ box.define_singleton_method(:split) do |*|
216
+ [box, HexaPDF::Layout::Box.new(height: 100) {}]
217
+ end
218
+ @composer.draw_box(box)
219
+ assert_operators(first_page_contents,
220
+ [[:save_graphics_state],
221
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 406]],
222
+ [:restore_graphics_state],
223
+ [:save_graphics_state],
224
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 6]],
225
+ [:restore_graphics_state]])
226
+ assert_operators(@composer.canvas.contents,
227
+ [[:save_graphics_state],
228
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 706]],
229
+ [:restore_graphics_state]])
230
+ end
231
+
232
+ it "finds a new region if splitting didn't work" do
233
+ first_page_contents = @composer.canvas.contents
234
+ @composer.draw_box(create_box(height: 400))
235
+ @composer.draw_box(create_box(height: 100, width: 300, style: {position: :float}))
236
+
237
+ box = create_box(width: 400, height: 400)
238
+ @composer.draw_box(box)
239
+ assert_operators(first_page_contents,
240
+ [[:save_graphics_state],
241
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 406]],
242
+ [:restore_graphics_state],
243
+ [:save_graphics_state],
244
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 306]],
245
+ [:restore_graphics_state]])
246
+ assert_operators(@composer.canvas.contents,
247
+ [[:save_graphics_state],
248
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 406]],
249
+ [:restore_graphics_state]])
250
+ end
251
+
252
+ it "raises an error if a box doesn't fit onto an empty page" do
253
+ assert_raises(HexaPDF::Error) do
254
+ @composer.draw_box(create_box(height: 800))
255
+ end
256
+ end
257
+ end
258
+ end