hexapdf 0.23.0 → 0.24.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 (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