hexapdf 0.32.2 → 0.33.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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -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/composer.rb +104 -52
  22. data/lib/hexapdf/configuration.rb +44 -39
  23. data/lib/hexapdf/content/canvas.rb +393 -267
  24. data/lib/hexapdf/content/color_space.rb +72 -25
  25. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  26. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  27. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  28. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  29. data/lib/hexapdf/content/graphic_object.rb +6 -7
  30. data/lib/hexapdf/content/graphics_state.rb +54 -45
  31. data/lib/hexapdf/content/operator.rb +52 -54
  32. data/lib/hexapdf/content/parser.rb +2 -2
  33. data/lib/hexapdf/content/processor.rb +15 -15
  34. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  35. data/lib/hexapdf/content.rb +5 -0
  36. data/lib/hexapdf/dictionary.rb +6 -5
  37. data/lib/hexapdf/dictionary_fields.rb +42 -14
  38. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  39. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  40. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  41. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  42. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  43. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  44. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  45. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  46. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  47. data/lib/hexapdf/digital_signature/verification_result.rb +2 -2
  48. data/lib/hexapdf/digital_signature.rb +7 -2
  49. data/lib/hexapdf/document/destinations.rb +12 -11
  50. data/lib/hexapdf/document/files.rb +1 -1
  51. data/lib/hexapdf/document/fonts.rb +1 -1
  52. data/lib/hexapdf/document/layout.rb +167 -39
  53. data/lib/hexapdf/document/pages.rb +3 -2
  54. data/lib/hexapdf/document.rb +89 -55
  55. data/lib/hexapdf/encryption/aes.rb +5 -5
  56. data/lib/hexapdf/encryption/arc4.rb +1 -1
  57. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  58. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  59. data/lib/hexapdf/encryption/identity.rb +1 -1
  60. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  61. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  63. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  64. data/lib/hexapdf/encryption.rb +7 -2
  65. data/lib/hexapdf/error.rb +18 -0
  66. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  67. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  68. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  69. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  70. data/lib/hexapdf/filter/pass_through.rb +1 -1
  71. data/lib/hexapdf/filter/predictor.rb +1 -1
  72. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  73. data/lib/hexapdf/filter.rb +55 -6
  74. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  75. data/lib/hexapdf/font/cmap.rb +1 -1
  76. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  77. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  79. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  82. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  83. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  84. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  85. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  86. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  87. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  88. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  89. data/lib/hexapdf/font_loader.rb +3 -0
  90. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  91. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  92. data/lib/hexapdf/image_loader/png.rb +2 -2
  93. data/lib/hexapdf/image_loader.rb +1 -1
  94. data/lib/hexapdf/importer.rb +13 -0
  95. data/lib/hexapdf/layout/box.rb +9 -2
  96. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  97. data/lib/hexapdf/layout/column_box.rb +18 -4
  98. data/lib/hexapdf/layout/frame.rb +30 -12
  99. data/lib/hexapdf/layout/image_box.rb +5 -0
  100. data/lib/hexapdf/layout/inline_box.rb +1 -0
  101. data/lib/hexapdf/layout/list_box.rb +17 -1
  102. data/lib/hexapdf/layout/page_style.rb +4 -4
  103. data/lib/hexapdf/layout/style.rb +18 -3
  104. data/lib/hexapdf/layout/table_box.rb +682 -0
  105. data/lib/hexapdf/layout/text_box.rb +5 -3
  106. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  107. data/lib/hexapdf/layout/text_layouter.rb +12 -4
  108. data/lib/hexapdf/layout.rb +1 -0
  109. data/lib/hexapdf/name_tree_node.rb +1 -1
  110. data/lib/hexapdf/number_tree_node.rb +1 -1
  111. data/lib/hexapdf/object.rb +18 -7
  112. data/lib/hexapdf/parser.rb +7 -7
  113. data/lib/hexapdf/pdf_array.rb +1 -1
  114. data/lib/hexapdf/rectangle.rb +1 -1
  115. data/lib/hexapdf/reference.rb +1 -1
  116. data/lib/hexapdf/revision.rb +1 -1
  117. data/lib/hexapdf/revisions.rb +3 -3
  118. data/lib/hexapdf/serializer.rb +15 -15
  119. data/lib/hexapdf/stream.rb +4 -2
  120. data/lib/hexapdf/tokenizer.rb +14 -14
  121. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  122. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  123. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  124. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  125. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  127. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  128. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  129. data/lib/hexapdf/type/acro_form.rb +1 -1
  130. data/lib/hexapdf/type/action.rb +1 -1
  131. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  132. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  133. data/lib/hexapdf/type/actions/launch.rb +1 -1
  134. data/lib/hexapdf/type/actions/uri.rb +1 -1
  135. data/lib/hexapdf/type/actions.rb +1 -1
  136. data/lib/hexapdf/type/annotation.rb +3 -3
  137. data/lib/hexapdf/type/annotations/link.rb +1 -1
  138. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  139. data/lib/hexapdf/type/annotations/text.rb +1 -1
  140. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  141. data/lib/hexapdf/type/annotations.rb +1 -1
  142. data/lib/hexapdf/type/catalog.rb +1 -1
  143. data/lib/hexapdf/type/cid_font.rb +3 -3
  144. data/lib/hexapdf/type/embedded_file.rb +1 -1
  145. data/lib/hexapdf/type/file_specification.rb +2 -2
  146. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  147. data/lib/hexapdf/type/font_simple.rb +2 -2
  148. data/lib/hexapdf/type/font_type0.rb +3 -3
  149. data/lib/hexapdf/type/font_type3.rb +1 -1
  150. data/lib/hexapdf/type/form.rb +1 -1
  151. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  152. data/lib/hexapdf/type/icon_fit.rb +1 -1
  153. data/lib/hexapdf/type/image.rb +1 -1
  154. data/lib/hexapdf/type/info.rb +1 -1
  155. data/lib/hexapdf/type/mark_information.rb +1 -1
  156. data/lib/hexapdf/type/names.rb +2 -2
  157. data/lib/hexapdf/type/object_stream.rb +2 -1
  158. data/lib/hexapdf/type/outline.rb +1 -1
  159. data/lib/hexapdf/type/outline_item.rb +1 -1
  160. data/lib/hexapdf/type/page.rb +19 -10
  161. data/lib/hexapdf/type/page_label.rb +1 -1
  162. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  163. data/lib/hexapdf/type/resources.rb +1 -1
  164. data/lib/hexapdf/type/trailer.rb +2 -2
  165. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  166. data/lib/hexapdf/type/xref_stream.rb +2 -2
  167. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  168. data/lib/hexapdf/version.rb +1 -1
  169. data/lib/hexapdf/writer.rb +4 -4
  170. data/lib/hexapdf/xref_section.rb +2 -2
  171. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  172. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  173. data/test/hexapdf/content/test_canvas.rb +0 -1
  174. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  175. data/test/hexapdf/document/test_files.rb +2 -2
  176. data/test/hexapdf/document/test_layout.rb +98 -0
  177. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  178. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  179. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  180. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  181. data/test/hexapdf/layout/test_box.rb +1 -1
  182. data/test/hexapdf/layout/test_column_box.rb +65 -21
  183. data/test/hexapdf/layout/test_frame.rb +14 -14
  184. data/test/hexapdf/layout/test_image_box.rb +4 -0
  185. data/test/hexapdf/layout/test_inline_box.rb +5 -0
  186. data/test/hexapdf/layout/test_list_box.rb +40 -6
  187. data/test/hexapdf/layout/test_page_style.rb +3 -2
  188. data/test/hexapdf/layout/test_style.rb +50 -0
  189. data/test/hexapdf/layout/test_table_box.rb +722 -0
  190. data/test/hexapdf/layout/test_text_box.rb +18 -0
  191. data/test/hexapdf/layout/test_text_layouter.rb +4 -0
  192. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  193. data/test/hexapdf/test_document.rb +1 -0
  194. data/test/hexapdf/test_filter.rb +8 -0
  195. data/test/hexapdf/test_importer.rb +9 -0
  196. data/test/hexapdf/test_object.rb +16 -5
  197. data/test/hexapdf/test_stream.rb +7 -0
  198. data/test/hexapdf/test_writer.rb +3 -3
  199. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  200. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  201. data/test/hexapdf/type/test_page.rb +18 -4
  202. 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