hexapdf 0.32.1 → 0.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/README.md +9 -0
  4. data/examples/002-graphics.rb +15 -17
  5. data/examples/003-arcs.rb +9 -9
  6. data/examples/009-text_layouter_alignment.rb +1 -1
  7. data/examples/010-text_layouter_inline_boxes.rb +2 -2
  8. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  9. data/examples/012-text_layouter_styling.rb +7 -7
  10. data/examples/013-text_layouter_shapes.rb +1 -1
  11. data/examples/014-text_in_polygon.rb +1 -1
  12. data/examples/015-boxes.rb +8 -7
  13. data/examples/016-frame_automatic_box_placement.rb +2 -2
  14. data/examples/017-frame_text_flow.rb +2 -1
  15. data/examples/018-composer.rb +1 -1
  16. data/examples/020-column_box.rb +2 -1
  17. data/examples/025-table_box.rb +46 -0
  18. data/lib/hexapdf/cli/command.rb +5 -2
  19. data/lib/hexapdf/cli/form.rb +5 -5
  20. data/lib/hexapdf/cli/inspect.rb +3 -3
  21. data/lib/hexapdf/cli.rb +4 -0
  22. data/lib/hexapdf/composer.rb +104 -52
  23. data/lib/hexapdf/configuration.rb +44 -39
  24. data/lib/hexapdf/content/canvas.rb +393 -267
  25. data/lib/hexapdf/content/color_space.rb +72 -25
  26. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  27. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  28. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  29. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  30. data/lib/hexapdf/content/graphic_object.rb +6 -7
  31. data/lib/hexapdf/content/graphics_state.rb +54 -45
  32. data/lib/hexapdf/content/operator.rb +52 -54
  33. data/lib/hexapdf/content/parser.rb +2 -2
  34. data/lib/hexapdf/content/processor.rb +15 -15
  35. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  36. data/lib/hexapdf/content.rb +5 -0
  37. data/lib/hexapdf/dictionary.rb +6 -5
  38. data/lib/hexapdf/dictionary_fields.rb +42 -14
  39. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  40. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  41. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  42. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  43. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  44. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  45. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  46. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  47. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  48. data/lib/hexapdf/digital_signature/verification_result.rb +2 -2
  49. data/lib/hexapdf/digital_signature.rb +7 -2
  50. data/lib/hexapdf/document/destinations.rb +12 -11
  51. data/lib/hexapdf/document/files.rb +1 -1
  52. data/lib/hexapdf/document/fonts.rb +1 -1
  53. data/lib/hexapdf/document/layout.rb +167 -39
  54. data/lib/hexapdf/document/pages.rb +3 -2
  55. data/lib/hexapdf/document.rb +89 -55
  56. data/lib/hexapdf/encryption/aes.rb +5 -5
  57. data/lib/hexapdf/encryption/arc4.rb +1 -1
  58. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  59. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  60. data/lib/hexapdf/encryption/identity.rb +1 -1
  61. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  62. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  63. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  64. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  65. data/lib/hexapdf/encryption.rb +7 -2
  66. data/lib/hexapdf/error.rb +18 -0
  67. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  68. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  69. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  70. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  71. data/lib/hexapdf/filter/pass_through.rb +1 -1
  72. data/lib/hexapdf/filter/predictor.rb +1 -1
  73. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  74. data/lib/hexapdf/filter.rb +55 -6
  75. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  76. data/lib/hexapdf/font/cmap.rb +1 -1
  77. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  79. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  80. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  82. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  83. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  84. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  85. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  86. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  87. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  88. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  89. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  90. data/lib/hexapdf/font_loader.rb +3 -0
  91. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  92. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  93. data/lib/hexapdf/image_loader/png.rb +2 -2
  94. data/lib/hexapdf/image_loader.rb +1 -1
  95. data/lib/hexapdf/importer.rb +13 -0
  96. data/lib/hexapdf/layout/box.rb +9 -2
  97. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  98. data/lib/hexapdf/layout/column_box.rb +18 -4
  99. data/lib/hexapdf/layout/frame.rb +30 -12
  100. data/lib/hexapdf/layout/image_box.rb +5 -0
  101. data/lib/hexapdf/layout/inline_box.rb +1 -0
  102. data/lib/hexapdf/layout/list_box.rb +17 -1
  103. data/lib/hexapdf/layout/page_style.rb +4 -4
  104. data/lib/hexapdf/layout/style.rb +18 -3
  105. data/lib/hexapdf/layout/table_box.rb +682 -0
  106. data/lib/hexapdf/layout/text_box.rb +5 -3
  107. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  108. data/lib/hexapdf/layout/text_layouter.rb +12 -4
  109. data/lib/hexapdf/layout.rb +1 -0
  110. data/lib/hexapdf/name_tree_node.rb +1 -1
  111. data/lib/hexapdf/number_tree_node.rb +1 -1
  112. data/lib/hexapdf/object.rb +18 -7
  113. data/lib/hexapdf/parser.rb +8 -8
  114. data/lib/hexapdf/pdf_array.rb +1 -1
  115. data/lib/hexapdf/rectangle.rb +1 -1
  116. data/lib/hexapdf/reference.rb +1 -1
  117. data/lib/hexapdf/revision.rb +1 -1
  118. data/lib/hexapdf/revisions.rb +3 -3
  119. data/lib/hexapdf/serializer.rb +15 -15
  120. data/lib/hexapdf/stream.rb +4 -2
  121. data/lib/hexapdf/tokenizer.rb +14 -14
  122. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  123. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  124. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  125. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  126. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  127. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  128. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  129. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  130. data/lib/hexapdf/type/acro_form.rb +1 -1
  131. data/lib/hexapdf/type/action.rb +1 -1
  132. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  133. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  134. data/lib/hexapdf/type/actions/launch.rb +1 -1
  135. data/lib/hexapdf/type/actions/uri.rb +1 -1
  136. data/lib/hexapdf/type/actions.rb +1 -1
  137. data/lib/hexapdf/type/annotation.rb +3 -3
  138. data/lib/hexapdf/type/annotations/link.rb +1 -1
  139. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  140. data/lib/hexapdf/type/annotations/text.rb +1 -1
  141. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  142. data/lib/hexapdf/type/annotations.rb +1 -1
  143. data/lib/hexapdf/type/catalog.rb +1 -1
  144. data/lib/hexapdf/type/cid_font.rb +3 -3
  145. data/lib/hexapdf/type/embedded_file.rb +1 -1
  146. data/lib/hexapdf/type/file_specification.rb +2 -2
  147. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  148. data/lib/hexapdf/type/font_simple.rb +2 -2
  149. data/lib/hexapdf/type/font_type0.rb +3 -3
  150. data/lib/hexapdf/type/font_type3.rb +1 -1
  151. data/lib/hexapdf/type/form.rb +1 -1
  152. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  153. data/lib/hexapdf/type/icon_fit.rb +1 -1
  154. data/lib/hexapdf/type/image.rb +1 -1
  155. data/lib/hexapdf/type/info.rb +1 -1
  156. data/lib/hexapdf/type/mark_information.rb +1 -1
  157. data/lib/hexapdf/type/names.rb +2 -2
  158. data/lib/hexapdf/type/object_stream.rb +7 -3
  159. data/lib/hexapdf/type/outline.rb +1 -1
  160. data/lib/hexapdf/type/outline_item.rb +1 -1
  161. data/lib/hexapdf/type/page.rb +19 -10
  162. data/lib/hexapdf/type/page_label.rb +1 -1
  163. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  164. data/lib/hexapdf/type/resources.rb +1 -1
  165. data/lib/hexapdf/type/trailer.rb +2 -2
  166. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  167. data/lib/hexapdf/type/xref_stream.rb +2 -2
  168. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  169. data/lib/hexapdf/version.rb +1 -1
  170. data/lib/hexapdf/writer.rb +4 -4
  171. data/lib/hexapdf/xref_section.rb +2 -2
  172. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  173. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  174. data/test/hexapdf/content/test_canvas.rb +0 -1
  175. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  176. data/test/hexapdf/document/test_files.rb +2 -2
  177. data/test/hexapdf/document/test_layout.rb +98 -0
  178. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  179. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  180. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  181. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  182. data/test/hexapdf/layout/test_box.rb +1 -1
  183. data/test/hexapdf/layout/test_column_box.rb +65 -21
  184. data/test/hexapdf/layout/test_frame.rb +14 -14
  185. data/test/hexapdf/layout/test_image_box.rb +4 -0
  186. data/test/hexapdf/layout/test_inline_box.rb +5 -0
  187. data/test/hexapdf/layout/test_list_box.rb +40 -6
  188. data/test/hexapdf/layout/test_page_style.rb +3 -2
  189. data/test/hexapdf/layout/test_style.rb +50 -0
  190. data/test/hexapdf/layout/test_table_box.rb +722 -0
  191. data/test/hexapdf/layout/test_text_box.rb +18 -0
  192. data/test/hexapdf/layout/test_text_layouter.rb +4 -0
  193. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  194. data/test/hexapdf/test_document.rb +1 -0
  195. data/test/hexapdf/test_filter.rb +8 -0
  196. data/test/hexapdf/test_importer.rb +9 -0
  197. data/test/hexapdf/test_object.rb +16 -5
  198. data/test/hexapdf/test_parser.rb +1 -1
  199. data/test/hexapdf/test_stream.rb +7 -0
  200. data/test/hexapdf/test_writer.rb +3 -3
  201. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  202. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  203. data/test/hexapdf/type/test_object_stream.rb +9 -3
  204. data/test/hexapdf/type/test_page.rb +18 -4
  205. metadata +17 -8
@@ -0,0 +1,722 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/layout/table_box'
6
+
7
+ describe HexaPDF::Layout::TableBox::Cell do
8
+ def create_cell(**kwargs)
9
+ HexaPDF::Layout::TableBox::Cell.new(row: 1, column: 1, **kwargs)
10
+ end
11
+
12
+ describe "initialize" do
13
+ it "creates a new instance with the given arguments" do
14
+ cell = create_cell(children: [:a], row: 5, column: 3, row_span: 7, col_span: 2,
15
+ style: {background_color: 'blue', padding: 3, border: {width: 3}})
16
+ assert_equal([:a], cell.children)
17
+ assert_equal(5, cell.row)
18
+ assert_equal(3, cell.column)
19
+ assert_equal(7, cell.row_span)
20
+ assert_equal(2, cell.col_span)
21
+ assert_equal('blue', cell.style.background_color)
22
+ assert_equal(3, cell.style.padding.left)
23
+ assert_equal(3, cell.style.border.width.left)
24
+ end
25
+
26
+ it "uses defaults for attributes that were not given" do
27
+ cell = create_cell
28
+ assert_equal(1, cell.row_span)
29
+ assert_equal(1, cell.col_span)
30
+ end
31
+
32
+ it "uses defaults for the border and padding" do
33
+ cell = create_cell
34
+ assert(cell.style.border.width.simple?)
35
+ assert_equal(1, cell.style.border.width.left)
36
+ assert(cell.style.padding.simple?)
37
+ assert_equal(5, cell.style.padding.left)
38
+ end
39
+ end
40
+
41
+ describe "empty?" do
42
+ it "returns true if the cell has not been fit yet" do
43
+ cell = create_cell(children: [:a], style: {border: {width: 0}})
44
+ assert(cell.empty?)
45
+ end
46
+
47
+ it "returns true if the cell has no content" do
48
+ cell = create_cell(children: nil, style: {border: {width: 0}})
49
+ cell.fit(100, 100, nil)
50
+ assert(cell.empty?)
51
+ end
52
+ end
53
+
54
+ describe "update_height" do
55
+ it "updates the height to the correct one" do
56
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 10, height: 10))
57
+ cell.fit(100, 100, nil)
58
+ assert_equal(22, cell.height)
59
+ cell.update_height(50)
60
+ assert_equal(50, cell.height)
61
+ end
62
+
63
+ it "fails if the given height is smaller than the one determined during #fit" do
64
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 10, height: 10))
65
+ cell.fit(100, 100, nil)
66
+ err = assert_raises(HexaPDF::Error) { cell.update_height(5) }
67
+ assert_match(/at least as big/, err.message)
68
+ end
69
+ end
70
+
71
+ describe "fit" do
72
+ it "fits a single box" do
73
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 10))
74
+ cell.fit(100, 100, nil)
75
+ assert_equal(100, cell.width)
76
+ assert_equal(22, cell.height)
77
+ assert_equal(32, cell.preferred_width)
78
+ assert_equal(22, cell.preferred_height)
79
+ end
80
+
81
+ it "fits a single box with horizontal aligning not being :left" do
82
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center))
83
+ cell.fit(100, 100, nil)
84
+ assert_equal(66, cell.preferred_width)
85
+ end
86
+
87
+ it "fits multiple boxes" do
88
+ box1 = HexaPDF::Layout::Box.create(width: 20, height: 10)
89
+ box2 = HexaPDF::Layout::Box.create(width: 50, height: 15)
90
+ cell = create_cell(children: [box1, box2])
91
+ cell.fit(100, 100, nil)
92
+ assert_equal(100, cell.width)
93
+ assert_equal(37, cell.height)
94
+ assert_equal(62, cell.preferred_width)
95
+ assert_equal(37, cell.preferred_height)
96
+ end
97
+
98
+ it "fits multiple boxes with horizontal aligning not being :left" do
99
+ box1 = HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center)
100
+ box2 = HexaPDF::Layout::Box.create(width: 50, height: 15)
101
+ cell = create_cell(children: [box1, box2])
102
+ cell.fit(100, 100, nil)
103
+ assert_equal(66, cell.preferred_width)
104
+ end
105
+
106
+ it "fits the cell even if it has no content" do
107
+ cell = create_cell(children: nil)
108
+ cell.fit(100, 100, nil)
109
+ assert_equal(100, cell.width)
110
+ assert_equal(12, cell.height)
111
+ assert_equal(12, cell.preferred_width)
112
+ assert_equal(12, cell.preferred_height)
113
+ end
114
+
115
+ it "doesn't fit anything if the available width or height are too small" do
116
+ cell = create_cell(children: nil)
117
+ refute(cell.fit(10, 100, nil))
118
+ refute(cell.fit(100, 10, nil))
119
+ end
120
+ end
121
+
122
+ describe "draw" do
123
+ before do
124
+ @canvas = HexaPDF::Document.new.pages.add.canvas
125
+ end
126
+
127
+ it "draws the boxes at the correct location" do
128
+ draw_block = lambda {|canvas, _| canvas.move_to(0, 0).end_path }
129
+ box1 = HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center, &draw_block)
130
+ box2 = HexaPDF::Layout::Box.create(width: 50, height: 15, &draw_block)
131
+ box = create_cell(children: [box1, box2])
132
+ box.fit(100, 100, nil)
133
+ box.draw(@canvas, 10, 75)
134
+ operators = [[:save_graphics_state],
135
+ [:append_rectangle, [9.5, 74.5, 101.0, 38.0]],
136
+ [:clip_path_non_zero],
137
+ [:end_path],
138
+ [:append_rectangle, [10.0, 75.0, 100.0, 37.0]],
139
+ [:stroke_path],
140
+ [:restore_graphics_state],
141
+ [:save_graphics_state],
142
+ [:concatenate_matrix, [1, 0, 0, 1, 50.0, 96]],
143
+ [:move_to, [0, 0]],
144
+ [:end_path],
145
+ [:restore_graphics_state],
146
+ [:save_graphics_state],
147
+ [:concatenate_matrix, [1, 0, 0, 1, 16, 81]],
148
+ [:move_to, [0, 0]],
149
+ [:end_path],
150
+ [:restore_graphics_state]]
151
+ assert_operators(@canvas.contents, operators)
152
+ end
153
+
154
+ it "works for a cell without content" do
155
+ box = create_cell(children: nil, style: {border: {width: 0}})
156
+ box.fit(100, 100, nil)
157
+ box.draw(@canvas, 10, 75)
158
+ assert_operators(@canvas.contents, [])
159
+ end
160
+ end
161
+
162
+ it "returns a useful representation when inspecting" do
163
+ cell = create_cell(row: 3, column: 2, row_span: 2, col_span: 3, children: [:a, "b"])
164
+ assert_equal("<Cell (3,2) 2x3 [Symbol, String]>", cell.inspect)
165
+ end
166
+ end
167
+
168
+ describe HexaPDF::Layout::TableBox::Cells do
169
+ def create_cells(data, cell_style = nil)
170
+ HexaPDF::Layout::TableBox::Cells.new(data, cell_style: cell_style)
171
+ end
172
+
173
+ describe "intialize" do
174
+ it "works with simple data" do
175
+ cells = create_cells([[:a]])
176
+ assert_equal(1, cells.number_of_columns)
177
+ assert_equal(1, cells.number_of_rows)
178
+ assert_equal(:a, cells[0, 0].children)
179
+
180
+ cells = create_cells([[:a, :b, :c]])
181
+ assert_equal(3, cells.number_of_columns)
182
+ assert_equal(1, cells.number_of_rows)
183
+ assert_equal(:a, cells[0, 0].children)
184
+ assert_equal(:b, cells[0, 1].children)
185
+ assert_equal(:c, cells[0, 2].children)
186
+
187
+ cells = create_cells([[:a], [:b], [:c]])
188
+ assert_equal(1, cells.number_of_columns)
189
+ assert_equal(3, cells.number_of_rows)
190
+ assert_equal(:a, cells[0, 0].children)
191
+ assert_equal(:b, cells[1, 0].children)
192
+ assert_equal(:c, cells[2, 0].children)
193
+
194
+ cells = create_cells([[:a, :b], [:c, :d, :e], [:f]])
195
+ assert_equal(3, cells.number_of_columns)
196
+ assert_equal(3, cells.number_of_rows)
197
+ assert_equal(:a, cells[0, 0].children)
198
+ assert_equal(:b, cells[0, 1].children)
199
+ assert_nil(cells[0, 2])
200
+ assert_equal(:c, cells[1, 0].children)
201
+ assert_equal(:d, cells[1, 1].children)
202
+ assert_equal(:e, cells[1, 2].children)
203
+ assert_equal(:f, cells[2, 0].children)
204
+ assert_nil(cells[2, 1])
205
+ assert_nil(cells[2, 2])
206
+ end
207
+
208
+ it "can handle column spans" do
209
+ cells = create_cells([[{col_span: 2, content: :a}, :b], [:c, {col_span: 3, content: :d}]])
210
+ assert_equal(4, cells.number_of_columns)
211
+ assert_equal(2, cells.number_of_rows)
212
+ assert_equal(:a, cells[0, 0].children)
213
+ assert_same(cells[0, 0], cells[0, 1])
214
+ assert_equal(:b, cells[0, 2].children)
215
+ assert_equal(:c, cells[1, 0].children)
216
+ assert_equal(:d, cells[1, 1].children)
217
+ assert_same(cells[1, 1], cells[1, 2])
218
+ assert_same(cells[1, 1], cells[1, 3])
219
+ end
220
+
221
+ it "can handle row spans" do
222
+ cells = create_cells([[{row_span: 2, content: :a}, :b], [{row_span: 2, content: :c}], [:d]])
223
+ assert_equal(2, cells.number_of_columns)
224
+ assert_equal(3, cells.number_of_rows)
225
+ assert_equal(:a, cells[0, 0].children)
226
+ assert_equal(:b, cells[0, 1].children)
227
+ assert_same(cells[0, 0], cells[1, 0])
228
+ assert_equal(:c, cells[1, 1].children)
229
+ assert_equal(:d, cells[2, 0].children)
230
+ assert_same(cells[1, 1], cells[2, 1])
231
+ end
232
+
233
+ it "can handle column and row spans concurrently" do
234
+ cells = create_cells([[:a, {col_span: 2, content: :b}, :c],
235
+ [{col_span: 2, row_span: 2, content: :d}, :e, :f],
236
+ [{row_span: 2, content: :g}, :h],
237
+ [:i, :j, :k]])
238
+ assert_equal(:a, cells[0, 0].children)
239
+ assert_equal(:b, cells[0, 1].children)
240
+ assert_same(cells[0, 1], cells[0, 2])
241
+ assert_equal(:c, cells[0, 3].children)
242
+ assert_equal(:d, cells[1, 0].children)
243
+ assert_same(cells[1, 0], cells[1, 1])
244
+ assert_equal(:e, cells[1, 2].children)
245
+ assert_equal(:f, cells[1, 3].children)
246
+ assert_same(cells[1, 0], cells[2, 0])
247
+ assert_same(cells[1, 0], cells[2, 1])
248
+ assert_equal(:g, cells[2, 2].children)
249
+ assert_equal(:h, cells[2, 3].children)
250
+ assert_equal(:i, cells[3, 0].children)
251
+ assert_equal(:j, cells[3, 1].children)
252
+ assert_same(cells[2, 2], cells[3, 2])
253
+ assert_equal(:k, cells[3, 3].children)
254
+ end
255
+
256
+ it "sets the correct information on the created cells" do
257
+ cells = create_cells([[:a, {col_span: 2, content: :b}],
258
+ [{col_span: 2, row_span: 2, content: :c}, {row_span: 2, content: :d}]])
259
+ assert_equal(0, cells[0, 0].row)
260
+ assert_equal(0, cells[0, 0].column)
261
+ assert_equal(1, cells[0, 0].row_span)
262
+ assert_equal(1, cells[0, 0].col_span)
263
+ assert_equal(0, cells[0, 1].row)
264
+ assert_equal(1, cells[0, 1].column)
265
+ assert_equal(1, cells[0, 1].row_span)
266
+ assert_equal(2, cells[0, 1].col_span)
267
+ assert_equal(1, cells[1, 0].row)
268
+ assert_equal(0, cells[1, 0].column)
269
+ assert_equal(2, cells[1, 0].row_span)
270
+ assert_equal(2, cells[1, 0].col_span)
271
+ assert_equal(1, cells[1, 2].row)
272
+ assert_equal(2, cells[1, 2].column)
273
+ assert_equal(2, cells[1, 2].row_span)
274
+ assert_equal(1, cells[1, 2].col_span)
275
+ end
276
+
277
+ it "allows setting cell styles and properties" do
278
+ cells = create_cells([[{content: :a, background_color: 'black', properties: {'x' => 'y'}}]])
279
+ assert_equal('black', cells[0, 0].style.background_color)
280
+ assert_equal('y', cells[0, 0].properties['x'])
281
+ end
282
+
283
+ it "allows setting styles for all cells using a hash as first item in data" do
284
+ cells = create_cells([{background_color: 'black'}, [:a, :b], [:c, :d]])
285
+ assert_equal('black', cells[0, 0].style.background_color)
286
+ assert_equal('black', cells[1, 0].style.background_color)
287
+ end
288
+
289
+ it "allows setting styles for all cells using a proc as first item in data" do
290
+ block = lambda {|cell| cell.style.background_color = 'black' if cell.row == 0 }
291
+ cells = create_cells([block, [:a, :b], [:c, :d]])
292
+ assert_equal('black', cells[0, 0].style.background_color)
293
+ assert_nil(cells[1, 0].style.background_color)
294
+ end
295
+
296
+ it "allows setting styles for all cells using a hash as the cell_style argument" do
297
+ cells = create_cells([[:a, :b], [:c, :d]], {background_color: 'black'})
298
+ assert_equal('black', cells[0, 0].style.background_color)
299
+ assert_equal('black', cells[1, 0].style.background_color)
300
+ end
301
+
302
+ it "allows setting styles for all cells using a proc as the cell_style argument" do
303
+ block = lambda {|cell| cell.style.background_color = 'black' if cell.row == 0 }
304
+ cells = create_cells([[:a, :b], [:c, :d]], block)
305
+ assert_equal('black', cells[0, 0].style.background_color)
306
+ assert_nil(cells[1, 0].style.background_color)
307
+ end
308
+
309
+ it "only uses the styling informtion from data if a cell_style argument is also provided" do
310
+ cells = create_cells([{background_color: 'yellow'}, [:a, :b], [:c, :d]], {background_color: 'black'})
311
+ assert_equal('yellow', cells[0, 0].style.background_color)
312
+ end
313
+
314
+ it "makes sure that cell styling information overrides global styling information" do
315
+ block = lambda do |cell|
316
+ cell.style.background_color = 'yellow'
317
+ cell.properties['key'] = :value
318
+ end
319
+ cells = create_cells([block, [{background_color: 'black', properties: {'key' => 5}, content: :a}, :b],
320
+ [:c, :d]])
321
+ assert_equal('black', cells[0, 0].style.background_color)
322
+ assert_equal(5, cells[0, 0].properties['key'])
323
+ end
324
+ end
325
+
326
+ describe "each_row" do
327
+ it "allows iterating over rows" do
328
+ cells = create_cells([[:a, :b], [:c], [:d, :e]])
329
+ assert_equal([[:a, :b], [:c], [:d, :e]], cells.each_row.map {|cols| cols.map(&:children) })
330
+ end
331
+ end
332
+
333
+ describe "style" do
334
+ it "assigns the style properties to all cells" do
335
+ cells = create_cells([[:a, :b], [:c, :d]])
336
+ cells.style(background_color: 'blue')
337
+ assert_equal('blue', cells[0, 0].style.background_color)
338
+ assert_equal('blue', cells[0, 1].style.background_color)
339
+ assert_equal('blue', cells[1, 0].style.background_color)
340
+ assert_equal('blue', cells[1, 1].style.background_color)
341
+ end
342
+
343
+ it "calls the given block for all cells" do
344
+ cells = create_cells([[:a, :b], [:c, :d]])
345
+ cells.style(background_color: 'blue') {|cell| cell.style.background_color = 'red' if cell.row == 0 }
346
+ assert_equal('red', cells[0, 0].style.background_color)
347
+ assert_equal('red', cells[0, 1].style.background_color)
348
+ assert_equal('blue', cells[1, 0].style.background_color)
349
+ assert_equal('blue', cells[1, 1].style.background_color)
350
+ end
351
+ end
352
+
353
+ #fit_rows and draw_rows are tested through TableBox#fit/#draw
354
+ end
355
+
356
+ describe HexaPDF::Layout::TableBox do
357
+ before do
358
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 160, 100)
359
+ draw_block = lambda {|canvas, _box| canvas.move_to(0, 0).end_path }
360
+ @fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
361
+ end
362
+
363
+ def create_box(**kwargs)
364
+ HexaPDF::Layout::TableBox.new(cells: [@fixed_size_boxes[0, 2], @fixed_size_boxes[2, 2]], **kwargs)
365
+ end
366
+
367
+ def check_box(box, does_fit, width, height, cell_data = nil)
368
+ assert(does_fit == box.fit(@frame.available_width, @frame.available_height, @frame), "box didn't fit")
369
+ assert_equal(width, box.width, "box width")
370
+ assert_equal(height, box.height, "box height")
371
+ if cell_data
372
+ cells = box.cells.each_row.to_a.flatten
373
+ assert_equal(cells.size, cell_data.size)
374
+ cell_data.each_with_index do |(left, top, cwidth, cheight), index|
375
+ cell = cells[index]
376
+ if left.nil?
377
+ assert_nil(cell.left, "cell #{index} left")
378
+ else
379
+ assert_equal(left, cell.left, "cell #{index} left")
380
+ end
381
+ if top.nil?
382
+ assert_nil(cell.top, "cell #{index} top")
383
+ else
384
+ assert_equal(top, cell.top, "cell #{index} top")
385
+ end
386
+ assert_equal(cwidth, cell.width, "cell #{index} width")
387
+ assert_equal(cheight, cell.height, "cell #{index} height")
388
+ end
389
+ end
390
+ box
391
+ end
392
+
393
+ def cell_infos(cells)
394
+ cells.each_row.map {|cols| cols.map {|c| [c.left, c.top, c.width, c.height] } }.flatten(1)
395
+ end
396
+
397
+ describe "initialize" do
398
+ it "creates a new instance with the default arguments" do
399
+ box = create_box(cells: [[:a, :b], [:c]])
400
+ assert_equal([[:a, :b], [:c]], box.cells.each_row.map {|cols| cols.map(&:children) })
401
+ assert_equal([], box.column_widths)
402
+ assert_nil(box.header_cells)
403
+ assert_nil(box.footer_cells)
404
+ assert_equal(0, box.start_row_index)
405
+ assert_equal(-1, box.last_fitted_row_index)
406
+ refute(box.supports_position_flow?)
407
+ end
408
+
409
+ it "creates a new instance with the given arguments" do
410
+ header = lambda {|_| [[:h1, :h2]] }
411
+ footer = lambda {|_| [[:f1], [:f2]] }
412
+ box = create_box(cells: [[:a, :b], [:c]], column_widths: [-2, -1], header: header, footer: footer,
413
+ cell_style: {background_color: 'black'})
414
+ assert_equal([[:a, :b], [:c]], box.cells.each_row.map {|cols| cols.map(&:children) })
415
+ assert_equal([[:h1, :h2]], box.header_cells.each_row.map {|cols| cols.map(&:children) })
416
+ assert_equal([[:f1], [:f2]], box.footer_cells.each_row.map {|cols| cols.map(&:children) })
417
+ assert_equal([-2, -1], box.column_widths)
418
+ [box.cells[0, 0], box.header_cells[0, 0], box.footer_cells[0, 0]].each do |cell|
419
+ assert_equal('black', cell.style.background_color)
420
+ end
421
+ end
422
+
423
+ it "also applies the cell_style information to header and footer cells of split boxes" do
424
+ header = lambda {|_| [[nil]] }
425
+ footer = lambda {|_| [[nil]] }
426
+ box = create_box(header: header, footer: footer, cells: [[nil], [nil]],
427
+ cell_style: {background_color: 'black'})
428
+ refute(box.fit(100, 40, nil))
429
+ box_a, box_b = box.split(100, 40, nil)
430
+ assert_same(box_a, box)
431
+ assert_equal('black', box_b.header_cells[0, 0].style.background_color)
432
+ assert_equal('black', box_b.footer_cells[0, 0].style.background_color)
433
+ end
434
+
435
+ it "allows providing a Cells instance instead of an array of arrays" do
436
+ box = create_box(cells: HexaPDF::Layout::TableBox::Cells.new([[:a, :b]]))
437
+ assert_equal(:a, box.cells[0, 0].children)
438
+ end
439
+ end
440
+
441
+ describe "empty?" do
442
+ it "is empty if nothing is fit yet" do
443
+ assert(create_box.empty?)
444
+ end
445
+
446
+ it "is empty if not as single row fits" do
447
+ box = create_box(column_widths: [5])
448
+ box.fit(@frame.available_width, @frame.available_height, @frame)
449
+ assert(box.empty?)
450
+ end
451
+
452
+ it "is not empty if at least one box fits" do
453
+ box = create_box
454
+ box.fit(@frame.available_width, @frame.available_height, @frame)
455
+ refute(box.empty?)
456
+ end
457
+ end
458
+
459
+ describe "fit" do
460
+ it "respects the set initial width" do
461
+ box = create_box(width: 50)
462
+ box.fit(@frame.available_width, @frame.available_height, @frame)
463
+ assert_equal(50, box.width)
464
+ end
465
+
466
+ it "respects the set initial height" do
467
+ box = create_box(height: 50)
468
+ box.fit(@frame.available_width, @frame.available_height, @frame)
469
+ assert_equal(50, box.height)
470
+ end
471
+
472
+ it "respects the border and padding" do
473
+ box = create_box(column_widths: [40, 40], style: {border: {width: [5, 4, 3, 2]}, padding: [5, 4, 3, 2]})
474
+ box.fit(@frame.available_width, @frame.available_height, @frame)
475
+ assert_equal(93, box.width)
476
+ assert_equal(61, box.height)
477
+ end
478
+
479
+ it "cannot fit the table if the available width smaller than the initial width" do
480
+ box = create_box(width: 200)
481
+ refute(box.fit(@frame.available_width, @frame.available_height, @frame))
482
+ end
483
+
484
+ it "cannot fit the table if the available height smaller than the initial height" do
485
+ box = create_box(height: 200)
486
+ refute(box.fit(@frame.available_width, @frame.available_height, @frame))
487
+ end
488
+
489
+ it "cannot fit the table if the specified column widths are smaller than the available width" do
490
+ box = create_box(column_widths: [200])
491
+ refute(box.fit(@frame.available_width, @frame.available_height, @frame))
492
+ end
493
+
494
+ it "fits a simple table" do
495
+ check_box(create_box, true, 160, 45,
496
+ [[0, 0, 79.5, 22], [79.5, 0, 79.5, 22], [0, 22, 79.5, 22], [79.5, 22, 79.5, 22]])
497
+ end
498
+
499
+ it "fits a table with column and row spans" do
500
+ cells = [[@fixed_size_boxes[0], {col_span: 2, content: @fixed_size_boxes[1]}, @fixed_size_boxes[2]],
501
+ [{col_span: 2, row_span: 2, content: @fixed_size_boxes[3]}, *@fixed_size_boxes[4, 2]],
502
+ [{row_span: 2, content: @fixed_size_boxes[6]}, @fixed_size_boxes[7]],
503
+ @fixed_size_boxes[8, 3]]
504
+ check_box(create_box(cells: cells), true, 160, 89,
505
+ [[0, 0, 39.75, 22], [39.75, 0, 79.5, 22], [39.75, 0, 79.50, 22], [119.25, 0, 39.75, 22],
506
+ [0, 22, 79.5, 44], [0, 22, 79.5, 44], [79.5, 22, 39.75, 22], [119.25, 22, 39.75, 22],
507
+ [0, 22, 79.5, 44], [0, 22, 79.5, 44], [79.5, 44, 39.75, 44], [119.25, 44, 39.75, 22],
508
+ [0, 66, 39.75, 22], [39.75, 66, 39.75, 22], [79.5, 44, 39.75, 44], [119.25, 66, 39.75, 22]])
509
+ end
510
+
511
+ it "fits a table with header rows" do
512
+ result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
513
+ header = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
514
+ box = create_box(header: header, cell_style: {padding: 0, border: {width: 0}})
515
+ box = check_box(box, true, 160, 40, result)
516
+ assert_equal(result, cell_infos(box.header_cells))
517
+ end
518
+
519
+ it "fits a table with footer rows" do
520
+ result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
521
+ footer = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
522
+ box = create_box(footer: footer, cell_style: {padding: 0, border: {width: 0}})
523
+ box = check_box(box, true, 160, 40, result)
524
+ assert_equal(result, cell_infos(box.footer_cells))
525
+ end
526
+
527
+ it "fits a table with header and footer rows" do
528
+ result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
529
+ cell_creator = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
530
+ box = create_box(header: cell_creator, footer: cell_creator,
531
+ cell_style: {padding: 0, border: {width: 0}})
532
+ box = check_box(box, true, 160, 60, result)
533
+ assert_equal(result, cell_infos(box.header_cells))
534
+ assert_equal(result, cell_infos(box.footer_cells))
535
+ end
536
+
537
+ it "partially fits a table if not enough height is available" do
538
+ box = create_box(height: 10, cell_style: {padding: 0, border: {width: 0}})
539
+ check_box(box, false, 160, 10,
540
+ [[0, 0, 80, 10], [80, 0, 80, 10], [nil, nil, 80, 0], [nil, nil, 0, 0]])
541
+ end
542
+ end
543
+
544
+ describe "split" do
545
+ it "splits the table if some rows could not be fit into the available region" do
546
+ box = create_box
547
+ refute(box.fit(100, 25, nil))
548
+ box_a, box_b = box.split(100, 25, nil)
549
+ assert_same(box_a, box)
550
+ assert(box_b.split_box?)
551
+
552
+ assert_equal(0, box_a.start_row_index)
553
+ assert_equal(0, box_a.last_fitted_row_index)
554
+ assert_equal(1, box_b.start_row_index)
555
+ assert_equal(-1, box_b.last_fitted_row_index)
556
+ end
557
+
558
+ it "splits the table if the header or footer rows don't fit" do
559
+ cells_creator = lambda {|_| [@fixed_size_boxes[10, 2]] }
560
+ [{header: cells_creator}, {footer: cells_creator}].each do |args|
561
+ box = create_box(**args)
562
+ box.cells.style(padding: 0, border: {width: 0})
563
+ refute(box.fit(100, 25, nil))
564
+ box_a, box_b = box.split(100, 25, nil)
565
+ assert_nil(box_a)
566
+ assert_same(box_b, box)
567
+ end
568
+ end
569
+
570
+ it "splits a table with a header or a footer" do
571
+ cells_creator = lambda {|_| [@fixed_size_boxes[10, 2]] }
572
+ [{header: cells_creator}, {footer: cells_creator}].each do |args|
573
+ box = create_box(**args)
574
+ refute(box.fit(100, 50, nil))
575
+ box_a, box_b = box.split(100, 50, nil)
576
+ assert_same(box_a, box)
577
+
578
+ assert_equal(0, box_a.start_row_index)
579
+ assert_equal(0, box_a.last_fitted_row_index)
580
+ assert_equal(1, box_b.start_row_index)
581
+ assert_equal(-1, box_b.last_fitted_row_index)
582
+ assert_nil(box_b.instance_variable_get(:@special_cells_fit_not_successful))
583
+ if args.key?(:header)
584
+ refute_same(box_a.header_cells, box_b.header_cells)
585
+ else
586
+ refute_same(box_a.footer_cells, box_b.footer_cells)
587
+ end
588
+ end
589
+ end
590
+ end
591
+
592
+ describe "draw_content" do
593
+ before do
594
+ @canvas = HexaPDF::Document.new.pages.add.canvas
595
+ end
596
+
597
+ it "draws the result onto the canvas" do
598
+ box = create_box
599
+ box.fit(100, 100, nil)
600
+ box.draw(@canvas, 20, 10)
601
+ operators = [[:save_graphics_state],
602
+ [:append_rectangle, [20.0, 32.0, 50.5, 23.0]],
603
+ [:clip_path_non_zero],
604
+ [:end_path],
605
+ [:append_rectangle, [20.5, 32.5, 49.5, 22.0]],
606
+ [:stroke_path],
607
+ [:restore_graphics_state],
608
+ [:save_graphics_state],
609
+ [:concatenate_matrix, [1, 0, 0, 1, 26.5, 38.5]],
610
+ [:move_to, [0, 0]],
611
+ [:end_path],
612
+ [:restore_graphics_state],
613
+ [:save_graphics_state],
614
+ [:append_rectangle, [69.5, 32.0, 50.5, 23.0]],
615
+ [:clip_path_non_zero],
616
+ [:end_path],
617
+ [:append_rectangle, [70.0, 32.5, 49.5, 22.0]],
618
+ [:stroke_path],
619
+ [:restore_graphics_state],
620
+ [:save_graphics_state],
621
+ [:concatenate_matrix, [1, 0, 0, 1, 76.0, 38.5]],
622
+ [:move_to, [0, 0]],
623
+ [:end_path],
624
+ [:restore_graphics_state],
625
+ [:save_graphics_state],
626
+ [:append_rectangle, [20.0, 10.0, 50.5, 23.0]],
627
+ [:clip_path_non_zero],
628
+ [:end_path],
629
+ [:append_rectangle, [20.5, 10.5, 49.5, 22.0]],
630
+ [:stroke_path],
631
+ [:restore_graphics_state],
632
+ [:save_graphics_state],
633
+ [:concatenate_matrix, [1, 0, 0, 1, 26.5, 16.5]],
634
+ [:move_to, [0, 0]],
635
+ [:end_path],
636
+ [:restore_graphics_state],
637
+ [:save_graphics_state],
638
+ [:append_rectangle, [69.5, 10.0, 50.5, 23.0]],
639
+ [:clip_path_non_zero],
640
+ [:end_path],
641
+ [:append_rectangle, [70.0, 10.5, 49.5, 22.0]],
642
+ [:stroke_path],
643
+ [:restore_graphics_state],
644
+ [:save_graphics_state],
645
+ [:concatenate_matrix, [1, 0, 0, 1, 76.0, 16.5]],
646
+ [:move_to, [0, 0]],
647
+ [:end_path],
648
+ [:restore_graphics_state]]
649
+ assert_operators(@canvas.contents, operators)
650
+ end
651
+
652
+ it "correctly works for split boxes" do
653
+ box = create_box(cell_style: {padding: 0, border: {width: 0}})
654
+ refute(box.fit(100, 10, nil))
655
+ _, split_box = box.split(100, 10, nil)
656
+ assert(split_box.fit(100, 100, nil))
657
+
658
+ box.draw(@canvas, 20, 10)
659
+ split_box.draw(@canvas, 0, 50)
660
+ operators = [[:save_graphics_state],
661
+ [:concatenate_matrix, [1, 0, 0, 1, 20.0, 10]],
662
+ [:move_to, [0, 0]],
663
+ [:end_path],
664
+ [:restore_graphics_state],
665
+ [:save_graphics_state],
666
+ [:concatenate_matrix, [1, 0, 0, 1, 70.0, 10]],
667
+ [:move_to, [0, 0]],
668
+ [:end_path],
669
+ [:restore_graphics_state],
670
+ [:save_graphics_state],
671
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 50]],
672
+ [:move_to, [0, 0]],
673
+ [:end_path],
674
+ [:restore_graphics_state],
675
+ [:save_graphics_state],
676
+ [:concatenate_matrix, [1, 0, 0, 1, 50.0, 50]],
677
+ [:move_to, [0, 0]],
678
+ [:end_path],
679
+ [:restore_graphics_state]]
680
+ assert_operators(@canvas.contents, operators)
681
+ end
682
+
683
+ it "correctly works for tables with headers and footers" do
684
+ box = create_box(header: lambda {|_| [@fixed_size_boxes[10, 1]] },
685
+ footer: lambda {|_| [@fixed_size_boxes[12, 1]] },
686
+ cell_style: {padding: 0, border: {width: 0}})
687
+ box.fit(100, 100, nil)
688
+ box.draw(@canvas, 20, 10)
689
+ operators = [[:save_graphics_state],
690
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 40]],
691
+ [:move_to, [0, 0]],
692
+ [:end_path],
693
+ [:restore_graphics_state],
694
+ [:save_graphics_state],
695
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 30]],
696
+ [:move_to, [0, 0]],
697
+ [:end_path],
698
+ [:restore_graphics_state],
699
+ [:save_graphics_state],
700
+ [:concatenate_matrix, [1, 0, 0, 1, 70.0, 30]],
701
+ [:move_to, [0, 0]],
702
+ [:end_path],
703
+ [:restore_graphics_state],
704
+ [:save_graphics_state],
705
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 20]],
706
+ [:move_to, [0, 0]],
707
+ [:end_path],
708
+ [:restore_graphics_state],
709
+ [:save_graphics_state],
710
+ [:concatenate_matrix, [1, 0, 0, 1, 70.0, 20]],
711
+ [:move_to, [0, 0]],
712
+ [:end_path],
713
+ [:restore_graphics_state],
714
+ [:save_graphics_state],
715
+ [:concatenate_matrix, [1, 0, 0, 1, 20.0, 10]],
716
+ [:move_to, [0, 0]],
717
+ [:end_path],
718
+ [:restore_graphics_state]]
719
+ assert_operators(@canvas.contents, operators)
720
+ end
721
+ end
722
+ end