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
@@ -0,0 +1,249 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative '../content/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/layout/list_box'
7
+
8
+ describe HexaPDF::Layout::ListBox 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
+ end
16
+
17
+ def create_box(**kwargs)
18
+ HexaPDF::Layout::ListBox.new(content_indentation: 10, **kwargs)
19
+ end
20
+
21
+ def check_box(box, width, height, fit_pos = nil)
22
+ assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box fit?")
23
+ assert_equal(width, box.width, "box width")
24
+ assert_equal(height, box.height, "box height")
25
+ if fit_pos
26
+ results = box.instance_variable_get(:@results)
27
+ results.each_with_index do |box_fitter, item_index|
28
+ box_fitter.fit_results.each_with_index do |fit_result, result_index|
29
+ x, y = fit_pos.shift
30
+ assert_equal(x, fit_result.x, "item #{item_index}, result #{result_index}, x")
31
+ assert_equal(y, fit_result.y, "item #{item_index}, result #{result_index}, y")
32
+ end
33
+ end
34
+ assert(fit_pos.empty?)
35
+ end
36
+ end
37
+
38
+ describe "initialize" do
39
+ it "creates a new instance with the given arguments" do
40
+ box = create_box(children: [:a], item_type: :circle, content_indentation: 15,
41
+ start_number: 4, item_spacing: 20)
42
+ assert_equal([:a], box.children)
43
+ assert_equal(:circle, box.item_type)
44
+ assert_equal(15, box.content_indentation)
45
+ assert_equal(4, box.start_number)
46
+ assert_equal(20, box.item_spacing)
47
+ assert(box.supports_position_flow?)
48
+ end
49
+ end
50
+
51
+ describe "fit" do
52
+ [:default, :flow].each do |position|
53
+ it "respects the set initial width, position #{position}" do
54
+ box = create_box(children: @text_boxes[0, 2], width: 50, style: {position: position})
55
+ check_box(box, 50, 80)
56
+ end
57
+
58
+ it "respects the set initial height, position #{position}" do
59
+ box = create_box(children: @text_boxes[0, 2], height: 50, style: {position: position})
60
+ check_box(box, 100, 40)
61
+ end
62
+
63
+ it "respects the border and padding around all list items, position #{position}" do
64
+ box = create_box(children: @text_boxes[0, 2],
65
+ style: {border: {width: [5, 4, 3, 2]}, padding: [5, 4, 3, 2], position: position})
66
+ check_box(box, 100, 76, [[14, 60], [14, 30]])
67
+ end
68
+ end
69
+
70
+ it "uses the frame's current cursor position and available width/height when 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: @text_boxes[0, 2])
73
+ check_box(box, 90, 40, [[20, 70], [20, 50]])
74
+ end
75
+
76
+ it "respects the frame's shape when style position=:flow" do
77
+ @frame.remove_area(Geom2D::Polygon([0, 0], [0, 40], [40, 40], [40, 0]))
78
+ box = create_box(children: @text_boxes[0, 4], style: {position: :flow})
79
+ check_box(box, 100, 90, [[10, 80], [10, 60], [10, 40], [50, 10]])
80
+ end
81
+
82
+ it "respects the content indentation" do
83
+ box = create_box(children: @text_boxes[0, 1], content_indentation: 30)
84
+ check_box(box, 100, 30, [[30, 70]])
85
+ end
86
+
87
+ it "respects the spacing between list items" do
88
+ box = create_box(children: @text_boxes[0, 2], item_spacing: 30)
89
+ check_box(box, 100, 70, [[10, 80], [10, 30]])
90
+ end
91
+ end
92
+
93
+ describe "split" do
94
+ it "splits before a list item if no part of it will fit" do
95
+ box = create_box(children: @text_boxes[0, 2], height: 20)
96
+ box.fit(100, 100, @frame)
97
+ box_a, box_b = box.split(100, 100, @frame)
98
+ assert_same(box, box_a)
99
+ assert_equal(:show_first_marker, box_b.split_box?)
100
+ assert_equal(1, box_a.instance_variable_get(:@results)[0].fit_results.size)
101
+ assert_equal(1, box_b.children.size)
102
+ assert_equal(2, box_b.start_number)
103
+ end
104
+
105
+ it "splits a list item if some part of it will fit" do
106
+ box = create_box(children: @text_boxes[0, 2], height: 10)
107
+ box.fit(100, 100, @frame)
108
+ box_a, box_b = box.split(100, 100, @frame)
109
+ assert_same(box, box_a)
110
+ assert_equal(:hide_first_marker, box_b.split_box?)
111
+ assert_equal(1, box_a.instance_variable_get(:@results)[0].fit_results.size)
112
+ assert_equal(2, box_b.children.size)
113
+ assert_equal(1, box_b.start_number)
114
+ end
115
+ end
116
+
117
+ describe "draw" do
118
+ before do
119
+ @canvas = HexaPDF::Document.new.pages.add.canvas
120
+ draw_block = lambda {|canvas, box| }
121
+ @fixed_size_boxes = 5.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
122
+ end
123
+
124
+ it "draws the result" do
125
+ box = create_box(children: @fixed_size_boxes[0, 2])
126
+ box.fit(100, 100, @frame)
127
+ box.draw(@canvas, 0, 0)
128
+ operators = [
129
+ [:save_graphics_state],
130
+ [:set_font_and_size, [:F1, 10]],
131
+ [:begin_text],
132
+ [:set_text_matrix, [1, 0, 0, 1, 1.5, 93.17]],
133
+ [:show_text, ["\x95".b]],
134
+ [:end_text],
135
+ [:restore_graphics_state],
136
+ [:save_graphics_state],
137
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 90]],
138
+ [:restore_graphics_state],
139
+
140
+ [:save_graphics_state],
141
+ [:set_font_and_size, [:F1, 10]],
142
+ [:begin_text],
143
+ [:set_text_matrix, [1, 0, 0, 1, 1.5, 83.17]],
144
+ [:show_text, ["\x95".b]],
145
+ [:end_text],
146
+ [:restore_graphics_state],
147
+ [:save_graphics_state],
148
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 80]],
149
+ [:restore_graphics_state],
150
+ ]
151
+ assert_operators(@canvas.contents, operators)
152
+ end
153
+
154
+ it "draws a cicle as marker" do
155
+ box = create_box(children: @fixed_size_boxes[0, 1], item_type: :circle)
156
+ box.fit(100, 100, @frame)
157
+ box.draw(@canvas, 0, 0)
158
+ operators = [
159
+ [:save_graphics_state],
160
+ [:set_font_and_size, [:F1, 5]],
161
+ [:set_text_rise, [-5.555556]],
162
+ [:begin_text],
163
+ [:set_text_matrix, [1, 0, 0, 1, 0.635, 100]],
164
+ [:show_text, ["m".b]],
165
+ [:end_text],
166
+ [:restore_graphics_state],
167
+ [:save_graphics_state],
168
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 90]],
169
+ [:restore_graphics_state],
170
+ ]
171
+ assert_operators(@canvas.contents, operators)
172
+ end
173
+
174
+ it "draws a square as marker" do
175
+ box = create_box(children: @fixed_size_boxes[0, 1], item_type: :square)
176
+ box.fit(100, 100, @frame)
177
+ box.draw(@canvas, 0, 0)
178
+ operators = [
179
+ [:save_graphics_state],
180
+ [:set_font_and_size, [:F1, 5]],
181
+ [:set_text_rise, [-5.555556]],
182
+ [:begin_text],
183
+ [:set_text_matrix, [1, 0, 0, 1, 1.195, 100]],
184
+ [:show_text, ["n".b]],
185
+ [:end_text],
186
+ [:restore_graphics_state],
187
+ [:save_graphics_state],
188
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 90]],
189
+ [:restore_graphics_state],
190
+ ]
191
+ assert_operators(@canvas.contents, operators)
192
+ end
193
+
194
+ it "draws decimal numbers as marker" do
195
+ box = create_box(children: @fixed_size_boxes[0, 2], item_type: :decimal,
196
+ content_indentation: 20)
197
+ box.fit(100, 100, @frame)
198
+ box.draw(@canvas, 0, 0)
199
+ operators = [
200
+ [:save_graphics_state],
201
+ [:set_font_and_size, [:F1, 10]],
202
+ [:begin_text],
203
+ [:set_text_matrix, [1, 0, 0, 1, 7.5, 93.17]],
204
+ [:show_text, ["1.".b]],
205
+ [:end_text],
206
+ [:restore_graphics_state],
207
+ [:save_graphics_state],
208
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 90]],
209
+ [:restore_graphics_state],
210
+
211
+ [:save_graphics_state],
212
+ [:set_font_and_size, [:F1, 10]],
213
+ [:begin_text],
214
+ [:set_text_matrix, [1, 0, 0, 1, 7.5, 83.17]],
215
+ [:show_text, ["2.".b]],
216
+ [:end_text],
217
+ [:restore_graphics_state],
218
+ [:save_graphics_state],
219
+ [:concatenate_matrix, [1, 0, 0, 1, 20, 80]],
220
+ [:restore_graphics_state],
221
+ ]
222
+ assert_operators(@canvas.contents, operators)
223
+ end
224
+
225
+ it "allows drawing custom markers" do
226
+ marker = lambda do |doc, list_box, index|
227
+ HexaPDF::Layout::Box.create(width: 10, height: 10) {}
228
+ end
229
+ box = create_box(children: @fixed_size_boxes[0, 1], item_type: marker)
230
+ box.fit(100, 100, @frame)
231
+ box.draw(@canvas, 0, 0)
232
+ operators = [
233
+ [:save_graphics_state],
234
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 90]],
235
+ [:restore_graphics_state],
236
+ [:save_graphics_state],
237
+ [:concatenate_matrix, [1, 0, 0, 1, 10, 90]],
238
+ [:restore_graphics_state],
239
+ ]
240
+ assert_operators(@canvas.contents, operators)
241
+ end
242
+
243
+ it "fails for unknown item types" do
244
+ box = create_box(children: @fixed_size_boxes[0, 1], item_type: :unknown)
245
+ box.fit(100, 100, @frame)
246
+ assert_raises(HexaPDF::Error) { box.draw(@canvas, 0, 0) }
247
+ end
248
+ end
249
+ end
@@ -12,7 +12,7 @@ describe HexaPDF::Layout::TextBox do
12
12
  end
13
13
 
14
14
  def create_box(items, **kwargs)
15
- HexaPDF::Layout::TextBox.new(items, **kwargs)
15
+ HexaPDF::Layout::TextBox.new(items: items, **kwargs)
16
16
  end
17
17
 
18
18
  describe "initialize" do
@@ -20,6 +20,10 @@ describe HexaPDF::Layout::TextBox do
20
20
  box = create_box([], width: 100)
21
21
  assert_equal(100, box.width)
22
22
  end
23
+
24
+ it "supports flowing text around other content" do
25
+ assert(create_box([]).supports_position_flow?)
26
+ end
23
27
  end
24
28
 
25
29
  describe "fit" do
@@ -104,11 +108,22 @@ describe HexaPDF::Layout::TextBox do
104
108
  assert_equal([nil, box], box.split(100, 100, @frame))
105
109
  end
106
110
 
107
- it "splits the box if necessary" do
111
+ it "splits the box if necessary when using non-flowing text" do
108
112
  box = create_box([@inline_box] * 10)
109
113
  boxes = box.split(50, 10, @frame)
110
114
  assert_equal(2, boxes.length)
111
115
  assert_equal(box, boxes[0])
116
+ refute(boxes[0].split_box?)
117
+ assert(boxes[1].split_box?)
118
+ assert_equal(5, boxes[1].instance_variable_get(:@items).length)
119
+ end
120
+
121
+ it "splits the box if necessary when using flowing text that results in a wider box" do
122
+ @frame.remove_area(Geom2D::Polygon.new([[0, 100], [50, 100], [50, 10], [0, 10]]))
123
+ box = create_box([@inline_box] * 60, style: {position: :flow})
124
+ boxes = box.split(50, 100, @frame)
125
+ assert_equal(2, boxes.length)
126
+ assert_equal(box, boxes[0])
112
127
  assert_equal(5, boxes[1].instance_variable_get(:@items).length)
113
128
  end
114
129
  end
@@ -369,7 +369,7 @@ describe HexaPDF::Layout::TextFragment do
369
369
  end
370
370
 
371
371
  it "can be inspected" do
372
- frag = setup_fragment(@font.decode_utf8("H"))
372
+ frag = setup_fragment(@font.decode_utf8("H") << 5)
373
373
  assert_match(/:H/, frag.inspect)
374
374
  end
375
375
  end
@@ -57,6 +57,24 @@ module TestTextLayouterHelpers
57
57
  end
58
58
  end
59
59
 
60
+ describe HexaPDF::Layout::TextLayouter::Box do
61
+ it "can describe itself" do
62
+ assert_equal('Box["test"]', HexaPDF::Layout::TextLayouter::Box.new("test").inspect)
63
+ end
64
+ end
65
+
66
+ describe HexaPDF::Layout::TextLayouter::Glue do
67
+ it "can describe itself" do
68
+ assert_equal('Glue["test"]', HexaPDF::Layout::TextLayouter::Glue.new("test", 5, 5).inspect)
69
+ end
70
+ end
71
+
72
+ describe HexaPDF::Layout::TextLayouter::Penalty do
73
+ it "can describe itself" do
74
+ assert_equal('Penalty[100 10 nil]', HexaPDF::Layout::TextLayouter::Penalty.new(100, 10).inspect)
75
+ end
76
+ end
77
+
60
78
  describe HexaPDF::Layout::TextLayouter::SimpleTextSegmentation do
61
79
  include TestTextLayouterHelpers
62
80
 
@@ -556,36 +574,43 @@ describe HexaPDF::Layout::TextLayouter do
556
574
  end
557
575
  end
558
576
 
559
- describe "breaks a text fragment into parts if it is wider than the available width" do
577
+ describe "breaking into parts of a too wide text fragment" do
560
578
  before do
561
- @str = " This is averylongstring"
579
+ @str = " Thisisaverylongstring"
562
580
  @frag = HexaPDF::Layout::TextFragment.create(@str, font: @font)
563
581
  end
564
582
 
565
- it "works with fixed width" do
583
+ it "arranges the parts if single parts fit into the available space" do
566
584
  result = @layouter.fit([@frag], 20, 100)
567
585
  assert(result.remaining_items.empty?)
568
586
  assert_equal(:success, result.status)
569
- assert_equal(@str.delete(" ").length, result.lines.sum {|l| l.items.sum {|i| i.items.count } })
570
- assert_equal(54, result.height)
587
+ assert_equal(@str.strip.length, result.lines.sum {|l| l.items.sum {|i| i.items.count } })
588
+ assert_equal(45, result.height)
589
+ end
571
590
 
591
+ it "works even if a single part doesn't fit" do
572
592
  result = @layouter.fit([@frag], 1, 100)
573
- assert_equal(8, result.remaining_items.count)
593
+ assert_equal(@str.strip.length, result.remaining_items.count)
574
594
  assert_equal(:box_too_wide, result.status)
575
595
  end
576
596
 
577
- it "works with variable width" do
578
- width_block = lambda do |height, line_height|
579
- # 'averylongstring' would fit when only considering height but not height + line_height
580
- if height + line_height < 15
581
- 63
582
- else
583
- 10
584
- end
585
- end
586
- result = @layouter.fit([@frag], width_block, 30)
597
+ it "works if the parts of the broken fragment are used in another .fit call" do
598
+ text = "This is a long string to test"
599
+ frag = HexaPDF::Layout::TextFragment.create(text, font: @font)
600
+ result = @layouter.fit([frag], lambda {|h, _| h > 17 ? 0 : 25 }, 20)
601
+ refute(result.remaining_items.empty?)
587
602
  assert_equal(:height, result.status)
588
- assert_equal([26.95, 9.44, 7.77], result.lines.map {|l| l.width.round(3) })
603
+ assert_equal(["This", "is a"],
604
+ result.lines.map {|l| l.items.map {|tf| tf.items.map(&:str).join }.join })
605
+
606
+ # Simulating a split text box where the second part is tried to fit into same remaining
607
+ # space
608
+ result = @layouter.fit(result.remaining_items, 25, 1)
609
+
610
+ # Now fit into next available space
611
+ result = @layouter.fit(result.remaining_items, 25, 100)
612
+ assert_equal(["long", "string", "to test"],
613
+ result.lines.map {|l| l.items.map {|tf| tf.items.map(&:str).join }.join })
589
614
  end
590
615
  end
591
616
 
@@ -32,6 +32,13 @@ describe HexaPDF::Layout::WidthFromPolygon do
32
32
  assert_equal([2.5, 7.5], ws.call(5, 1))
33
33
  end
34
34
 
35
+ it "works for polygons in counterclockwise order with some segments crossing only top or bottom" do
36
+ ws = create_width_spec(Geom2D::PolygonSet(Geom2D::Polygon([55.0, 65.0], [70, 65], [70.0, 50.0],
37
+ [100.0, 50.0], [100.0, 63.0], [120, 63],
38
+ [120, 70], [55, 70])))
39
+ assert_equal([70, 30], ws.call(0, 10))
40
+ end
41
+
35
42
  it "works if some segments only cross the top line" do
36
43
  ws = create_width_spec(Geom2D::Polygon([0, 0], [0, 10], [2, 11], [4, 9], [6, 11], [10, 10],
37
44
  [10, 0]))
@@ -60,6 +67,12 @@ describe HexaPDF::Layout::WidthFromPolygon do
60
67
  assert_equal([5, 5], ws.call(4, 2))
61
68
  end
62
69
 
70
+ it "works in case of small floating point differences" do
71
+ ws = create_width_spec(Geom2D::Polygon([0, 0], [10, 0], [10, 5.99999999999994], [8, 6], [8, 10],
72
+ [6, 10], [6, 5], [0, 5]))
73
+ assert_equal([6, 4], ws.call(4.0, 3.0))
74
+ end
75
+
63
76
  describe "multiple polygons" do
64
77
  it "rectangle in rectangle" do
65
78
  ws = create_width_spec(Geom2D::PolygonSet(Geom2D::Polygon([0, 0], [0, 10], [10, 10], [10, 0]),
@@ -249,6 +249,17 @@ describe HexaPDF::Composer do
249
249
  end
250
250
  end
251
251
 
252
+ describe "box" do
253
+ it "creates the named box and draws it on the canvas" do
254
+ box = nil
255
+ @composer.define_singleton_method(:draw_box) {|arg| box = arg }
256
+ image = @composer.document.images.add(File.join(TEST_DATA_DIR, 'images', 'gray.jpg'))
257
+ @composer.box(:list, width: 20) {|list| list.image(image) }
258
+ assert_equal(20, box.width)
259
+ assert_same(image, box.children[0].image)
260
+ end
261
+ end
262
+
252
263
  describe "draw_box" do
253
264
  def create_box(**kwargs)
254
265
  HexaPDF::Layout::Box.new(**kwargs) {}
@@ -37,7 +37,7 @@ describe HexaPDF::DictionaryFields do
37
37
  describe "convert" do
38
38
  it "returns the converted object, using the first usable converter" do
39
39
  doc = Minitest::Mock.new
40
- doc.expect(:wrap, :data, [Hash, Hash])
40
+ doc.expect(:wrap, :data, [Hash], type: Integer)
41
41
  @field.convert({}, doc)
42
42
  doc.verify
43
43
 
@@ -67,20 +67,20 @@ describe HexaPDF::DictionaryFields do
67
67
  end
68
68
 
69
69
  it "allows conversion from a hash" do
70
- @doc.expect(:wrap, :data, [Hash, Hash])
70
+ @doc.expect(:wrap, :data, [Hash], type: Class)
71
71
  @field.convert({Test: :value}, @doc)
72
72
  @doc.verify
73
73
  end
74
74
 
75
75
  it "allows conversion from a Dictionary" do
76
- @doc.expect(:wrap, :data, [HexaPDF::Dictionary, Hash])
76
+ @doc.expect(:wrap, :data, [HexaPDF::Dictionary], type: Class)
77
77
  @field.convert(HexaPDF::Dictionary.new({Test: :value}), @doc)
78
78
  @doc.verify
79
79
  end
80
80
 
81
81
  it "allows conversion from an HexaPDF::Dictionary to a Stream if stream data is set" do
82
82
  @field = self.class::Field.new(HexaPDF::Stream)
83
- @doc.expect(:wrap, :data, [HexaPDF::Dictionary, Hash])
83
+ @doc.expect(:wrap, :data, [HexaPDF::Dictionary], type: Class)
84
84
  data = HexaPDF::PDFData.new({}, 0, 0, "")
85
85
  @field.convert(HexaPDF::Dictionary.new(data), @doc)
86
86
  @doc.verify
@@ -108,7 +108,7 @@ describe HexaPDF::DictionaryFields do
108
108
  end
109
109
 
110
110
  it "allows conversion from an array" do
111
- @doc.expect(:wrap, :data, [[1, 2], {type: HexaPDF::PDFArray}])
111
+ @doc.expect(:wrap, :data, [[1, 2]], type: HexaPDF::PDFArray)
112
112
  @field.convert([1, 2], @doc)
113
113
  @doc.verify
114
114
  end
@@ -208,14 +208,14 @@ describe HexaPDF::DictionaryFields do
208
208
 
209
209
  it "allows conversion from a string" do
210
210
  @doc = Minitest::Mock.new
211
- @doc.expect(:wrap, :data, [{F: 'test'}, {type: HexaPDF::Type::FileSpecification}])
211
+ @doc.expect(:wrap, :data, [{F: 'test'}], type: HexaPDF::Type::FileSpecification)
212
212
  @field.convert('test', @doc)
213
213
  @doc.verify
214
214
  end
215
215
 
216
216
  it "allows conversion from a hash/dictionary" do
217
217
  @doc = Minitest::Mock.new
218
- @doc.expect(:wrap, :data, [{F: 'test'}, {type: HexaPDF::Type::FileSpecification}])
218
+ @doc.expect(:wrap, :data, [{F: 'test'}], type: HexaPDF::Type::FileSpecification)
219
219
  @field.convert({F: 'test'}, @doc)
220
220
  @doc.verify
221
221
  end
@@ -232,7 +232,7 @@ describe HexaPDF::DictionaryFields do
232
232
 
233
233
  it "allows conversion to a Rectangle from an Array" do
234
234
  doc = Minitest::Mock.new
235
- doc.expect(:wrap, :data, [[0, 1, 2, 3], {type: HexaPDF::Rectangle}])
235
+ doc.expect(:wrap, :data, [[0, 1, 2, 3]], type: HexaPDF::Rectangle)
236
236
  @field.convert([0, 1, 2, 3], doc)
237
237
  doc.verify
238
238
  end
@@ -240,7 +240,7 @@ describe HexaPDF::DictionaryFields do
240
240
  it "allows conversion to a Rectangle from a HexaPDF::PDFArray" do
241
241
  data = HexaPDF::PDFArray.new([0, 1, 2, 3])
242
242
  doc = Minitest::Mock.new
243
- doc.expect(:wrap, :data, [data, {type: HexaPDF::Rectangle}])
243
+ doc.expect(:wrap, :data, [data], type: HexaPDF::Rectangle)
244
244
  @field.convert(data, doc)
245
245
  doc.verify
246
246
  end
@@ -302,7 +302,7 @@ describe HexaPDF::Document do
302
302
 
303
303
  it "defers to @revisions for iterating over all objects" do
304
304
  revs = Minitest::Mock.new
305
- revs.expect(:each_object, :retval, [{only_current: true, only_loaded: true}])
305
+ revs.expect(:each_object, :retval, only_current: true, only_loaded: true)
306
306
  doc = HexaPDF::Document.new
307
307
  doc.instance_variable_set(:@revisions, revs)
308
308
  doc.each(only_current: true, only_loaded: true)
@@ -328,7 +328,7 @@ describe HexaPDF::Document do
328
328
 
329
329
  it "doesn't decrypt the document if document.auto_encrypt=false" do
330
330
  test_file = File.join(TEST_DATA_DIR, 'standard-security-handler', 'nopwd-arc4-40bit-V1.pdf')
331
- doc = HexaPDF::Document.new(io: StringIO.new(File.read(test_file)),
331
+ doc = HexaPDF::Document.new(io: StringIO.new(File.binread(test_file)),
332
332
  config: {'document.auto_decrypt' => false})
333
333
  assert_kind_of(String, doc.trailer[:Info][:ModDate])
334
334
  handler = HexaPDF::Encryption::SecurityHandler.set_up_decryption(doc)
@@ -510,8 +510,8 @@ describe HexaPDF::Document do
510
510
 
511
511
  it "allows to conveniently sign a document" do
512
512
  mock = Minitest::Mock.new
513
- mock.expect(:handler, :handler, [{name: :handler, opt: :key}])
514
- mock.expect(:add, :added, [:io, :handler, {signature: :sig, write_options: :write_options}])
513
+ mock.expect(:handler, :handler, name: :handler, opt: :key)
514
+ mock.expect(:add, :added, [:io, :handler], signature: :sig, write_options: :write_options)
515
515
  @doc.instance_variable_set(:@signatures, mock)
516
516
  result = @doc.sign(:io, handler: :handler, write_options: :write_options, signature: :sig, opt: :key)
517
517
  assert_equal(:added, result)
@@ -62,7 +62,7 @@ describe HexaPDF::Filter do
62
62
 
63
63
  describe "source_from_file" do
64
64
  before do
65
- @file = Tempfile.new('hexapdf-filter')
65
+ @file = Tempfile.new('hexapdf-filter', binmode: true)
66
66
  @file.write(@str)
67
67
  @file.close
68
68
  end
@@ -543,14 +543,12 @@ describe HexaPDF::Parser do
543
543
  xref_section, trailer = @parser.load_revision(@parser.startxref_offset)
544
544
  assert_equal({Test: 'now'}, trailer)
545
545
  assert(xref_section[1].in_use?)
546
- refute(@parser.contains_xref_streams?)
547
546
  end
548
547
 
549
548
  it "works for a cross-reference stream" do
550
549
  xref_section, trailer = @parser.load_revision(212)
551
550
  assert_equal({Size: 2, Type: :XRef}, trailer)
552
551
  assert(xref_section[1].in_use?)
553
- assert(@parser.contains_xref_streams?)
554
552
  end
555
553
 
556
554
  it "fails if another object is found instead of a cross-reference stream" do
@@ -185,8 +185,8 @@ describe HexaPDF::Revisions do
185
185
  end
186
186
 
187
187
  it "iterates over all objects" do
188
- assert_equal([10, 400, 200, 20, @obj3, @obj3],
189
- @revisions.each_object(only_current: false).sort.map(&:value))
188
+ assert_equal([@obj3, 400, 200, @obj3, 10, 20],
189
+ @revisions.each_object(only_current: false).map(&:value))
190
190
  end
191
191
 
192
192
  it "iterates over all loaded objects" do
@@ -101,16 +101,12 @@ describe HexaPDF::Serializer do
101
101
  end
102
102
 
103
103
  it "serializes time like objects" do
104
- tz = ENV['TZ']
105
- ENV['TZ'] = 'Europe/Vienna'
106
104
  assert_serialized("(D:20150416094100)", Time.new(2015, 04, 16, 9, 41, 0, 0))
107
105
  assert_serialized("(D:20150416094100+01'00')", Time.new(2015, 04, 16, 9, 41, 0, 3600))
108
106
  assert_serialized("(D:20150416094100-01'20')", Time.new(2015, 04, 16, 9, 41, 0, -4800))
109
- assert_serialized("(D:20150416000000+02'00')", Date.parse("2015-04-16 9:41:00 +02:00"))
107
+ assert_serialized("(D:20150416000000)", Date.parse("2015-04-16 9:41:00 +02:00"))
110
108
  assert_serialized("(D:20150416094100+02'00')",
111
109
  Time.parse("2015-04-16 9:41:00 +02:00").to_datetime)
112
- ensure
113
- ENV['TZ'] = tz
114
110
  end
115
111
 
116
112
  it "serializes HexaPDF objects" do