hexapdf 0.17.1 → 0.17.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (255) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -0
  3. data/LICENSE +29 -0
  4. data/README.md +129 -0
  5. data/Rakefile +109 -0
  6. data/agpl-3.0.txt +661 -0
  7. data/examples/001-hello_world.rb +16 -0
  8. data/examples/002-graphics.rb +275 -0
  9. data/examples/003-arcs.rb +50 -0
  10. data/examples/004-optimizing.rb +23 -0
  11. data/examples/005-merging.rb +27 -0
  12. data/examples/006-standard_pdf_fonts.rb +73 -0
  13. data/examples/007-truetype.rb +42 -0
  14. data/examples/008-show_char_bboxes.rb +55 -0
  15. data/examples/009-text_layouter_alignment.rb +47 -0
  16. data/examples/010-text_layouter_inline_boxes.rb +64 -0
  17. data/examples/011-text_layouter_line_wrapping.rb +57 -0
  18. data/examples/012-text_layouter_styling.rb +122 -0
  19. data/examples/013-text_layouter_shapes.rb +176 -0
  20. data/examples/014-text_in_polygon.rb +60 -0
  21. data/examples/015-boxes.rb +76 -0
  22. data/examples/016-frame_automatic_box_placement.rb +90 -0
  23. data/examples/017-frame_text_flow.rb +60 -0
  24. data/examples/018-composer.rb +44 -0
  25. data/examples/019-acro_form.rb +88 -0
  26. data/examples/emoji-smile.png +0 -0
  27. data/examples/emoji-wink.png +0 -0
  28. data/examples/machupicchu.jpg +0 -0
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -0
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +13 -0
  31. data/lib/hexapdf/version.rb +1 -1
  32. data/test/data/aes-test-vectors/CBCGFSbox-128-decrypt.data.gz +0 -0
  33. data/test/data/aes-test-vectors/CBCGFSbox-128-encrypt.data.gz +0 -0
  34. data/test/data/aes-test-vectors/CBCGFSbox-192-decrypt.data.gz +0 -0
  35. data/test/data/aes-test-vectors/CBCGFSbox-192-encrypt.data.gz +0 -0
  36. data/test/data/aes-test-vectors/CBCGFSbox-256-decrypt.data.gz +0 -0
  37. data/test/data/aes-test-vectors/CBCGFSbox-256-encrypt.data.gz +0 -0
  38. data/test/data/aes-test-vectors/CBCKeySbox-128-decrypt.data.gz +0 -0
  39. data/test/data/aes-test-vectors/CBCKeySbox-128-encrypt.data.gz +0 -0
  40. data/test/data/aes-test-vectors/CBCKeySbox-192-decrypt.data.gz +0 -0
  41. data/test/data/aes-test-vectors/CBCKeySbox-192-encrypt.data.gz +0 -0
  42. data/test/data/aes-test-vectors/CBCKeySbox-256-decrypt.data.gz +0 -0
  43. data/test/data/aes-test-vectors/CBCKeySbox-256-encrypt.data.gz +0 -0
  44. data/test/data/aes-test-vectors/CBCVarKey-128-decrypt.data.gz +0 -0
  45. data/test/data/aes-test-vectors/CBCVarKey-128-encrypt.data.gz +0 -0
  46. data/test/data/aes-test-vectors/CBCVarKey-192-decrypt.data.gz +0 -0
  47. data/test/data/aes-test-vectors/CBCVarKey-192-encrypt.data.gz +0 -0
  48. data/test/data/aes-test-vectors/CBCVarKey-256-decrypt.data.gz +0 -0
  49. data/test/data/aes-test-vectors/CBCVarKey-256-encrypt.data.gz +0 -0
  50. data/test/data/aes-test-vectors/CBCVarTxt-128-decrypt.data.gz +0 -0
  51. data/test/data/aes-test-vectors/CBCVarTxt-128-encrypt.data.gz +0 -0
  52. data/test/data/aes-test-vectors/CBCVarTxt-192-decrypt.data.gz +0 -0
  53. data/test/data/aes-test-vectors/CBCVarTxt-192-encrypt.data.gz +0 -0
  54. data/test/data/aes-test-vectors/CBCVarTxt-256-decrypt.data.gz +0 -0
  55. data/test/data/aes-test-vectors/CBCVarTxt-256-encrypt.data.gz +0 -0
  56. data/test/data/fonts/Ubuntu-Title.ttf +0 -0
  57. data/test/data/images/cmyk.jpg +0 -0
  58. data/test/data/images/fillbytes.jpg +0 -0
  59. data/test/data/images/gray.jpg +0 -0
  60. data/test/data/images/greyscale-1bit.png +0 -0
  61. data/test/data/images/greyscale-2bit.png +0 -0
  62. data/test/data/images/greyscale-4bit.png +0 -0
  63. data/test/data/images/greyscale-8bit.png +0 -0
  64. data/test/data/images/greyscale-alpha-8bit.png +0 -0
  65. data/test/data/images/greyscale-trns-8bit.png +0 -0
  66. data/test/data/images/greyscale-with-gamma1.0.png +0 -0
  67. data/test/data/images/greyscale-with-gamma1.5.png +0 -0
  68. data/test/data/images/indexed-1bit.png +0 -0
  69. data/test/data/images/indexed-2bit.png +0 -0
  70. data/test/data/images/indexed-4bit.png +0 -0
  71. data/test/data/images/indexed-8bit.png +0 -0
  72. data/test/data/images/indexed-alpha-4bit.png +0 -0
  73. data/test/data/images/indexed-alpha-8bit.png +0 -0
  74. data/test/data/images/rgb.jpg +0 -0
  75. data/test/data/images/truecolour-8bit.png +0 -0
  76. data/test/data/images/truecolour-alpha-8bit.png +0 -0
  77. data/test/data/images/truecolour-gama-chrm-8bit.png +0 -0
  78. data/test/data/images/truecolour-srgb-8bit.png +0 -0
  79. data/test/data/images/ycck.jpg +0 -0
  80. data/test/data/minimal.pdf +44 -0
  81. data/test/data/standard-security-handler/README +9 -0
  82. data/test/data/standard-security-handler/bothpwd-aes-128bit-V4.pdf +44 -0
  83. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5.pdf +0 -0
  84. data/test/data/standard-security-handler/bothpwd-arc4-128bit-V2.pdf +43 -0
  85. data/test/data/standard-security-handler/bothpwd-arc4-128bit-V4.pdf +43 -0
  86. data/test/data/standard-security-handler/bothpwd-arc4-40bit-V1.pdf +0 -0
  87. data/test/data/standard-security-handler/nopwd-aes-128bit-V4.pdf +43 -0
  88. data/test/data/standard-security-handler/nopwd-aes-256bit-V5.pdf +0 -0
  89. data/test/data/standard-security-handler/nopwd-arc4-128bit-V2.pdf +43 -0
  90. data/test/data/standard-security-handler/nopwd-arc4-128bit-V4.pdf +43 -0
  91. data/test/data/standard-security-handler/nopwd-arc4-40bit-V1.pdf +43 -0
  92. data/test/data/standard-security-handler/ownerpwd-aes-128bit-V4.pdf +0 -0
  93. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5.pdf +43 -0
  94. data/test/data/standard-security-handler/ownerpwd-arc4-128bit-V2.pdf +43 -0
  95. data/test/data/standard-security-handler/ownerpwd-arc4-128bit-V4.pdf +43 -0
  96. data/test/data/standard-security-handler/ownerpwd-arc4-40bit-V1.pdf +43 -0
  97. data/test/data/standard-security-handler/userpwd-aes-128bit-V4.pdf +43 -0
  98. data/test/data/standard-security-handler/userpwd-aes-256bit-V5.pdf +43 -0
  99. data/test/data/standard-security-handler/userpwd-arc4-128bit-V2.pdf +0 -0
  100. data/test/data/standard-security-handler/userpwd-arc4-128bit-V4.pdf +0 -0
  101. data/test/data/standard-security-handler/userpwd-arc4-40bit-V1.pdf +43 -0
  102. data/test/hexapdf/common_tokenizer_tests.rb +236 -0
  103. data/test/hexapdf/content/common.rb +39 -0
  104. data/test/hexapdf/content/graphic_object/test_arc.rb +102 -0
  105. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +90 -0
  106. data/test/hexapdf/content/graphic_object/test_geom2d.rb +79 -0
  107. data/test/hexapdf/content/graphic_object/test_solid_arc.rb +86 -0
  108. data/test/hexapdf/content/test_canvas.rb +1279 -0
  109. data/test/hexapdf/content/test_color_space.rb +176 -0
  110. data/test/hexapdf/content/test_graphics_state.rb +151 -0
  111. data/test/hexapdf/content/test_operator.rb +619 -0
  112. data/test/hexapdf/content/test_parser.rb +99 -0
  113. data/test/hexapdf/content/test_processor.rb +163 -0
  114. data/test/hexapdf/content/test_transformation_matrix.rb +64 -0
  115. data/test/hexapdf/document/test_files.rb +72 -0
  116. data/test/hexapdf/document/test_fonts.rb +60 -0
  117. data/test/hexapdf/document/test_images.rb +72 -0
  118. data/test/hexapdf/document/test_pages.rb +130 -0
  119. data/test/hexapdf/encryption/common.rb +87 -0
  120. data/test/hexapdf/encryption/test_aes.rb +129 -0
  121. data/test/hexapdf/encryption/test_arc4.rb +39 -0
  122. data/test/hexapdf/encryption/test_fast_aes.rb +17 -0
  123. data/test/hexapdf/encryption/test_fast_arc4.rb +12 -0
  124. data/test/hexapdf/encryption/test_identity.rb +21 -0
  125. data/test/hexapdf/encryption/test_ruby_aes.rb +23 -0
  126. data/test/hexapdf/encryption/test_ruby_arc4.rb +20 -0
  127. data/test/hexapdf/encryption/test_security_handler.rb +380 -0
  128. data/test/hexapdf/encryption/test_standard_security_handler.rb +322 -0
  129. data/test/hexapdf/filter/common.rb +53 -0
  130. data/test/hexapdf/filter/test_ascii85_decode.rb +59 -0
  131. data/test/hexapdf/filter/test_ascii_hex_decode.rb +38 -0
  132. data/test/hexapdf/filter/test_crypt.rb +21 -0
  133. data/test/hexapdf/filter/test_encryption.rb +24 -0
  134. data/test/hexapdf/filter/test_flate_decode.rb +44 -0
  135. data/test/hexapdf/filter/test_lzw_decode.rb +52 -0
  136. data/test/hexapdf/filter/test_predictor.rb +219 -0
  137. data/test/hexapdf/filter/test_run_length_decode.rb +32 -0
  138. data/test/hexapdf/font/cmap/test_parser.rb +102 -0
  139. data/test/hexapdf/font/cmap/test_writer.rb +66 -0
  140. data/test/hexapdf/font/encoding/test_base.rb +45 -0
  141. data/test/hexapdf/font/encoding/test_difference_encoding.rb +29 -0
  142. data/test/hexapdf/font/encoding/test_glyph_list.rb +59 -0
  143. data/test/hexapdf/font/encoding/test_zapf_dingbats_encoding.rb +16 -0
  144. data/test/hexapdf/font/test_cmap.rb +104 -0
  145. data/test/hexapdf/font/test_encoding.rb +27 -0
  146. data/test/hexapdf/font/test_invalid_glyph.rb +34 -0
  147. data/test/hexapdf/font/test_true_type_wrapper.rb +186 -0
  148. data/test/hexapdf/font/test_type1_wrapper.rb +107 -0
  149. data/test/hexapdf/font/true_type/common.rb +17 -0
  150. data/test/hexapdf/font/true_type/table/common.rb +27 -0
  151. data/test/hexapdf/font/true_type/table/test_cmap.rb +47 -0
  152. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +141 -0
  153. data/test/hexapdf/font/true_type/table/test_directory.rb +30 -0
  154. data/test/hexapdf/font/true_type/table/test_glyf.rb +58 -0
  155. data/test/hexapdf/font/true_type/table/test_head.rb +56 -0
  156. data/test/hexapdf/font/true_type/table/test_hhea.rb +26 -0
  157. data/test/hexapdf/font/true_type/table/test_hmtx.rb +30 -0
  158. data/test/hexapdf/font/true_type/table/test_kern.rb +61 -0
  159. data/test/hexapdf/font/true_type/table/test_loca.rb +33 -0
  160. data/test/hexapdf/font/true_type/table/test_maxp.rb +50 -0
  161. data/test/hexapdf/font/true_type/table/test_name.rb +76 -0
  162. data/test/hexapdf/font/true_type/table/test_os2.rb +55 -0
  163. data/test/hexapdf/font/true_type/table/test_post.rb +78 -0
  164. data/test/hexapdf/font/true_type/test_builder.rb +42 -0
  165. data/test/hexapdf/font/true_type/test_font.rb +116 -0
  166. data/test/hexapdf/font/true_type/test_optimizer.rb +26 -0
  167. data/test/hexapdf/font/true_type/test_subsetter.rb +73 -0
  168. data/test/hexapdf/font/true_type/test_table.rb +48 -0
  169. data/test/hexapdf/font/type1/common.rb +6 -0
  170. data/test/hexapdf/font/type1/test_afm_parser.rb +65 -0
  171. data/test/hexapdf/font/type1/test_font.rb +104 -0
  172. data/test/hexapdf/font/type1/test_font_metrics.rb +22 -0
  173. data/test/hexapdf/font/type1/test_pfb_parser.rb +37 -0
  174. data/test/hexapdf/font_loader/test_from_configuration.rb +43 -0
  175. data/test/hexapdf/font_loader/test_from_file.rb +36 -0
  176. data/test/hexapdf/font_loader/test_standard14.rb +33 -0
  177. data/test/hexapdf/image_loader/test_jpeg.rb +93 -0
  178. data/test/hexapdf/image_loader/test_pdf.rb +47 -0
  179. data/test/hexapdf/image_loader/test_png.rb +259 -0
  180. data/test/hexapdf/layout/test_box.rb +154 -0
  181. data/test/hexapdf/layout/test_frame.rb +350 -0
  182. data/test/hexapdf/layout/test_image_box.rb +73 -0
  183. data/test/hexapdf/layout/test_inline_box.rb +71 -0
  184. data/test/hexapdf/layout/test_line.rb +206 -0
  185. data/test/hexapdf/layout/test_style.rb +790 -0
  186. data/test/hexapdf/layout/test_text_box.rb +140 -0
  187. data/test/hexapdf/layout/test_text_fragment.rb +375 -0
  188. data/test/hexapdf/layout/test_text_layouter.rb +758 -0
  189. data/test/hexapdf/layout/test_text_shaper.rb +62 -0
  190. data/test/hexapdf/layout/test_width_from_polygon.rb +109 -0
  191. data/test/hexapdf/task/test_dereference.rb +51 -0
  192. data/test/hexapdf/task/test_optimize.rb +162 -0
  193. data/test/hexapdf/test_composer.rb +258 -0
  194. data/test/hexapdf/test_configuration.rb +93 -0
  195. data/test/hexapdf/test_data_dir.rb +32 -0
  196. data/test/hexapdf/test_dictionary.rb +340 -0
  197. data/test/hexapdf/test_dictionary_fields.rb +269 -0
  198. data/test/hexapdf/test_document.rb +641 -0
  199. data/test/hexapdf/test_filter.rb +100 -0
  200. data/test/hexapdf/test_importer.rb +106 -0
  201. data/test/hexapdf/test_object.rb +258 -0
  202. data/test/hexapdf/test_parser.rb +645 -0
  203. data/test/hexapdf/test_pdf_array.rb +169 -0
  204. data/test/hexapdf/test_rectangle.rb +73 -0
  205. data/test/hexapdf/test_reference.rb +50 -0
  206. data/test/hexapdf/test_revision.rb +188 -0
  207. data/test/hexapdf/test_revisions.rb +196 -0
  208. data/test/hexapdf/test_serializer.rb +195 -0
  209. data/test/hexapdf/test_stream.rb +274 -0
  210. data/test/hexapdf/test_tokenizer.rb +80 -0
  211. data/test/hexapdf/test_type.rb +18 -0
  212. data/test/hexapdf/test_writer.rb +140 -0
  213. data/test/hexapdf/test_xref_section.rb +61 -0
  214. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +795 -0
  215. data/test/hexapdf/type/acro_form/test_button_field.rb +308 -0
  216. data/test/hexapdf/type/acro_form/test_choice_field.rb +220 -0
  217. data/test/hexapdf/type/acro_form/test_field.rb +259 -0
  218. data/test/hexapdf/type/acro_form/test_form.rb +357 -0
  219. data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
  220. data/test/hexapdf/type/acro_form/test_text_field.rb +201 -0
  221. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +88 -0
  222. data/test/hexapdf/type/actions/test_launch.rb +24 -0
  223. data/test/hexapdf/type/actions/test_uri.rb +23 -0
  224. data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
  225. data/test/hexapdf/type/annotations/test_text.rb +34 -0
  226. data/test/hexapdf/type/annotations/test_widget.rb +225 -0
  227. data/test/hexapdf/type/test_annotation.rb +97 -0
  228. data/test/hexapdf/type/test_catalog.rb +48 -0
  229. data/test/hexapdf/type/test_cid_font.rb +61 -0
  230. data/test/hexapdf/type/test_file_specification.rb +141 -0
  231. data/test/hexapdf/type/test_font.rb +67 -0
  232. data/test/hexapdf/type/test_font_descriptor.rb +61 -0
  233. data/test/hexapdf/type/test_font_simple.rb +176 -0
  234. data/test/hexapdf/type/test_font_true_type.rb +31 -0
  235. data/test/hexapdf/type/test_font_type0.rb +120 -0
  236. data/test/hexapdf/type/test_font_type1.rb +142 -0
  237. data/test/hexapdf/type/test_font_type3.rb +26 -0
  238. data/test/hexapdf/type/test_form.rb +120 -0
  239. data/test/hexapdf/type/test_image.rb +261 -0
  240. data/test/hexapdf/type/test_info.rb +9 -0
  241. data/test/hexapdf/type/test_object_stream.rb +117 -0
  242. data/test/hexapdf/type/test_page.rb +598 -0
  243. data/test/hexapdf/type/test_page_tree_node.rb +315 -0
  244. data/test/hexapdf/type/test_resources.rb +209 -0
  245. data/test/hexapdf/type/test_trailer.rb +116 -0
  246. data/test/hexapdf/type/test_xref_stream.rb +143 -0
  247. data/test/hexapdf/utils/test_bit_field.rb +63 -0
  248. data/test/hexapdf/utils/test_bit_stream.rb +69 -0
  249. data/test/hexapdf/utils/test_graphics_helpers.rb +37 -0
  250. data/test/hexapdf/utils/test_lru_cache.rb +22 -0
  251. data/test/hexapdf/utils/test_object_hash.rb +120 -0
  252. data/test/hexapdf/utils/test_pdf_doc_encoding.rb +18 -0
  253. data/test/hexapdf/utils/test_sorted_tree_node.rb +239 -0
  254. data/test/test_helper.rb +58 -0
  255. metadata +263 -3
@@ -0,0 +1,758 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/layout'
5
+ require 'hexapdf/document'
6
+ require_relative "../content/common"
7
+
8
+ module TestTextLayouterHelpers
9
+ def boxes(*dims)
10
+ dims.map do |width, height|
11
+ box = HexaPDF::Layout::InlineBox.create(width: width, height: height || 0) {}
12
+ HexaPDF::Layout::TextLayouter::Box.new(box)
13
+ end
14
+ end
15
+
16
+ def glue(width)
17
+ HexaPDF::Layout::TextLayouter::Glue.new(HexaPDF::Layout::InlineBox.create(width: width) {})
18
+ end
19
+
20
+ def penalty(penalty, item = nil)
21
+ if item
22
+ HexaPDF::Layout::TextLayouter::Penalty.new(penalty, item.width, item: item)
23
+ else
24
+ HexaPDF::Layout::TextLayouter::Penalty.new(penalty)
25
+ end
26
+ end
27
+
28
+ def assert_box(obj, item)
29
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Box, obj)
30
+ if obj.item.kind_of?(HexaPDF::Layout::InlineBox)
31
+ assert_same(item, obj.item)
32
+ else
33
+ assert_same(item.style, obj.item.style)
34
+ assert_equal(item.items, obj.item.items)
35
+ end
36
+ end
37
+
38
+ def assert_glue(obj, fragment)
39
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Glue, obj)
40
+ assert_same(fragment.style, obj.item.style)
41
+ end
42
+
43
+ def assert_penalty(obj, penalty, item = nil)
44
+ assert_kind_of(HexaPDF::Layout::TextLayouter::Penalty, obj)
45
+ assert_equal(penalty, obj.penalty)
46
+ if item
47
+ assert_same(item.style, obj.item.style)
48
+ assert_equal(item.items, obj.item.items)
49
+ end
50
+ end
51
+
52
+ def assert_line_wrapping(result, widths)
53
+ rest, lines = *result
54
+ assert(rest.empty?)
55
+ assert_equal(widths.length, lines.count)
56
+ widths.each_with_index {|width, index| assert_equal(width, lines[index].width) }
57
+ end
58
+ end
59
+
60
+ describe HexaPDF::Layout::TextLayouter::SimpleTextSegmentation do
61
+ include TestTextLayouterHelpers
62
+
63
+ before do
64
+ @doc = HexaPDF::Document.new
65
+ @font = @doc.fonts.add("Times")
66
+ @obj = HexaPDF::Layout::TextLayouter::SimpleTextSegmentation
67
+ end
68
+
69
+ def setup_fragment(text, style = nil)
70
+ if style
71
+ HexaPDF::Layout::TextFragment.create(text, style)
72
+ else
73
+ HexaPDF::Layout::TextFragment.create(text, font: @font)
74
+ end
75
+ end
76
+
77
+ it "handles InlineBox objects" do
78
+ input = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {}
79
+ result = @obj.call([input, input])
80
+ assert_equal(2, result.size)
81
+ assert_box(result[0], input)
82
+ assert_box(result[1], input)
83
+ end
84
+
85
+ it "handles plain text" do
86
+ frag = setup_fragment("Testtext")
87
+ result = @obj.call([frag])
88
+ assert_equal(1, result.size)
89
+ assert_box(result[0], frag)
90
+ end
91
+
92
+ it "inserts a glue in places where spaces are" do
93
+ frag = setup_fragment("This is a test")
94
+ space = setup_fragment(" ", frag.style)
95
+
96
+ result = @obj.call([frag])
97
+ assert_equal(7, result.size)
98
+ assert_glue(result[1], space)
99
+ assert_glue(result[3], space)
100
+ assert_glue(result[5], space)
101
+ end
102
+
103
+ it "inserts a glue representing 8 spaces when a tab is encountered" do
104
+ frag = setup_fragment("This\ttest")
105
+ tab = setup_fragment(" " * 8, frag.style)
106
+
107
+ result = @obj.call([frag])
108
+ assert_equal(3, result.size)
109
+ assert_glue(result[1], tab)
110
+ end
111
+
112
+ it "insert a mandatory break when an Unicode line boundary characters is encountered" do
113
+ frag = setup_fragment("A\rB\r\nC\nD\vE\fF\u{85}G\u{2029}H\u{2028}I\r")
114
+ frag.items << 5 << frag.items[-2]
115
+
116
+ result = @obj.call([frag])
117
+ assert_equal(20, result.size)
118
+ [1, 3, 5, 7, 9, 11, 13, 17, 19].each do |index|
119
+ assert_penalty(result[index],
120
+ HexaPDF::Layout::TextLayouter::Penalty::PARAGRAPH_BREAK)
121
+ assert_equal([], result[index].item.items)
122
+ assert(result[index].item.items.frozen?)
123
+ assert_same(frag.style, result[index].item.style)
124
+ end
125
+ assert_penalty(result[15], HexaPDF::Layout::TextLayouter::Penalty::LINE_BREAK)
126
+ assert_equal([], result[15].item.items)
127
+ assert(result[15].item.items.frozen?)
128
+ assert_same(frag.style, result[15].item.style)
129
+ end
130
+
131
+ it "insert a standard penalty after a hyphen" do
132
+ frag = setup_fragment("hy-phen-a-tion - cool!")
133
+
134
+ result = @obj.call([frag])
135
+ assert_equal(12, result.size)
136
+ [1, 3, 5, 9].each do |index|
137
+ assert_penalty(result[index], HexaPDF::Layout::TextLayouter::Penalty::Standard.penalty)
138
+ end
139
+ end
140
+
141
+ it "insert a neutral penalty in places where zero-width-spaces are" do
142
+ frag = setup_fragment("zero\u{200B}width\u{200B}space")
143
+
144
+ result = @obj.call([frag])
145
+ assert_equal(5, result.size)
146
+ assert_penalty(result[1], 0)
147
+ assert_penalty(result[3], 0)
148
+ end
149
+
150
+ it "insert a special penalty for soft-hyphens" do
151
+ frag = setup_fragment("soft\u{00AD}hyphened")
152
+ hyphen = setup_fragment("-", frag.style)
153
+
154
+ result = @obj.call([frag])
155
+ assert_equal(3, result.size)
156
+ assert_penalty(result[1], HexaPDF::Layout::TextLayouter::Penalty::Standard.penalty, hyphen)
157
+ end
158
+
159
+ it "insert a prohibited break penalty for non-breaking spaces" do
160
+ frag = setup_fragment("soft\u{00A0}hyphened")
161
+ space = setup_fragment(" ", frag.style)
162
+
163
+ result = @obj.call([frag])
164
+ assert_equal(3, result.size)
165
+ assert_penalty(result[1], HexaPDF::Layout::TextLayouter::Penalty::ProhibitedBreak.penalty, space)
166
+ end
167
+ end
168
+
169
+ # Common tests for fixed and variable width line wrapping. The including class needs to define a
170
+ # #call(items, width = 100) method with a default with of 100. The optional block is called after a
171
+ # line has been yielded by the line wrapping algorithm.
172
+ module CommonLineWrappingTests
173
+ extend Minitest::Spec::DSL
174
+
175
+ include TestTextLayouterHelpers
176
+
177
+ it "breaks before a box if it doesn't fit onto the line anymore" do
178
+ rest, lines = call(boxes(25, 50, 25, 10))
179
+ assert_line_wrapping([rest, lines], [100, 10])
180
+ lines.each {|line| line.items.each {|item| assert_kind_of(HexaPDF::Layout::InlineBox, item) } }
181
+ end
182
+
183
+ it "breaks at a glue and ignores it if it doesn't fit onto the line anymore" do
184
+ result = call(boxes(90) + [glue(20)] + boxes(20))
185
+ assert_line_wrapping(result, [90, 20])
186
+ end
187
+
188
+ it "handles spaces at the start of a line" do
189
+ rest, lines = call([glue(15)] + boxes(25, 50))
190
+ assert_line_wrapping([rest, lines], [75])
191
+ assert_equal(25, lines[0].items[0].width)
192
+ end
193
+
194
+ it "handles spaces at the end of a line" do
195
+ rest, lines = call(boxes(20, 50) + [glue(10), glue(10)] + boxes(20))
196
+ assert_line_wrapping([rest, lines], [70, 20])
197
+ assert_equal(50, lines[0].items[-1].width)
198
+ end
199
+
200
+ it "handles spaces at the end of a line before a mandatory break" do
201
+ rest, lines = call(boxes(20, 50) + [glue(10), penalty(-5000)] + boxes(20))
202
+ assert_line_wrapping([rest, lines], [70, 20])
203
+ assert_equal(50, lines[0].items[-1].width)
204
+ end
205
+
206
+ it "handles multiple glue items after another" do
207
+ result = call(boxes(20) + [glue(20), glue(20)] + boxes(20, 50, 20))
208
+ assert_line_wrapping(result, [80, 70])
209
+ end
210
+
211
+ it "handles mandatory line breaks" do
212
+ rest, lines = call(boxes(20) + [penalty(-5000)] + boxes(20))
213
+ assert_line_wrapping([rest, lines], [20, 20])
214
+ assert(lines[0].ignore_justification?)
215
+ end
216
+
217
+ it "handles breaking at penalties with zero width" do
218
+ result = call(boxes(80) + [penalty(0)] + boxes(10) + [penalty(0)] + boxes(20))
219
+ assert_line_wrapping(result, [90, 20])
220
+ end
221
+
222
+ it "handles breaking at penalties with non-zero width if they fit on the line" do
223
+ pitem = penalty(0, boxes(20).first)
224
+ rest, lines = call(boxes(20) + [pitem] + boxes(50) + [glue(10), pitem] + boxes(30))
225
+ assert_line_wrapping([rest, lines], [100, 30])
226
+ assert_same(pitem.item, lines[0].items[-1])
227
+ end
228
+
229
+ it "handles breaking at penalties with non-zero width that fit on the line and are followed by 1+ penalties" do
230
+ pitem = penalty(0, boxes(20).first)
231
+ result = call(boxes(80) + [pitem, penalty(0), penalty(0)] + boxes(30))
232
+ assert_line_wrapping(result, [100, 30])
233
+ end
234
+
235
+ it "handles penalties with non-zero width if they don't fit on the line" do
236
+ item = boxes(20).first
237
+ result = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(0, item)] + boxes(30))
238
+ assert_line_wrapping(result, [70, 40])
239
+ end
240
+
241
+ it "handles breaking at penalties with non-zero width surrounded by glue" do
242
+ item = boxes(20).first
243
+ result = call(boxes(70) + [glue(10)] + [penalty(0, item)] + [glue(30)] + boxes(30))
244
+ assert_line_wrapping(result, [100, 30])
245
+ end
246
+
247
+ it "handles prohibited breakpoint penalties with zero width" do
248
+ result = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000)] + boxes(30))
249
+ assert_line_wrapping(result, [70, 40])
250
+ end
251
+
252
+ it "handles prohibited breakpoint penalties with non-zero width" do
253
+ item = boxes(20).first
254
+ result = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
255
+ assert_line_wrapping(result, [70, 60])
256
+ end
257
+
258
+ it "stops when nil is returned by the block: last item is a box" do
259
+ done = false
260
+ rest, lines = call(boxes(20, 20, 20), 20) { done ? nil : done = true }
261
+ assert_equal(2, rest.count)
262
+ assert_equal(2, lines.count)
263
+ end
264
+
265
+ it "stops when nil is returned by the block: last item is a glue" do
266
+ done = false
267
+ items = boxes(20, 15, 20).insert(-2, glue(10))
268
+ rest, = call(items, 20) { done ? nil : done = true }
269
+ assert_equal(3, rest.count)
270
+ assert_equal(15, rest[0].width)
271
+ end
272
+
273
+ it "stops when nil is returned by the block: last item is a mandatory break penalty" do
274
+ items = boxes(20, 20).insert(-2, penalty(-5000))
275
+ rest, = call(items, 20) { nil }
276
+ assert_equal(3, rest.count)
277
+ end
278
+
279
+ it "stops when nil is returned by the block: works for the last line" do
280
+ done = false
281
+ rest, lines = call(boxes(20, 20), 20) { done ? nil : done = true }
282
+ assert_equal(1, rest.count)
283
+ assert_equal(2, lines.count)
284
+ end
285
+
286
+ end
287
+
288
+ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
289
+ before do
290
+ @obj = HexaPDF::Layout::TextLayouter::SimpleLineWrapping
291
+ end
292
+
293
+ describe "fixed width wrapping" do
294
+ include CommonLineWrappingTests
295
+
296
+ def call(items, width = 100, &block)
297
+ lines = []
298
+ block ||= proc { true }
299
+ rest = @obj.call(items, proc { width }) {|line, item| lines << line; block.call(line, item) }
300
+ [rest, lines]
301
+ end
302
+ end
303
+
304
+ describe "variable width wrapping" do
305
+ include CommonLineWrappingTests
306
+
307
+ def call(items, width = 100, &block)
308
+ lines = []
309
+ block ||= proc { true }
310
+ rest = @obj.call(items, proc {|_| width }) {|line, i| lines << line; block.call(line, i) }
311
+ [rest, lines]
312
+ end
313
+
314
+ it "handles changing widths" do
315
+ height = 0
316
+ width_block = lambda do |line|
317
+ case height + line.height
318
+ when 0..10 then 60
319
+ when 11..20 then 40
320
+ when 21..30 then 20
321
+ else 60
322
+ end
323
+ end
324
+ lines = []
325
+ rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block) do |line|
326
+ height += line.height
327
+ lines << line
328
+ true
329
+ end
330
+ assert(rest.empty?)
331
+ assert_equal(3, lines.size)
332
+ assert_equal(30, lines[0].width)
333
+ assert_equal(20, lines[1].width)
334
+ assert_equal(40, lines[2].width)
335
+ end
336
+
337
+ it "handles changing widths when breaking on a penalty" do
338
+ height = 0
339
+ width_block = lambda do |line|
340
+ case height + line.height
341
+ when 0..10 then 80
342
+ else 50
343
+ end
344
+ end
345
+ lines = []
346
+ item = HexaPDF::Layout::InlineBox.create(width: 20, height: 10) {}
347
+ items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
348
+ rest = @obj.call(items, width_block) do |line|
349
+ height += line.height
350
+ lines << line
351
+ true
352
+ end
353
+ assert(rest.empty?)
354
+ assert_equal(2, lines.size)
355
+ assert_equal(40, lines[0].width)
356
+ assert_equal(40, lines[1].width)
357
+ assert_equal(25, height)
358
+ end
359
+ end
360
+ end
361
+
362
+ describe HexaPDF::Layout::TextLayouter do
363
+ include TestTextLayouterHelpers
364
+
365
+ before do
366
+ @doc = HexaPDF::Document.new
367
+ @font = @doc.fonts.add("Times")
368
+ @style = HexaPDF::Layout::Style.new(font: @font)
369
+ end
370
+
371
+ describe "initialize" do
372
+ it "can use a Style object" do
373
+ style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
374
+ layouter = HexaPDF::Layout::TextLayouter.new(style)
375
+ assert_equal(20, layouter.style.font_size)
376
+ end
377
+
378
+ it "can use a style options" do
379
+ layouter = HexaPDF::Layout::TextLayouter.new(font: @font, font_size: 20)
380
+ assert_equal(20, layouter.style.font_size)
381
+ end
382
+ end
383
+
384
+ describe "fit" do
385
+ before do
386
+ @layouter = HexaPDF::Layout::TextLayouter.new(@style)
387
+ end
388
+
389
+ it "does nothing if there are no items" do
390
+ result = @layouter.fit([], 100, 100)
391
+ assert_equal(:success, result.status)
392
+ assert_equal(0, result.height)
393
+ end
394
+
395
+ it "handles text indentation" do
396
+ items = boxes([20, 20], [20, 20], [20, 20]) +
397
+ [penalty(HexaPDF::Layout::TextLayouter::Penalty::PARAGRAPH_BREAK)] +
398
+ boxes([40, 20]) + [glue(20)] +
399
+ boxes(*([[20, 20]] * 4)) + [penalty(HexaPDF::Layout::TextLayouter::Penalty::LINE_BREAK)] +
400
+ boxes(*([[20, 20]] * 4))
401
+ @style.text_indent = 20
402
+
403
+ [60, proc { 60 }].each do |width|
404
+ result = @layouter.fit(items, width, 200)
405
+ assert_equal([40, 20, 40, 60, 20, 60, 20], result.lines.map(&:width))
406
+ assert_equal([20, 0, 20, 0, 0, 0, 0], result.lines.map(&:x_offset))
407
+ assert(result.remaining_items.empty?)
408
+ assert_equal(:success, result.status)
409
+ end
410
+ end
411
+
412
+ it "fits using a limited height" do
413
+ result = @layouter.fit(boxes(*([[20, 20]] * 100)), 20, 100)
414
+ assert_equal(95, result.remaining_items.count)
415
+ assert_equal(:height, result.status)
416
+ assert_equal(100, result.height)
417
+ end
418
+
419
+ it "takes line spacing into account when calculating the height" do
420
+ @style.line_spacing = :double
421
+ result = @layouter.fit(boxes(*([[20, 20]] * 5)), 20, 200)
422
+ assert(result.remaining_items.empty?)
423
+ assert_equal(:success, result.status)
424
+ assert_equal(20 * (5 + 4), result.height)
425
+ end
426
+
427
+ it "takes line spacing into account with variable width" do
428
+ @style.line_spacing = :double
429
+ width_block = lambda {|l, h| l + h <= 90 ? 40 : 20 }
430
+ result = @layouter.fit(boxes(*([[20, 20]] * 6)), width_block, 170)
431
+ assert(result.remaining_items.empty?)
432
+ assert_equal(:success, result.status)
433
+ assert_line_wrapping([[], result.lines], [40, 40, 20, 20])
434
+ assert_equal(140, result.height)
435
+ end
436
+
437
+ it "handles empty lines if the break penalties don't have an item" do
438
+ items = boxes([20, 20]) + [penalty(-5000)] + boxes([30, 20]) + [penalty(-5000)] * 2 +
439
+ boxes([20, 20]) + [penalty(-5000)] * 2
440
+ result = @layouter.fit(items, 30, 100)
441
+ assert(result.remaining_items.empty?)
442
+ assert_equal(:success, result.status)
443
+ assert_equal(5, result.lines.count)
444
+ assert_equal(20 + 20 + 9 + 20 + 9, result.height)
445
+ end
446
+
447
+ it "handles line breaks in combination with multiple parts per line" do
448
+ items = boxes([20, 20]) + [penalty(-5000)] +
449
+ boxes([20, 20], [20, 20], [20, 20]) + [penalty(-5000)] +
450
+ [penalty(-5000)] +
451
+ boxes([20, 20])
452
+ result = @layouter.fit(items, [0, 40, 0, 40, 0, 40], 100)
453
+ assert_equal(:success, result.status)
454
+ assert_equal([20, 40, 20, 0, 20], result.lines.map(&:width))
455
+ assert_equal([20, 20, 0, 6.83, 22.17], result.lines.map(&:y_offset))
456
+ end
457
+
458
+ describe "fixed width and too wide item" do
459
+ it "single part per line" do
460
+ result = @layouter.fit(boxes([20, 20], [50, 20]), 30, 100)
461
+ assert_equal(1, result.remaining_items.count)
462
+ assert_equal(:box_too_wide, result.status)
463
+ assert_equal(20, result.height)
464
+ end
465
+
466
+ it "multiple parts per line, one fits" do
467
+ result = @layouter.fit(boxes([20, 20], [40, 20], [40, 20], [10, 20]),
468
+ [0, 30, 0, 50, 0, 30, 0, 30], 100)
469
+ assert_equal(0, result.remaining_items.count)
470
+ assert_equal([20, 40, 0, 0, 0, 50], result.lines.map(&:width))
471
+ assert_equal([0, 30, 80, 110, 0, 30], result.lines.map(&:x_offset))
472
+ assert_equal([20, 0, 0, 0, 20, 0], result.lines.map(&:y_offset))
473
+ assert_equal(:success, result.status)
474
+ assert_equal(40, result.height)
475
+ end
476
+
477
+ it "multiple parts per line, none fits" do
478
+ result = @layouter.fit(boxes([20, 20], [50, 20]), [0, 30, 0, 30, 0, 30], 100)
479
+ assert_equal(1, result.remaining_items.count)
480
+ assert_equal(:box_too_wide, result.status)
481
+ assert_equal(20, result.height)
482
+ end
483
+ end
484
+
485
+ describe "variable width" do
486
+ it "single part per line, searches for vertical offset if the first item is too wide" do
487
+ width_block = lambda do |height, _|
488
+ case height
489
+ when 0..20 then 10
490
+ else 40
491
+ end
492
+ end
493
+ result = @layouter.fit(boxes([20, 18]), width_block, 100)
494
+ assert(result.remaining_items.empty?)
495
+ assert_equal(:success, result.status)
496
+ assert_equal(1, result.lines.count)
497
+ assert_equal(42, result.lines[0].y_offset)
498
+ assert_equal(42, result.height)
499
+ end
500
+
501
+ it "single part per line, searches for vertical offset if an item is too wide" do
502
+ width_block = lambda do |height, line_height|
503
+ if (40..60).cover?(height) || (40..60).cover?(height + line_height)
504
+ 10
505
+ else
506
+ 40
507
+ end
508
+ end
509
+ result = @layouter.fit(boxes(*([[20, 18]] * 7)), width_block, 100)
510
+ assert_equal(1, result.remaining_items.count)
511
+ assert_equal(:height, result.status)
512
+ assert_equal(3, result.lines.count)
513
+ assert_equal(18, result.lines[0].y_offset)
514
+ assert_equal(18, result.lines[1].y_offset)
515
+ assert_equal(48, result.lines[2].y_offset)
516
+ assert_equal(84, result.height)
517
+ end
518
+
519
+ it "multiple parts per line, reset line because of line height change during first part" do
520
+ width_block = lambda do |height, line_height|
521
+ if height == 0 && line_height <= 10
522
+ [0, 40, 0, 40]
523
+ elsif height == 0 && line_height > 10
524
+ [0, 30, 0, 40]
525
+ else
526
+ [0, 60]
527
+ end
528
+ end
529
+ result = @layouter.fit(boxes([20, 10], [20, 10], [20, 18], [20, 10]), width_block, 100)
530
+ assert_equal(:success, result.status)
531
+ assert_equal(0, result.remaining_items.count)
532
+ assert_equal(3, result.lines.count)
533
+ assert_equal([20, 40, 20], result.lines.map(&:width))
534
+ assert_equal([18, 0, 10], result.lines.map(&:y_offset))
535
+ assert_equal(28, result.height)
536
+ end
537
+
538
+ it "multiple parts per line, reset line because of line height change after first part" do
539
+ width_block = lambda do |height, line_height|
540
+ if height == 0 && line_height <= 10
541
+ [0, 40, 0, 40]
542
+ elsif height == 0 && line_height > 10
543
+ [0, 30, 0, 40]
544
+ else
545
+ [0, 60]
546
+ end
547
+ end
548
+ result = @layouter.fit(boxes([20, 10], [15, 10], [10, 10], [12, 18], [20, 10]),
549
+ width_block, 100)
550
+ assert_equal(:success, result.status)
551
+ assert_equal(0, result.remaining_items.count)
552
+ assert_equal(3, result.lines.count)
553
+ assert_equal([20, 37, 20], result.lines.map(&:width))
554
+ assert_equal([18, 0, 10], result.lines.map(&:y_offset))
555
+ assert_equal(28, result.height)
556
+ end
557
+ end
558
+
559
+ describe "breaks a text fragment into parts if it is wider than the available width" do
560
+ before do
561
+ @str = " This is averylongstring"
562
+ @frag = HexaPDF::Layout::TextFragment.create(@str, font: @font)
563
+ end
564
+
565
+ it "works with fixed width" do
566
+ result = @layouter.fit([@frag], 20, 100)
567
+ assert(result.remaining_items.empty?)
568
+ 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)
571
+
572
+ result = @layouter.fit([@frag], 1, 100)
573
+ assert_equal(8, result.remaining_items.count)
574
+ assert_equal(:box_too_wide, result.status)
575
+ end
576
+
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)
587
+ assert_equal(:height, result.status)
588
+ assert_equal([26.95, 9.44, 7.77], result.lines.map {|l| l.width.round(3) })
589
+ end
590
+ end
591
+
592
+ describe "horizontal alignment" do
593
+ before do
594
+ @items = boxes(*[[20, 20]] * 4) + [glue(10), penalty(-5000, boxes(0).first.item)]
595
+ end
596
+
597
+ it "aligns the contents to the left" do
598
+ @style.align = :left
599
+ result = @layouter.fit(@items, 100, 100)
600
+ assert_equal(0, result.lines[0].x_offset)
601
+ assert_equal(80, result.lines[0].width)
602
+ result = @layouter.fit(@items, proc { 100 }, 100)
603
+ assert_equal(0, result.lines[0].x_offset)
604
+ assert_equal(80, result.lines[0].width)
605
+ end
606
+
607
+ it "aligns the contents to the center" do
608
+ @style.align = :center
609
+ result = @layouter.fit(@items, 100, 100)
610
+ assert_equal(10, result.lines[0].x_offset)
611
+ result = @layouter.fit(@items, proc { 100 }, 100)
612
+ assert_equal(10, result.lines[0].x_offset)
613
+ end
614
+
615
+ it "aligns the contents to the right" do
616
+ @style.align = :right
617
+ result = @layouter.fit(@items, 100, 100)
618
+ assert_equal(20, result.lines[0].x_offset)
619
+ result = @layouter.fit(@items, proc { 100 }, 100)
620
+ assert_equal(20, result.lines[0].x_offset)
621
+ end
622
+ end
623
+
624
+ describe "vertical alignment" do
625
+ before do
626
+ @items = boxes(*[[20, 20]] * 4)
627
+ end
628
+
629
+ it "aligns the contents to the top" do
630
+ @style.valign = :top
631
+ result = @layouter.fit(@items, 40, 100)
632
+ assert_equal(result.lines[0].y_max, result.lines[0].y_offset)
633
+ assert_equal(40, result.height)
634
+ end
635
+
636
+ it "aligns the contents to the center" do
637
+ @style.valign = :center
638
+ result = @layouter.fit(@items, 40, 100)
639
+ assert_equal((100 - 40) / 2 + 20, result.lines[0].y_offset)
640
+ assert_equal(70, result.height)
641
+ end
642
+
643
+ it "aligns the contents to the bottom" do
644
+ @style.valign = :bottom
645
+ result = @layouter.fit(@items, 40, 100)
646
+ assert_equal(100 - 20 * 2 + 20, result.lines[0].y_offset)
647
+ assert_equal(100, result.height)
648
+ end
649
+ end
650
+
651
+ it "post-processes lines for justification if needed" do
652
+ frag10 = HexaPDF::Layout::TextFragment.create(" ", font: @font)
653
+ frag10.items.freeze
654
+ frag10b = HexaPDF::Layout::TextLayouter::Box.new(frag10)
655
+ frag20 = HexaPDF::Layout::TextFragment.create(" ", font: @font, font_size: 20)
656
+ frag20b = HexaPDF::Layout::TextLayouter::Box.new(frag20)
657
+ items = boxes(20, 20, 20, 20, 30).insert(1, frag10b).insert(3, frag20b).insert(5, frag10b)
658
+ # Width of spaces: 2.5 * 2 + 5 = 10 (from AFM file, adjusted for font size)
659
+ # Line width: 20 * 4 + width_of_spaces = 90
660
+ # Missing width: 100 - 90 = 10
661
+ # -> Each space must be doubled!
662
+
663
+ @style.align = :justify
664
+ result = @layouter.fit(items, 100, 100)
665
+ assert(result.remaining_items.empty?)
666
+ assert_equal(:success, result.status)
667
+ assert_equal(9, result.lines[0].items.count)
668
+ assert_in_delta(100, result.lines[0].width)
669
+ assert_equal(-250, result.lines[0].items[1].items[0])
670
+ assert_equal(-250, result.lines[0].items[4].items[0])
671
+ assert_equal(-250, result.lines[0].items[6].items[0])
672
+ assert_equal(30, result.lines[1].width)
673
+ end
674
+ end
675
+
676
+ describe "Result#draw" do
677
+ def assert_positions(content, positions)
678
+ processor = TestHelper::OperatorRecorder.new
679
+ HexaPDF::Content::Parser.new.parse(content, processor)
680
+ result = processor.recorded_ops
681
+ leading = (result.select {|name, _| name == :set_leading } || [0]).map(&:last).flatten.first
682
+ pos = [0, 0]
683
+ result.select! {|name, _| name == :set_text_matrix || name == :move_text_next_line }.
684
+ map! do |name, ops|
685
+ case name
686
+ when :set_text_matrix then pos = ops[-2, 2]
687
+ when :move_text_next_line then pos[1] -= leading
688
+ end
689
+ pos.dup
690
+ end
691
+ positions.each_with_index do |(x, y), index|
692
+ assert_in_delta(x, result[index][0], 0.00001)
693
+ assert_in_delta(y, result[index][1], 0.00001)
694
+ end
695
+ end
696
+
697
+ before do
698
+ @frag = HexaPDF::Layout::TextFragment.create("This is some more text.\n" \
699
+ "This is some more text.", font: @font)
700
+ @width = HexaPDF::Layout::TextFragment.create("This is some ", font: @font).width
701
+ @layouter = HexaPDF::Layout::TextLayouter.new
702
+ @canvas = @doc.pages.add.canvas
703
+
704
+ @line1w = HexaPDF::Layout::TextFragment.create("This is some", font: @font).width
705
+ @line2w = HexaPDF::Layout::TextFragment.create("more text.", font: @font).width
706
+ end
707
+
708
+ it "respects the x- and y-offsets" do
709
+ top = 100
710
+ @layouter.style.valign = :center
711
+ @layouter.style.align = :center
712
+
713
+ result = @layouter.fit([@frag], @width, top)
714
+ result.draw(@canvas, 5, top)
715
+
716
+ initial_baseline = top - result.lines.first.y_offset
717
+ assert_positions(@canvas.contents,
718
+ [[5 + (@width - @line1w) / 2, initial_baseline],
719
+ [5 + (@width - @line2w) / 2, initial_baseline - @frag.height],
720
+ [5 + (@width - @line1w) / 2, initial_baseline - @frag.height * 2],
721
+ [5 + (@width - @line2w) / 2, initial_baseline - @frag.height * 3]])
722
+ end
723
+
724
+ it "makes sure that text fragments don't pollute the graphics state for inline boxes" do
725
+ inline_box = HexaPDF::Layout::InlineBox.create(width: 10, height: 10) {|c, _| c.text("A") }
726
+ result = @layouter.fit([@frag, inline_box], 200, 100)
727
+ assert_raises(HexaPDF::Error) { result.draw(@canvas, 0, 0) } # bc font should be reset to nil
728
+ end
729
+
730
+ it "doesn't do unnecessary work for consecutive text fragments with same style" do
731
+ @layouter.fit([@frag], 200, 100).draw(@canvas, 0, 0)
732
+ assert_operators(@canvas.contents, [[:save_graphics_state],
733
+ [:set_leading, [9.0]],
734
+ [:set_font_and_size, [:F1, 10]],
735
+ [:begin_text],
736
+ [:move_text, [0, -6.83]],
737
+ [:show_text, ["This is some more text."]],
738
+ [:move_text_next_line],
739
+ [:show_text, ["This is some more text."]],
740
+ [:end_text],
741
+ [:restore_graphics_state]])
742
+ end
743
+
744
+ it "doesn't do unnecessary work for placeholder boxes" do
745
+ box1 = HexaPDF::Layout::InlineBox.create(width: 10, height: 20)
746
+ box2 = HexaPDF::Layout::InlineBox.create(width: 30, height: 40) { @canvas.line_width(2) }
747
+ @layouter.fit([box1, box2], 200, 100).draw(@canvas, 0, 0)
748
+ assert_operators(@canvas.contents, [[:save_graphics_state],
749
+ [:restore_graphics_state],
750
+ [:save_graphics_state],
751
+ [:concatenate_matrix, [1, 0, 0, 1, 10, -40]],
752
+ [:set_line_width, [2]],
753
+ [:restore_graphics_state],
754
+ [:save_graphics_state],
755
+ [:restore_graphics_state]])
756
+ end
757
+ end
758
+ end