hexapdf 0.23.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (243) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/LICENSE +1 -1
  4. data/Rakefile +1 -1
  5. data/examples/016-frame_automatic_box_placement.rb +7 -2
  6. data/examples/017-frame_text_flow.rb +10 -18
  7. data/examples/020-column_box.rb +40 -0
  8. data/examples/021-list_box.rb +26 -0
  9. data/lib/hexapdf/cli/batch.rb +1 -1
  10. data/lib/hexapdf/cli/command.rb +1 -1
  11. data/lib/hexapdf/cli/files.rb +1 -1
  12. data/lib/hexapdf/cli/fonts.rb +1 -1
  13. data/lib/hexapdf/cli/form.rb +2 -2
  14. data/lib/hexapdf/cli/image2pdf.rb +1 -1
  15. data/lib/hexapdf/cli/images.rb +1 -1
  16. data/lib/hexapdf/cli/info.rb +2 -2
  17. data/lib/hexapdf/cli/inspect.rb +2 -2
  18. data/lib/hexapdf/cli/merge.rb +1 -1
  19. data/lib/hexapdf/cli/modify.rb +1 -1
  20. data/lib/hexapdf/cli/optimize.rb +1 -1
  21. data/lib/hexapdf/cli/split.rb +1 -1
  22. data/lib/hexapdf/cli/watermark.rb +1 -1
  23. data/lib/hexapdf/cli.rb +1 -1
  24. data/lib/hexapdf/composer.rb +45 -126
  25. data/lib/hexapdf/configuration.rb +17 -1
  26. data/lib/hexapdf/content/canvas.rb +1 -1
  27. data/lib/hexapdf/content/color_space.rb +1 -1
  28. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +2 -1
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  32. data/lib/hexapdf/content/graphic_object.rb +1 -1
  33. data/lib/hexapdf/content/graphics_state.rb +1 -1
  34. data/lib/hexapdf/content/operator.rb +1 -1
  35. data/lib/hexapdf/content/parser.rb +1 -1
  36. data/lib/hexapdf/content/processor.rb +1 -1
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +1 -1
  39. data/lib/hexapdf/data_dir.rb +1 -1
  40. data/lib/hexapdf/dictionary.rb +1 -1
  41. data/lib/hexapdf/dictionary_fields.rb +1 -1
  42. data/lib/hexapdf/document/files.rb +1 -1
  43. data/lib/hexapdf/document/fonts.rb +1 -1
  44. data/lib/hexapdf/document/images.rb +1 -1
  45. data/lib/hexapdf/document/layout.rb +397 -0
  46. data/lib/hexapdf/document/pages.rb +17 -1
  47. data/lib/hexapdf/document/signatures.rb +5 -4
  48. data/lib/hexapdf/document.rb +8 -1
  49. data/lib/hexapdf/encryption/aes.rb +1 -1
  50. data/lib/hexapdf/encryption/arc4.rb +1 -1
  51. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  52. data/lib/hexapdf/encryption/fast_arc4.rb +30 -21
  53. data/lib/hexapdf/encryption/identity.rb +1 -1
  54. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  55. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  56. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  57. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  58. data/lib/hexapdf/encryption.rb +1 -1
  59. data/lib/hexapdf/error.rb +1 -1
  60. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  61. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  62. data/lib/hexapdf/filter/crypt.rb +1 -1
  63. data/lib/hexapdf/filter/encryption.rb +1 -1
  64. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  65. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  66. data/lib/hexapdf/filter/pass_through.rb +1 -1
  67. data/lib/hexapdf/filter/predictor.rb +1 -1
  68. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  69. data/lib/hexapdf/filter.rb +1 -1
  70. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  71. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  72. data/lib/hexapdf/font/cmap.rb +1 -1
  73. data/lib/hexapdf/font/encoding/base.rb +1 -1
  74. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  75. data/lib/hexapdf/font/encoding/glyph_list.rb +2 -2
  76. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  77. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  79. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  82. data/lib/hexapdf/font/encoding.rb +1 -1
  83. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  84. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  85. data/lib/hexapdf/font/true_type/font.rb +1 -1
  86. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  87. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  88. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  89. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  90. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  91. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  92. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  93. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  94. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  95. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  96. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  97. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  98. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  99. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  100. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  101. data/lib/hexapdf/font/true_type/table.rb +1 -1
  102. data/lib/hexapdf/font/true_type.rb +1 -1
  103. data/lib/hexapdf/font/true_type_wrapper.rb +1 -1
  104. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  105. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  106. data/lib/hexapdf/font/type1/font.rb +1 -1
  107. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  108. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  109. data/lib/hexapdf/font/type1.rb +1 -1
  110. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  111. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  112. data/lib/hexapdf/font_loader/from_file.rb +1 -1
  113. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  114. data/lib/hexapdf/font_loader.rb +1 -1
  115. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  116. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  117. data/lib/hexapdf/image_loader/png.rb +1 -1
  118. data/lib/hexapdf/image_loader.rb +1 -1
  119. data/lib/hexapdf/importer.rb +1 -1
  120. data/lib/hexapdf/layout/box.rb +121 -22
  121. data/lib/hexapdf/layout/box_fitter.rb +136 -0
  122. data/lib/hexapdf/layout/column_box.rb +247 -0
  123. data/lib/hexapdf/layout/frame.rb +155 -139
  124. data/lib/hexapdf/layout/image_box.rb +19 -4
  125. data/lib/hexapdf/layout/inline_box.rb +1 -1
  126. data/lib/hexapdf/layout/line.rb +1 -1
  127. data/lib/hexapdf/layout/list_box.rb +355 -0
  128. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  129. data/lib/hexapdf/layout/style.rb +5 -1
  130. data/lib/hexapdf/layout/text_box.rb +20 -9
  131. data/lib/hexapdf/layout/text_fragment.rb +3 -2
  132. data/lib/hexapdf/layout/text_layouter.rb +17 -2
  133. data/lib/hexapdf/layout/text_shaper.rb +1 -1
  134. data/lib/hexapdf/layout/width_from_polygon.rb +12 -7
  135. data/lib/hexapdf/layout.rb +4 -1
  136. data/lib/hexapdf/name_tree_node.rb +1 -1
  137. data/lib/hexapdf/number_tree_node.rb +1 -1
  138. data/lib/hexapdf/object.rb +1 -1
  139. data/lib/hexapdf/parser.rb +1 -8
  140. data/lib/hexapdf/pdf_array.rb +1 -1
  141. data/lib/hexapdf/rectangle.rb +1 -1
  142. data/lib/hexapdf/reference.rb +1 -1
  143. data/lib/hexapdf/revision.rb +1 -1
  144. data/lib/hexapdf/revisions.rb +1 -1
  145. data/lib/hexapdf/serializer.rb +1 -1
  146. data/lib/hexapdf/stream.rb +1 -1
  147. data/lib/hexapdf/task/dereference.rb +1 -1
  148. data/lib/hexapdf/task/optimize.rb +1 -1
  149. data/lib/hexapdf/task.rb +1 -1
  150. data/lib/hexapdf/tokenizer.rb +1 -1
  151. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  152. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  153. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  154. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  155. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  156. data/lib/hexapdf/type/acro_form/signature_field.rb +1 -1
  157. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  158. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  159. data/lib/hexapdf/type/acro_form.rb +1 -1
  160. data/lib/hexapdf/type/action.rb +1 -1
  161. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  162. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  163. data/lib/hexapdf/type/actions/launch.rb +1 -1
  164. data/lib/hexapdf/type/actions/uri.rb +1 -1
  165. data/lib/hexapdf/type/actions.rb +1 -1
  166. data/lib/hexapdf/type/annotation.rb +1 -1
  167. data/lib/hexapdf/type/annotations/link.rb +1 -1
  168. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  169. data/lib/hexapdf/type/annotations/text.rb +1 -1
  170. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  171. data/lib/hexapdf/type/annotations.rb +1 -1
  172. data/lib/hexapdf/type/catalog.rb +1 -1
  173. data/lib/hexapdf/type/cid_font.rb +1 -1
  174. data/lib/hexapdf/type/embedded_file.rb +1 -1
  175. data/lib/hexapdf/type/file_specification.rb +1 -1
  176. data/lib/hexapdf/type/font.rb +1 -1
  177. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  178. data/lib/hexapdf/type/font_simple.rb +1 -1
  179. data/lib/hexapdf/type/font_true_type.rb +1 -1
  180. data/lib/hexapdf/type/font_type0.rb +1 -1
  181. data/lib/hexapdf/type/font_type1.rb +1 -1
  182. data/lib/hexapdf/type/font_type3.rb +1 -1
  183. data/lib/hexapdf/type/form.rb +1 -1
  184. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  185. data/lib/hexapdf/type/icon_fit.rb +1 -1
  186. data/lib/hexapdf/type/image.rb +1 -1
  187. data/lib/hexapdf/type/info.rb +1 -1
  188. data/lib/hexapdf/type/names.rb +1 -1
  189. data/lib/hexapdf/type/object_stream.rb +1 -1
  190. data/lib/hexapdf/type/page.rb +1 -1
  191. data/lib/hexapdf/type/page_tree_node.rb +19 -2
  192. data/lib/hexapdf/type/resources.rb +1 -1
  193. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  194. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +1 -1
  195. data/lib/hexapdf/type/signature/handler.rb +1 -1
  196. data/lib/hexapdf/type/signature/verification_result.rb +1 -1
  197. data/lib/hexapdf/type/signature.rb +1 -1
  198. data/lib/hexapdf/type/trailer.rb +2 -2
  199. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  200. data/lib/hexapdf/type/xref_stream.rb +1 -1
  201. data/lib/hexapdf/type.rb +1 -1
  202. data/lib/hexapdf/utils/bit_field.rb +1 -1
  203. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  204. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  205. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  206. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  207. data/lib/hexapdf/utils/object_hash.rb +1 -1
  208. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  209. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  210. data/lib/hexapdf/version.rb +2 -2
  211. data/lib/hexapdf/writer.rb +9 -7
  212. data/lib/hexapdf/xref_section.rb +1 -1
  213. data/lib/hexapdf.rb +1 -1
  214. data/test/hexapdf/content/graphic_object/test_geom2d.rb +1 -1
  215. data/test/hexapdf/document/test_destinations.rb +1 -1
  216. data/test/hexapdf/document/test_images.rb +1 -1
  217. data/test/hexapdf/document/test_layout.rb +264 -0
  218. data/test/hexapdf/document/test_pages.rb +9 -0
  219. data/test/hexapdf/document/test_signatures.rb +10 -3
  220. data/test/hexapdf/encryption/test_security_handler.rb +1 -1
  221. data/test/hexapdf/font/encoding/test_glyph_list.rb +4 -0
  222. data/test/hexapdf/layout/test_box.rb +53 -3
  223. data/test/hexapdf/layout/test_box_fitter.rb +62 -0
  224. data/test/hexapdf/layout/test_column_box.rb +159 -0
  225. data/test/hexapdf/layout/test_frame.rb +99 -38
  226. data/test/hexapdf/layout/test_image_box.rb +1 -1
  227. data/test/hexapdf/layout/test_list_box.rb +249 -0
  228. data/test/hexapdf/layout/test_text_box.rb +17 -2
  229. data/test/hexapdf/layout/test_text_fragment.rb +1 -1
  230. data/test/hexapdf/layout/test_text_layouter.rb +42 -17
  231. data/test/hexapdf/layout/test_width_from_polygon.rb +13 -0
  232. data/test/hexapdf/test_composer.rb +11 -0
  233. data/test/hexapdf/test_dictionary_fields.rb +9 -9
  234. data/test/hexapdf/test_document.rb +4 -4
  235. data/test/hexapdf/test_filter.rb +1 -1
  236. data/test/hexapdf/test_parser.rb +0 -2
  237. data/test/hexapdf/test_revisions.rb +2 -2
  238. data/test/hexapdf/test_serializer.rb +1 -5
  239. data/test/hexapdf/test_writer.rb +58 -3
  240. data/test/hexapdf/type/test_page_tree_node.rb +21 -1
  241. data/test/hexapdf/type/test_trailer.rb +3 -3
  242. data/test/test_helper.rb +5 -1
  243. metadata +28 -3
@@ -57,6 +57,14 @@ describe HexaPDF::Layout::Box do
57
57
  end
58
58
  end
59
59
 
60
+ it "returns false when asking whether it is a split box by default" do
61
+ refute(create_box.split_box?)
62
+ end
63
+
64
+ it "doesn't support the position :flow" do
65
+ refute(create_box.supports_position_flow?)
66
+ end
67
+
60
68
  describe "fit" do
61
69
  before do
62
70
  @frame = Object.new
@@ -89,9 +97,51 @@ describe HexaPDF::Layout::Box do
89
97
  end
90
98
  end
91
99
 
92
- it "can't be split into two parts" do
93
- box = create_box(width: 100, height: 100)
94
- assert_equal([nil, box], box.split(50, 50, nil))
100
+ describe "split" do
101
+ before do
102
+ @box = create_box(width: 100, height: 100)
103
+ @box.fit(100, 100, nil)
104
+ end
105
+
106
+ it "doesn't need to be split if it completely fits" do
107
+ assert_equal([@box, nil], @box.split(100, 100, nil))
108
+ end
109
+
110
+ it "can't be split if it doesn't (completely) fit and its width is greater than the available width" do
111
+ @box.fit(90, 100, nil)
112
+ assert_equal([nil, @box], @box.split(50, 150, nil))
113
+ end
114
+
115
+ it "can't be split if it doesn't (completely) fit and its height is greater than the available height" do
116
+ @box.fit(90, 100, nil)
117
+ assert_equal([nil, @box], @box.split(150, 50, nil))
118
+ end
119
+
120
+ it "can't be split if it doesn't (completely) fit and its content width is zero" do
121
+ box = create_box(width: 0, height: 100)
122
+ assert_equal([nil, box], box.split(150, 150, nil))
123
+ end
124
+
125
+ it "can't be split if it doesn't (completely) fit and its content height is zero" do
126
+ box = create_box(width: 100, height: 0)
127
+ assert_equal([nil, box], box.split(150, 150, nil))
128
+ end
129
+
130
+ it "can't be split if it doesn't (completely) fit as the default implementation knows nothing about the content" do
131
+ @box.style.position = :flow # make sure we would generally be splitable
132
+ @box.fit(90, 100, nil)
133
+ assert_equal([nil, @box], @box.split(150, 150, nil))
134
+ end
135
+ end
136
+
137
+ it "can create a cloned box for splitting" do
138
+ box = create_box
139
+ box.fit(100, 100, nil)
140
+ cloned_box = box.send(:create_split_box)
141
+ assert(cloned_box.split_box?)
142
+ refute(cloned_box.instance_variable_get(:@fit_successful))
143
+ assert_equal(0, cloned_box.width)
144
+ assert_equal(0, cloned_box.height)
95
145
  end
96
146
 
97
147
  describe "draw" do
@@ -0,0 +1,62 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'geom2d/polygon'
5
+ require 'hexapdf/layout/box_fitter'
6
+ require 'hexapdf/layout'
7
+
8
+ describe HexaPDF::Layout::BoxFitter do
9
+ before do
10
+ shape = Geom2D::Polygon([0, 0], [100, 0], [100, 90], [10, 90], [10, 80], [0, 80])
11
+ frames = [
12
+ HexaPDF::Layout::Frame.new(0, 0, 100, 100, shape: shape),
13
+ HexaPDF::Layout::Frame.new(100, 100, 50, 50),
14
+ ]
15
+ @box_fitter = HexaPDF::Layout::BoxFitter.new(frames)
16
+ end
17
+
18
+ def fit_box(count, width: 10, height: 10)
19
+ ibox = HexaPDF::Layout::InlineBox.create(width: width, height: height) {}
20
+ @box_fitter.fit(HexaPDF::Layout::TextBox.new(items: [ibox] * count))
21
+ end
22
+
23
+ def check_result(*pos, content_heights:, successful: true, boxes_remain: false)
24
+ pos.each_slice(2).with_index do |(x, y), index|
25
+ assert_equal(x, @box_fitter.fit_results[index].x, "x #{index}")
26
+ assert_equal(y, @box_fitter.fit_results[index].y, "y #{index}")
27
+ end
28
+ assert_equal(content_heights, @box_fitter.content_heights)
29
+ successful ? assert(@box_fitter.fit_successful?) : refute(@box_fitter.fit_successful?)
30
+ rboxes = @box_fitter.remaining_boxes.empty?
31
+ boxes_remain ? refute(rboxes) : assert(rboxes)
32
+ end
33
+
34
+ it "successfully places boxes only in one frame" do
35
+ fit_box(20)
36
+ fit_box(20)
37
+ check_result(10, 60, 0, 40, content_heights: [50, 0])
38
+ end
39
+
40
+ it "successfully places boxes in multiple frames, without splitting" do
41
+ fit_box(1, height: 80)
42
+ fit_box(1, height: 40)
43
+ check_result(10, 10, 100, 110, content_heights: [80, 40])
44
+ end
45
+
46
+ it "successfully places boxes in multiple framess, with splitting" do
47
+ fit_box(63)
48
+ fit_box(30)
49
+ fit_box(10)
50
+ check_result(10, 20, 0, 0, 100, 130, 100, 110, content_heights: [90, 40])
51
+ end
52
+
53
+ it "fails when some boxes can't be fitted" do
54
+ fit_box(9)
55
+ fit_box(70)
56
+ fit_box(40)
57
+ fit_box(20)
58
+ check_result(10, 80, 0, 10, 0, 0, 100, 100, successful: false, boxes_remain: true,
59
+ content_heights: [90, 50])
60
+ assert_equal(2, @box_fitter.remaining_boxes.size)
61
+ end
62
+ end
@@ -0,0 +1,159 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative '../content/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/layout/column_box'
7
+
8
+ describe HexaPDF::Layout::ColumnBox do
9
+ before do
10
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
11
+ inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {}
12
+ @text_boxes = 5.times.map do
13
+ HexaPDF::Layout::TextBox.new(items: [inline_box] * 15, style: {position: :default})
14
+ end
15
+ draw_block = lambda do |canvas, box|
16
+ canvas.move_to(0, 0).end_path
17
+ end
18
+ @fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
19
+ end
20
+
21
+ def create_box(**kwargs)
22
+ HexaPDF::Layout::ColumnBox.new(gaps: 10, **kwargs)
23
+ end
24
+
25
+ def check_box(box, width, height, fit_pos = nil)
26
+ assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box fit?")
27
+ assert_equal(width, box.width, "box width")
28
+ assert_equal(height, box.height, "box height")
29
+ if fit_pos
30
+ box_fitter = box.instance_variable_get(:@box_fitter)
31
+ assert_equal(fit_pos.size, box_fitter.fit_results.size)
32
+ fit_pos.each_with_index do |(x, y), index|
33
+ assert_equal(x, box_fitter.fit_results[index].x, "result[#{index}].x")
34
+ assert_equal(y, box_fitter.fit_results[index].y, "result[#{index}].y")
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "initialize" do
40
+ it "creates a new instance with the given arguments" do
41
+ box = create_box(children: [:a], columns: 3, gaps: 10, equal_height: false)
42
+ assert_equal([:a], box.children)
43
+ assert_equal([-1, -1, -1], box.columns)
44
+ assert_equal([10], box.gaps)
45
+ assert_equal(false, box.equal_height)
46
+ assert(box.supports_position_flow?)
47
+ end
48
+ end
49
+
50
+ describe "fit" do
51
+ [:default, :flow].each do |position|
52
+ it "respects the set initial width, position #{position}" do
53
+ box = create_box(children: @text_boxes[0..1], width: 50, style: {position: position})
54
+ check_box(box, 50, 80)
55
+ end
56
+
57
+ it "respects the set initial height, position #{position}" do
58
+ box = create_box(children: @text_boxes[0..1], height: 50, equal_height: false,
59
+ style: {position: position})
60
+ check_box(box, 100, 50)
61
+ end
62
+
63
+ it "respects the border and padding around all columns, position #{position}" do
64
+ box = create_box(children: @fixed_size_boxes[0, 3],
65
+ style: {border: {width: [5, 4, 3, 2]}, padding: [5, 4, 3, 2], position: position})
66
+ check_box(box, 100, 36, [[4, 80], [4, 70], [53, 80]])
67
+ end
68
+ end
69
+
70
+ it "uses the frame's current cursor position and available width/height when style position=:default" do
71
+ @frame.remove_area(Geom2D::Polygon([0, 0], [10, 0], [10, 90], [100, 90], [100, 100], [0, 100]))
72
+ box = create_box(children: @fixed_size_boxes[0, 4])
73
+ check_box(box, 90, 20, [[10, 80], [10, 70], [60, 80], [60, 70]])
74
+ end
75
+
76
+ it "respects the frame's shape when style position=:flow" do
77
+ @frame.remove_area(Geom2D::Polygon([30, 65], [70, 65], [70, 35], [30, 35]))
78
+ box = create_box(children: @text_boxes[0, 3], style: {position: :flow})
79
+ check_box(box, 100, 70, [[0, 70], [0, 60], [0, 30], [55, 80], [55, 70], [70, 30]])
80
+ end
81
+
82
+ it "allows fitting the contents to fill the columns instead of equalizing the height" do
83
+ box = create_box(children: @fixed_size_boxes, equal_height: false)
84
+ check_box(box, 100, 100, [[0, 90], [0, 80], [0, 70], [0, 60], [0, 50], [0, 40], [0, 30],
85
+ [0, 20], [0, 10], [0, 0], [55, 90], [55, 80], [55, 70],
86
+ [55, 60], [55, 50]])
87
+ end
88
+
89
+ describe "columns calculations" do
90
+ it "works for a single column with a specified width" do
91
+ box = create_box(children: @fixed_size_boxes[0..0], columns: [50])
92
+ check_box(box, 50, 10, [[0, 90]])
93
+ end
94
+
95
+ it "works for multiple columns with specified widths" do
96
+ box = create_box(children: @fixed_size_boxes[0..1], columns: [50, 30])
97
+ check_box(box, 90, 10, [[0, 90], [60, 90]])
98
+ end
99
+
100
+ it "works for a single column with auto-width" do
101
+ box = create_box(children: @fixed_size_boxes[0..0], columns: [-5])
102
+ check_box(box, 100, 10, [[0, 90]])
103
+ end
104
+
105
+ it "works for multiple columns with auto-widths" do
106
+ box = create_box(children: @fixed_size_boxes[0..1], columns: [-2, -1])
107
+ check_box(box, 100, 10, [[0, 90], [70, 90]])
108
+ end
109
+
110
+ it "works for mixed columns with specified widths and auto-widths" do
111
+ box = create_box(children: @fixed_size_boxes[0..2], columns: [20, -1, -2])
112
+ check_box(box, 100, 10, [[0, 90], [30, 90], [60, 90]])
113
+ end
114
+
115
+ it "cycles the gap array" do
116
+ box = create_box(children: @fixed_size_boxes[0..3], columns: 4, gaps: [5, 10])
117
+ check_box(box, 100, 10, [[0, 90], [25, 90], [55, 90], [80, 90]])
118
+ end
119
+
120
+ it "fails if the necessary width is larger than the available one" do
121
+ box = create_box(children: @fixed_size_boxes[0..2], columns: 4, gaps: [40])
122
+ refute(box.fit(100, 100, @frame))
123
+ end
124
+ end
125
+ end
126
+
127
+ it "splits the children if they are too big to fill the colums" do
128
+ box = create_box(children: @fixed_size_boxes, width: 50, height: 50)
129
+ box.fit(100, 100, @frame)
130
+ box_a, box_b = box.split(100, 100, @frame)
131
+ assert_same(box, box_a)
132
+ assert(box_b.split_box?)
133
+ assert_equal(5, box_b.children.size)
134
+ end
135
+
136
+ it "draws the result onto the canvas" do
137
+ box = create_box(children: @fixed_size_boxes)
138
+ box.fit(100, 100, @frame)
139
+
140
+ @canvas = HexaPDF::Document.new.pages.add.canvas
141
+ box.draw(@canvas, 0, 0)
142
+ operators = 90.step(to: 20, by: -10).map do |y|
143
+ [[:save_graphics_state],
144
+ [:concatenate_matrix, [1, 0, 0, 1, 0, y]],
145
+ [:move_to, [0, 0]],
146
+ [:end_path],
147
+ [:restore_graphics_state]]
148
+ end
149
+ operators.concat(90.step(to: 30, by: -10).map do |y|
150
+ [[:save_graphics_state],
151
+ [:concatenate_matrix, [1, 0, 0, 1, 55, y]],
152
+ [:move_to, [0, 0]],
153
+ [:end_path],
154
+ [:restore_graphics_state]]
155
+ end)
156
+ operators.flatten!(1)
157
+ assert_operators(@canvas.contents, operators)
158
+ end
159
+ end
@@ -3,6 +3,35 @@
3
3
  require 'test_helper'
4
4
  require 'hexapdf/layout/frame'
5
5
  require 'hexapdf/layout/box'
6
+ require 'hexapdf/document'
7
+
8
+ describe HexaPDF::Layout::Frame do
9
+ it "shows the box's mask area on #draw when using debug output" do
10
+ doc = HexaPDF::Document.new(config: {'debug' => true})
11
+ canvas = doc.pages.add.canvas
12
+ box = HexaPDF::Layout::Box.create(width: 20, height: 20) {}
13
+ result = HexaPDF::Layout::Frame::FitResult.new(box)
14
+ result.mask = Geom2D::Polygon([0, 0], [0, 20], [20, 20], [20, 0])
15
+ result.x = result.y = 0
16
+ result.draw(canvas)
17
+ assert_equal(<<~CONTENTS, canvas.contents)
18
+ q
19
+ 0.0 0.501961 0.0 rg
20
+ 0.0 0.392157 0.0 RG
21
+ /GS1 gs
22
+ 0 0 m
23
+ 0 20 l
24
+ 20 20 l
25
+ 20 0 l
26
+ h
27
+ B
28
+ Q
29
+ q
30
+ 1 0 0 1 0 0 cm
31
+ Q
32
+ CONTENTS
33
+ end
34
+ end
6
35
 
7
36
  describe HexaPDF::Layout::Frame do
8
37
  before do
@@ -23,16 +52,14 @@ describe HexaPDF::Layout::Frame do
23
52
  assert_equal(150, @frame.available_height)
24
53
  end
25
54
 
26
- describe "contour_line" do
27
- it "has a contour line equal to the bounding box by default" do
28
- assert_equal([[5, 10], [105, 10], [105, 160], [5, 160]], @frame.contour_line.polygons[0].to_a)
29
- end
30
-
31
- it "can have a custom contour line polygon" do
32
- contour_line = Geom2D::Polygon([0, 0], [10, 10], [10, 0])
33
- frame = HexaPDF::Layout::Frame.new(0, 0, 10, 10, contour_line: contour_line)
34
- assert_same(contour_line, frame.contour_line)
35
- end
55
+ it "allows setting the shape of the frame on initialization" do
56
+ shape = Geom2D::Polygon([50, 10], [55, 100], [105, 100], [105, 10])
57
+ frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150, shape: shape)
58
+ assert_equal(shape, frame.shape)
59
+ assert_equal(55, frame.x)
60
+ assert_equal(100, frame.y)
61
+ assert_equal(50, frame.available_width)
62
+ assert_equal(90, frame.available_height)
36
63
  end
37
64
 
38
65
  it "returns an appropriate width specification object" do
@@ -44,15 +71,25 @@ describe HexaPDF::Layout::Frame do
44
71
  before do
45
72
  @frame = HexaPDF::Layout::Frame.new(10, 10, 100, 100)
46
73
  @canvas = Minitest::Mock.new
74
+ def @canvas.context
75
+ Object.new.tap do |ctx|
76
+ def ctx.document; Object.new.tap {|doc| def doc.config; Hash.new(false); end } end
77
+ end
78
+ end
47
79
  end
48
80
 
49
81
  # Creates a box with the given option, storing it in @box, and draws it inside @frame. It is
50
82
  # checked whether the box coordinates are pos and whether the frame has the shape given by
51
83
  # points.
52
- def check_box(box_opts, pos, points)
84
+ def check_box(box_opts, pos, mask, points)
85
+ flow_supported = !box_opts.delete(:doesnt_support_position_flow)
53
86
  @box = HexaPDF::Layout::Box.create(**box_opts) {}
87
+ @box.define_singleton_method(:supports_position_flow?) { true } if flow_supported
54
88
  @canvas.expect(:translate, nil, pos)
55
- assert(@frame.draw(@canvas, @box))
89
+ fit_result = @frame.fit(@box)
90
+ refute_nil(fit_result)
91
+ @frame.draw(@canvas, fit_result)
92
+ assert_equal(mask, fit_result.mask.bbox.to_a)
56
93
  assert_equal(points, @frame.shape.polygons.map(&:to_a))
57
94
  @canvas.verify
58
95
  end
@@ -75,6 +112,7 @@ describe HexaPDF::Layout::Frame do
75
112
  check_box(
76
113
  {width: 50, height: 50, position: :absolute, position_hint: [10, 10]},
77
114
  [20, 20],
115
+ [20, 20, 70, 70],
78
116
  [[[10, 10], [110, 10], [110, 110], [10, 110]],
79
117
  [[20, 20], [70, 20], [70, 70], [20, 70]]]
80
118
  )
@@ -84,6 +122,7 @@ describe HexaPDF::Layout::Frame do
84
122
  check_box(
85
123
  {position: :absolute, position_hint: [10, 10]},
86
124
  [20, 20],
125
+ [20, 20, 110, 110],
87
126
  [[[10, 10], [110, 10], [110, 20], [20, 20], [20, 110], [10, 110]]]
88
127
  )
89
128
  end
@@ -93,6 +132,7 @@ describe HexaPDF::Layout::Frame do
93
132
  {width: 50, height: 50, position: :absolute, position_hint: [10, 10],
94
133
  margin: [10, 20, 30, 40]},
95
134
  [20, 20],
135
+ [-20, -10, 90, 80],
96
136
  [[[10, 80], [90, 80], [90, 10], [110, 10], [110, 110], [10, 110]]]
97
137
  )
98
138
  end
@@ -102,18 +142,21 @@ describe HexaPDF::Layout::Frame do
102
142
  it "draws the box on the left side" do
103
143
  check_box({width: 50, height: 50},
104
144
  [10, 60],
145
+ [10, 60, 110, 110],
105
146
  [[[10, 10], [110, 10], [110, 60], [10, 60]]])
106
147
  end
107
148
 
108
149
  it "draws the box on the right side" do
109
150
  check_box({width: 50, height: 50, position_hint: :right},
110
151
  [60, 60],
152
+ [10, 60, 110, 110],
111
153
  [[[10, 10], [110, 10], [110, 60], [10, 60]]])
112
154
  end
113
155
 
114
156
  it "draws the box in the center" do
115
157
  check_box({width: 50, height: 50, position_hint: :center},
116
158
  [35, 60],
159
+ [10, 60, 110, 110],
117
160
  [[[10, 10], [110, 10], [110, 60], [10, 60]]])
118
161
  end
119
162
 
@@ -121,7 +164,7 @@ describe HexaPDF::Layout::Frame do
121
164
  [:left, :center, :right].each do |hint|
122
165
  it "ignores all margins if the box fills the whole frame, with position hint #{hint}" do
123
166
  check_box({margin: 10, position_hint: hint},
124
- [10, 10], [])
167
+ [10, 10], [10, 10, 110, 110], [])
125
168
  assert_equal(100, @box.width)
126
169
  assert_equal(100, @box.height)
127
170
  end
@@ -130,6 +173,7 @@ describe HexaPDF::Layout::Frame do
130
173
  "frame's, with position hint #{hint}" do
131
174
  check_box({height: 50, margin: 10, position_hint: hint},
132
175
  [10, 60],
176
+ [10, 50, 110, 110],
133
177
  [[[10, 10], [110, 10], [110, 50], [10, 50]]])
134
178
  end
135
179
 
@@ -138,6 +182,7 @@ describe HexaPDF::Layout::Frame do
138
182
  remove_area(:top)
139
183
  check_box({height: 50, margin: 10, position_hint: hint},
140
184
  [10, 40],
185
+ [10, 30, 110, 100],
141
186
  [[[10, 10], [110, 10], [110, 30], [10, 30]]])
142
187
  assert_equal(100, @box.width)
143
188
  end
@@ -147,6 +192,7 @@ describe HexaPDF::Layout::Frame do
147
192
  remove_area(:left)
148
193
  check_box({height: 50, margin: 10, position_hint: hint},
149
194
  [30, 60],
195
+ [10, 50, 110, 110],
150
196
  [[[20, 10], [110, 10], [110, 50], [20, 50]]])
151
197
  assert_equal(80, @box.width)
152
198
  end
@@ -156,6 +202,7 @@ describe HexaPDF::Layout::Frame do
156
202
  remove_area(:right)
157
203
  check_box({height: 50, margin: 10, position_hint: hint},
158
204
  [10, 60],
205
+ [10, 50, 110, 110],
159
206
  [[[10, 10], [100, 10], [100, 50], [10, 50]]])
160
207
  assert_equal(80, @box.width)
161
208
  end
@@ -164,6 +211,7 @@ describe HexaPDF::Layout::Frame do
164
211
  it "perfectly centers a box if possible, margins ignored" do
165
212
  check_box({width: 50, height: 10, margin: [10, 10, 10, 20], position_hint: :center},
166
213
  [35, 100],
214
+ [10, 90, 110, 110],
167
215
  [[[10, 10], [110, 10], [110, 90], [10, 90]]])
168
216
  end
169
217
 
@@ -171,6 +219,7 @@ describe HexaPDF::Layout::Frame do
171
219
  remove_area(:left, :right)
172
220
  check_box({width: 40, height: 10, margin: [10, 10, 10, 20], position_hint: :center},
173
221
  [40, 100],
222
+ [10, 90, 110, 110],
174
223
  [[[20, 10], [100, 10], [100, 90], [20, 90]]])
175
224
  end
176
225
 
@@ -178,6 +227,7 @@ describe HexaPDF::Layout::Frame do
178
227
  remove_area(:left, :right)
179
228
  check_box({width: 20, height: 10, margin: [10, 10, 10, 40], position_hint: :center},
180
229
  [65, 100],
230
+ [10, 90, 110, 110],
181
231
  [[[20, 10], [100, 10], [100, 90], [20, 90]]])
182
232
  end
183
233
  end
@@ -187,18 +237,21 @@ describe HexaPDF::Layout::Frame do
187
237
  it "draws the box on the left side" do
188
238
  check_box({width: 50, height: 50, position: :float},
189
239
  [10, 60],
240
+ [10, 60, 60, 110],
190
241
  [[[10, 10], [110, 10], [110, 110], [60, 110], [60, 60], [10, 60]]])
191
242
  end
192
243
 
193
244
  it "draws the box on the right side" do
194
245
  check_box({width: 50, height: 50, position: :float, position_hint: :right},
195
246
  [60, 60],
247
+ [60, 60, 110, 110],
196
248
  [[[10, 10], [110, 10], [110, 60], [60, 60], [60, 110], [10, 110]]])
197
249
  end
198
250
 
199
251
  it "draws the box in the center" do
200
252
  check_box({width: 50, height: 50, position: :float, position_hint: :center},
201
253
  [35, 60],
254
+ [35, 60, 85, 110],
202
255
  [[[10, 10], [110, 10], [110, 110], [85, 110], [85, 60], [35, 60],
203
256
  [35, 110], [10, 110]]])
204
257
  end
@@ -207,7 +260,7 @@ describe HexaPDF::Layout::Frame do
207
260
  [:left, :center, :right].each do |hint|
208
261
  it "ignores all margins if the box fills the whole frame, with position hint #{hint}" do
209
262
  check_box({margin: 10, position: :float, position_hint: hint},
210
- [10, 10], [])
263
+ [10, 10], [10, 10, 110, 110], [])
211
264
  assert_equal(100, @box.width)
212
265
  assert_equal(100, @box.height)
213
266
  end
@@ -216,6 +269,7 @@ describe HexaPDF::Layout::Frame do
216
269
  it "ignores the left, but not the right margin if aligned left to the frame border" do
217
270
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :left},
218
271
  [10, 60],
272
+ [10, 50, 70, 110],
219
273
  [[[10, 10], [110, 10], [110, 110], [70, 110], [70, 50], [10, 50]]])
220
274
  end
221
275
 
@@ -223,12 +277,14 @@ describe HexaPDF::Layout::Frame do
223
277
  remove_area(:left)
224
278
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :left},
225
279
  [30, 60],
280
+ [20, 50, 90, 110],
226
281
  [[[20, 10], [110, 10], [110, 110], [90, 110], [90, 50], [20, 50]]])
227
282
  end
228
283
 
229
284
  it "uses the left and the right margin if aligned center" do
230
285
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :center},
231
286
  [35, 60],
287
+ [25, 50, 95, 110],
232
288
  [[[10, 10], [110, 10], [110, 110], [95, 110], [95, 50], [25, 50],
233
289
  [25, 110], [10, 110]]])
234
290
  end
@@ -236,6 +292,7 @@ describe HexaPDF::Layout::Frame do
236
292
  it "ignores the right, but not the left margin if aligned right to the frame border" do
237
293
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :right},
238
294
  [60, 60],
295
+ [50, 50, 110, 110],
239
296
  [[[10, 10], [110, 10], [110, 50], [50, 50], [50, 110], [10, 110]]])
240
297
  end
241
298
 
@@ -243,6 +300,7 @@ describe HexaPDF::Layout::Frame do
243
300
  remove_area(:right)
244
301
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :right},
245
302
  [40, 60],
303
+ [30, 50, 100, 110],
246
304
  [[[10, 10], [100, 10], [100, 50], [30, 50], [30, 110], [10, 110]]])
247
305
  end
248
306
  end
@@ -250,35 +308,48 @@ describe HexaPDF::Layout::Frame do
250
308
 
251
309
  describe "flowing boxes" do
252
310
  it "flows inside the frame's outline" do
253
- check_box({width: 10, height: 20, position: :flow},
311
+ check_box({width: 10, height: 20, margin: 10, position: :flow},
254
312
  [0, 90],
255
- [[[10, 10], [110, 10], [110, 90], [10, 90]]])
313
+ [10, 80, 110, 110],
314
+ [[[10, 10], [110, 10], [110, 80], [10, 80]]])
256
315
  end
257
- end
258
316
 
259
- it "doesn't draw the box if it doesn't fit into the available space" do
260
- box = HexaPDF::Layout::Box.create(width: 150, height: 50)
261
- refute(@frame.draw(@canvas, box))
317
+ it "uses position=default if the box indicates it doesn't support flowing contents" do
318
+ check_box({width: 10, height: 20, margin: 10, position: :flow, doesnt_support_position_flow: true},
319
+ [10, 90],
320
+ [10, 80, 110, 110],
321
+ [[[10, 10], [110, 10], [110, 80], [10, 80]]])
322
+ end
262
323
  end
263
324
 
264
325
  it "can't fit the box if there is no available space" do
265
326
  @frame.remove_area(Geom2D::Polygon([0, 0], [110, 0], [110, 110], [0, 110]))
266
327
  box = HexaPDF::Layout::Box.create
267
- refute(@frame.fit(box))
328
+ refute(@frame.fit(box).success?)
268
329
  end
269
330
 
270
- it "draws the box even if the box's height is zero" do
271
- box = HexaPDF::Layout::Box.create
272
- box.define_singleton_method(:height) { 0 }
273
- assert(@frame.draw(@canvas, box))
331
+ it "handles (but doesn't draw) the box if the its height or width is zero" do
332
+ result = Minitest::Mock.new
333
+ box = Minitest::Mock.new
334
+
335
+ result.expect(:box, box)
336
+ box.expect(:height, 0)
337
+ @frame.draw(@canvas, result)
338
+
339
+ result.expect(:box, box)
340
+ box.expect(:height, 5)
341
+ result.expect(:box, box)
342
+ box.expect(:width, 0)
343
+ @frame.draw(@canvas, result)
344
+
345
+ result.verify
274
346
  end
275
347
  end
276
348
 
277
349
  describe "split" do
278
350
  it "splits the box if necessary" do
279
351
  box = HexaPDF::Layout::Box.create(width: 10, height: 10)
280
- assert_equal([nil, box], @frame.split(box))
281
- assert_nil(@frame.instance_variable_get(:@fit_data).box)
352
+ assert_equal([box, nil], @frame.split(@frame.fit(box)))
282
353
  end
283
354
  end
284
355
 
@@ -348,17 +419,7 @@ describe HexaPDF::Layout::Frame do
348
419
  frame.remove_area(Geom2D::PolygonSet(top_cut, left_cut))
349
420
 
350
421
  check_regions(frame, [[0, 100, 20, 60], [0, 90, 20, 50], [0, 80, 100, 40],
351
- [30, 40, 70, 40], [0, 20, 100, 20]])
352
- end
353
- end
354
-
355
- describe "remove_area" do
356
- it "recalculates the contour line only if necessary" do
357
- contour = Geom2D::Polygon([10, 10], [10, 90], [90, 90], [90, 10])
358
- frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100, contour_line: contour)
359
- frame.remove_area(Geom2D::Polygon([0, 0], [20, 0], [20, 100], [0, 100]))
360
- assert_equal([[[20, 10], [90, 10], [90, 90], [20, 90]]],
361
- frame.contour_line.polygons.map(&:to_a))
422
+ [30, 80, 70, 80], [0, 20, 100, 20]])
362
423
  end
363
424
  end
364
425
  end
@@ -13,7 +13,7 @@ describe HexaPDF::Layout::ImageBox do
13
13
  end
14
14
 
15
15
  def create_box(**kwargs)
16
- HexaPDF::Layout::ImageBox.new(@image, **kwargs)
16
+ HexaPDF::Layout::ImageBox.new(image: @image, **kwargs)
17
17
  end
18
18
 
19
19
  describe "initialize" do