hexapdf 0.21.1 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -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 +20 -37
  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 +31 -4
  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 +19 -6
  18. data/lib/hexapdf/cli/merge.rb +1 -1
  19. data/lib/hexapdf/cli/modify.rb +24 -4
  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 +66 -125
  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 +2 -2
  42. data/lib/hexapdf/document/destinations.rb +396 -0
  43. data/lib/hexapdf/document/files.rb +1 -1
  44. data/lib/hexapdf/document/fonts.rb +1 -1
  45. data/lib/hexapdf/document/images.rb +1 -1
  46. data/lib/hexapdf/document/layout.rb +397 -0
  47. data/lib/hexapdf/document/pages.rb +17 -1
  48. data/lib/hexapdf/document/signatures.rb +5 -4
  49. data/lib/hexapdf/document.rb +46 -90
  50. data/lib/hexapdf/encryption/aes.rb +1 -1
  51. data/lib/hexapdf/encryption/arc4.rb +1 -1
  52. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  53. data/lib/hexapdf/encryption/fast_arc4.rb +30 -21
  54. data/lib/hexapdf/encryption/identity.rb +1 -1
  55. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  56. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  57. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  58. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  59. data/lib/hexapdf/encryption.rb +1 -1
  60. data/lib/hexapdf/error.rb +1 -1
  61. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  62. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  63. data/lib/hexapdf/filter/crypt.rb +1 -1
  64. data/lib/hexapdf/filter/encryption.rb +1 -1
  65. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  66. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  67. data/lib/hexapdf/filter/pass_through.rb +1 -1
  68. data/lib/hexapdf/filter/predictor.rb +1 -1
  69. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  70. data/lib/hexapdf/filter.rb +1 -1
  71. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  72. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  73. data/lib/hexapdf/font/cmap.rb +1 -1
  74. data/lib/hexapdf/font/encoding/base.rb +1 -1
  75. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  76. data/lib/hexapdf/font/encoding/glyph_list.rb +2 -2
  77. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  78. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  79. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  82. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  83. data/lib/hexapdf/font/encoding.rb +1 -1
  84. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  85. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  86. data/lib/hexapdf/font/true_type/font.rb +1 -1
  87. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  88. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  89. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  90. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  91. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  92. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  93. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  94. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  95. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  96. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  97. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  98. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  99. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  100. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  101. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  102. data/lib/hexapdf/font/true_type/table.rb +1 -1
  103. data/lib/hexapdf/font/true_type.rb +1 -1
  104. data/lib/hexapdf/font/true_type_wrapper.rb +1 -1
  105. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  106. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  107. data/lib/hexapdf/font/type1/font.rb +1 -1
  108. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  109. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  110. data/lib/hexapdf/font/type1.rb +1 -1
  111. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  112. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  113. data/lib/hexapdf/font_loader/from_file.rb +1 -1
  114. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  115. data/lib/hexapdf/font_loader.rb +1 -1
  116. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  117. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  118. data/lib/hexapdf/image_loader/png.rb +1 -1
  119. data/lib/hexapdf/image_loader.rb +1 -1
  120. data/lib/hexapdf/importer.rb +1 -1
  121. data/lib/hexapdf/layout/box.rb +121 -22
  122. data/lib/hexapdf/layout/box_fitter.rb +136 -0
  123. data/lib/hexapdf/layout/column_box.rb +168 -89
  124. data/lib/hexapdf/layout/frame.rb +155 -140
  125. data/lib/hexapdf/layout/image_box.rb +19 -4
  126. data/lib/hexapdf/layout/inline_box.rb +1 -1
  127. data/lib/hexapdf/layout/line.rb +1 -1
  128. data/lib/hexapdf/layout/list_box.rb +355 -0
  129. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  130. data/lib/hexapdf/layout/style.rb +285 -8
  131. data/lib/hexapdf/layout/text_box.rb +30 -11
  132. data/lib/hexapdf/layout/text_fragment.rb +3 -2
  133. data/lib/hexapdf/layout/text_layouter.rb +23 -3
  134. data/lib/hexapdf/layout/text_shaper.rb +1 -1
  135. data/lib/hexapdf/layout/width_from_polygon.rb +12 -7
  136. data/lib/hexapdf/layout.rb +4 -1
  137. data/lib/hexapdf/name_tree_node.rb +1 -1
  138. data/lib/hexapdf/number_tree_node.rb +1 -1
  139. data/lib/hexapdf/object.rb +1 -1
  140. data/lib/hexapdf/parser.rb +1 -8
  141. data/lib/hexapdf/pdf_array.rb +1 -1
  142. data/lib/hexapdf/rectangle.rb +1 -1
  143. data/lib/hexapdf/reference.rb +1 -1
  144. data/lib/hexapdf/revision.rb +9 -2
  145. data/lib/hexapdf/revisions.rb +152 -51
  146. data/lib/hexapdf/serializer.rb +1 -1
  147. data/lib/hexapdf/stream.rb +1 -1
  148. data/lib/hexapdf/task/dereference.rb +1 -1
  149. data/lib/hexapdf/task/optimize.rb +22 -12
  150. data/lib/hexapdf/task.rb +1 -1
  151. data/lib/hexapdf/tokenizer.rb +1 -1
  152. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  153. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  154. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  155. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  156. data/lib/hexapdf/type/acro_form/form.rb +12 -6
  157. data/lib/hexapdf/type/acro_form/signature_field.rb +1 -1
  158. data/lib/hexapdf/type/acro_form/text_field.rb +9 -1
  159. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  160. data/lib/hexapdf/type/acro_form.rb +1 -1
  161. data/lib/hexapdf/type/action.rb +1 -1
  162. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  163. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  164. data/lib/hexapdf/type/actions/launch.rb +1 -1
  165. data/lib/hexapdf/type/actions/uri.rb +1 -1
  166. data/lib/hexapdf/type/actions.rb +1 -1
  167. data/lib/hexapdf/type/annotation.rb +1 -1
  168. data/lib/hexapdf/type/annotations/link.rb +1 -1
  169. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  170. data/lib/hexapdf/type/annotations/text.rb +1 -1
  171. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  172. data/lib/hexapdf/type/annotations.rb +1 -1
  173. data/lib/hexapdf/type/catalog.rb +10 -2
  174. data/lib/hexapdf/type/cid_font.rb +1 -1
  175. data/lib/hexapdf/type/embedded_file.rb +1 -1
  176. data/lib/hexapdf/type/file_specification.rb +1 -1
  177. data/lib/hexapdf/type/font.rb +1 -1
  178. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  179. data/lib/hexapdf/type/font_simple.rb +1 -1
  180. data/lib/hexapdf/type/font_true_type.rb +1 -1
  181. data/lib/hexapdf/type/font_type0.rb +1 -1
  182. data/lib/hexapdf/type/font_type1.rb +1 -1
  183. data/lib/hexapdf/type/font_type3.rb +1 -1
  184. data/lib/hexapdf/type/form.rb +1 -1
  185. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  186. data/lib/hexapdf/type/icon_fit.rb +1 -1
  187. data/lib/hexapdf/type/image.rb +48 -4
  188. data/lib/hexapdf/type/info.rb +1 -1
  189. data/lib/hexapdf/type/names.rb +14 -1
  190. data/lib/hexapdf/type/object_stream.rb +1 -1
  191. data/lib/hexapdf/type/page.rb +1 -1
  192. data/lib/hexapdf/type/page_tree_node.rb +19 -2
  193. data/lib/hexapdf/type/resources.rb +1 -1
  194. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  195. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +1 -1
  196. data/lib/hexapdf/type/signature/handler.rb +1 -1
  197. data/lib/hexapdf/type/signature/verification_result.rb +1 -1
  198. data/lib/hexapdf/type/signature.rb +1 -1
  199. data/lib/hexapdf/type/trailer.rb +2 -2
  200. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  201. data/lib/hexapdf/type/xref_stream.rb +3 -2
  202. data/lib/hexapdf/type.rb +1 -1
  203. data/lib/hexapdf/utils/bit_field.rb +1 -1
  204. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  205. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  206. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  207. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  208. data/lib/hexapdf/utils/object_hash.rb +1 -1
  209. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  210. data/lib/hexapdf/utils/sorted_tree_node.rb +4 -2
  211. data/lib/hexapdf/version.rb +2 -2
  212. data/lib/hexapdf/writer.rb +23 -8
  213. data/lib/hexapdf/xref_section.rb +1 -1
  214. data/lib/hexapdf.rb +1 -1
  215. data/test/hexapdf/content/graphic_object/test_geom2d.rb +1 -1
  216. data/test/hexapdf/document/test_destinations.rb +338 -0
  217. data/test/hexapdf/document/test_images.rb +1 -1
  218. data/test/hexapdf/document/test_layout.rb +264 -0
  219. data/test/hexapdf/document/test_pages.rb +9 -0
  220. data/test/hexapdf/document/test_signatures.rb +10 -3
  221. data/test/hexapdf/encryption/test_security_handler.rb +3 -3
  222. data/test/hexapdf/font/encoding/test_glyph_list.rb +4 -0
  223. data/test/hexapdf/layout/test_box.rb +53 -3
  224. data/test/hexapdf/layout/test_box_fitter.rb +62 -0
  225. data/test/hexapdf/layout/test_column_box.rb +159 -0
  226. data/test/hexapdf/layout/test_frame.rb +114 -39
  227. data/test/hexapdf/layout/test_image_box.rb +1 -1
  228. data/test/hexapdf/layout/test_list_box.rb +249 -0
  229. data/test/hexapdf/layout/test_text_box.rb +33 -2
  230. data/test/hexapdf/layout/test_text_fragment.rb +1 -1
  231. data/test/hexapdf/layout/test_text_layouter.rb +49 -17
  232. data/test/hexapdf/layout/test_width_from_polygon.rb +13 -0
  233. data/test/hexapdf/task/test_optimize.rb +17 -4
  234. data/test/hexapdf/test_composer.rb +35 -1
  235. data/test/hexapdf/test_dictionary_fields.rb +10 -10
  236. data/test/hexapdf/test_document.rb +33 -136
  237. data/test/hexapdf/test_filter.rb +1 -1
  238. data/test/hexapdf/test_parser.rb +1 -3
  239. data/test/hexapdf/test_revision.rb +14 -0
  240. data/test/hexapdf/test_revisions.rb +137 -29
  241. data/test/hexapdf/test_serializer.rb +1 -5
  242. data/test/hexapdf/test_writer.rb +99 -15
  243. data/test/hexapdf/type/acro_form/test_form.rb +2 -1
  244. data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
  245. data/test/hexapdf/type/test_catalog.rb +8 -0
  246. data/test/hexapdf/type/test_image.rb +45 -9
  247. data/test/hexapdf/type/test_names.rb +20 -0
  248. data/test/hexapdf/type/test_page_tree_node.rb +21 -1
  249. data/test/hexapdf/type/test_trailer.rb +3 -3
  250. data/test/hexapdf/type/test_xref_stream.rb +2 -1
  251. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  252. data/test/test_helper.rb +5 -1
  253. metadata +29 -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
@@ -53,6 +57,22 @@ describe HexaPDF::Layout::TextBox do
53
57
  assert_equal(20, box.height)
54
58
  end
55
59
 
60
+ it "uses the whole available width when aligning to the center or right" do
61
+ [:center, :right].each do |align|
62
+ box = create_box([@inline_box], style: {align: align})
63
+ assert(box.fit(100, 100, @frame))
64
+ assert_equal(100, box.width)
65
+ end
66
+ end
67
+
68
+ it "uses the whole available height when vertically aligning to the center or bottom" do
69
+ [:center, :bottom].each do |valign|
70
+ box = create_box([@inline_box], style: {valign: valign})
71
+ assert(box.fit(100, 100, @frame))
72
+ assert_equal(100, box.height)
73
+ end
74
+ end
75
+
56
76
  it "can't fit the text box if the set width is bigger than the available width" do
57
77
  box = create_box([@inline_box], width: 101)
58
78
  refute(box.fit(100, 100, @frame))
@@ -88,11 +108,22 @@ describe HexaPDF::Layout::TextBox do
88
108
  assert_equal([nil, box], box.split(100, 100, @frame))
89
109
  end
90
110
 
91
- it "splits the box if necessary" do
111
+ it "splits the box if necessary when using non-flowing text" do
92
112
  box = create_box([@inline_box] * 10)
93
113
  boxes = box.split(50, 10, @frame)
94
114
  assert_equal(2, boxes.length)
95
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])
96
127
  assert_equal(5, boxes[1].instance_variable_get(:@items).length)
97
128
  end
98
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
 
@@ -646,6 +671,13 @@ describe HexaPDF::Layout::TextLayouter do
646
671
  assert_equal(100 - 20 * 2 + 20, result.lines[0].y_offset)
647
672
  assert_equal(100, result.height)
648
673
  end
674
+
675
+ it "doesn't vertically align when layouting in variable-width mode" do
676
+ @style.valign = :bottom
677
+ result = @layouter.fit(@items, proc { 40 }, 100)
678
+ assert_equal(result.lines[0].y_max, result.lines[0].y_offset)
679
+ assert_equal(40, result.height)
680
+ end
649
681
  end
650
682
 
651
683
  it "post-processes lines for justification if needed" do
@@ -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]),
@@ -52,7 +52,7 @@ describe HexaPDF::Task::Optimize do
52
52
  describe "compact" do
53
53
  it "compacts the document" do
54
54
  @doc.task(:optimize, compact: true)
55
- assert_equal(1, @doc.revisions.size)
55
+ assert_equal(1, @doc.revisions.count)
56
56
  assert_equal(2, @doc.each(only_current: false).to_a.size)
57
57
  refute_equal(@obj2, @doc.object(@obj2))
58
58
  refute_equal(@obj3, @doc.object(@obj3))
@@ -81,8 +81,8 @@ describe HexaPDF::Task::Optimize do
81
81
  end
82
82
 
83
83
  it "compacts and deletes xref streams" do
84
- @doc.add({Type: :XRef}, revision: 0)
85
- @doc.add({Type: :XRef}, revision: 1)
84
+ @doc.revisions.all[0].add(@doc.wrap({Type: :XRef}, oid: @doc.revisions.next_oid))
85
+ @doc.revisions.all[1].add(@doc.wrap({Type: :XRef}, oid: @doc.revisions.next_oid))
86
86
  @doc.task(:optimize, compact: true, xref_streams: :delete)
87
87
  assert_no_xrefstms
88
88
  assert_default_deleted
@@ -108,7 +108,9 @@ describe HexaPDF::Task::Optimize do
108
108
  assert_objstms_generated
109
109
  assert_default_deleted
110
110
  assert_nil(@doc.object(objstm).value)
111
- assert_equal(2, @doc.revisions.current.find_all {|obj| obj.type == :ObjStm }.size)
111
+ objstms = @doc.revisions.current.find_all {|obj| obj.type == :ObjStm }
112
+ assert_equal(2, objstms.size)
113
+ assert_equal(400, objstms[0].instance_variable_get(:@objects).size)
112
114
  end
113
115
 
114
116
  it "deletes object and xref streams" do
@@ -158,6 +160,17 @@ describe HexaPDF::Task::Optimize do
158
160
  @doc.task(:optimize, compress_pages: true)
159
161
  assert_equal("10 10 m\nq\nQ\nBI\n/Name 5 ID\ndataEI\n", page.contents)
160
162
  end
163
+
164
+ it "uses parser.on_correctable_error to defer a decision regarding invalid operations" do
165
+ page = @doc.pages.add
166
+ page.contents = "10 20-30 m"
167
+ @doc.task(:optimize, compress_pages: true)
168
+ assert_equal("", page.contents)
169
+
170
+ @doc.config['parser.on_correctable_error'] = proc { true }
171
+ page.contents = "10 20-30 m"
172
+ assert_raises(HexaPDF::Error) { @doc.task(:optimize, compress_pages: true) }
173
+ end
161
174
  end
162
175
 
163
176
  describe "prune_page_resources" do
@@ -90,7 +90,7 @@ describe HexaPDF::Composer do
90
90
  it "creates a new style if it does not exist based on the base argument" do
91
91
  @composer.style(:base, font_size: 20)
92
92
  assert_equal(20, @composer.style(:newstyle, subscript: true).font_size)
93
- refute( @composer.style(:base).subscript)
93
+ refute(@composer.style(:base).subscript)
94
94
  assert_equal(10, @composer.style(:another_new, base: nil).font_size)
95
95
  assert(@composer.style(:yet_another_new, base: :newstyle).subscript)
96
96
  end
@@ -240,6 +240,24 @@ describe HexaPDF::Composer do
240
240
  assert(box.style.subscript)
241
241
  assert_same(@composer.document.images.add(image_path), box.image)
242
242
  end
243
+
244
+ it "allows using a form XObject" do
245
+ form = @composer.document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 10, 10]})
246
+ @composer.image(form, width: 10)
247
+ assert_equal(796, @composer.y)
248
+ assert_equal(36, @composer.x)
249
+ end
250
+ end
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
243
261
  end
244
262
 
245
263
  describe "draw_box" do
@@ -318,4 +336,20 @@ describe HexaPDF::Composer do
318
336
  end
319
337
  end
320
338
  end
339
+
340
+ describe "create_stamp" do
341
+ it "creates and returns a form XObject" do
342
+ stamp = @composer.create_stamp(10, 5)
343
+ assert_kind_of(HexaPDF::Type::Form, stamp)
344
+ assert_equal(10, stamp.width)
345
+ assert_equal(5, stamp.height)
346
+ end
347
+
348
+ it "allows using a block to draw on the canvas of the form XObject" do
349
+ stamp = @composer.create_stamp(10, 10) do |canvas|
350
+ canvas.line_width(5)
351
+ end
352
+ assert_equal("5 w\n", stamp.canvas.contents)
353
+ end
354
+ end
321
355
  end
@@ -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
@@ -134,7 +134,7 @@ describe HexaPDF::DictionaryFields do
134
134
  assert_equal(Encoding::UTF_8, str.encoding)
135
135
  end
136
136
 
137
- def configuration
137
+ def config
138
138
  HexaPDF::Configuration.with_defaults
139
139
  end
140
140
 
@@ -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