hexapdf 0.17.1 → 0.17.2

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 (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