prawn 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/Gemfile +1 -9
  5. data/Rakefile +12 -22
  6. data/lib/prawn.rb +29 -48
  7. data/lib/prawn/document.rb +148 -123
  8. data/lib/prawn/document/bounding_box.rb +33 -26
  9. data/lib/prawn/document/column_box.rb +5 -7
  10. data/lib/prawn/document/internals.rb +6 -6
  11. data/lib/prawn/document/span.rb +20 -17
  12. data/lib/prawn/encoding.rb +65 -67
  13. data/lib/prawn/errors.rb +10 -7
  14. data/lib/prawn/font.rb +78 -62
  15. data/lib/prawn/font/afm.rb +93 -66
  16. data/lib/prawn/font/dfont.rb +2 -10
  17. data/lib/prawn/font/ttc.rb +34 -0
  18. data/lib/prawn/font/ttf.rb +73 -65
  19. data/lib/prawn/font_metric_cache.rb +9 -8
  20. data/lib/prawn/graphics.rb +110 -70
  21. data/lib/prawn/graphics/blend_mode.rb +7 -8
  22. data/lib/prawn/graphics/cap_style.rb +2 -4
  23. data/lib/prawn/graphics/color.rb +23 -26
  24. data/lib/prawn/graphics/dash.rb +22 -12
  25. data/lib/prawn/graphics/join_style.rb +8 -4
  26. data/lib/prawn/graphics/patterns.rb +185 -96
  27. data/lib/prawn/graphics/transformation.rb +11 -9
  28. data/lib/prawn/graphics/transparency.rb +15 -13
  29. data/lib/prawn/grid.rb +20 -20
  30. data/lib/prawn/image_handler.rb +4 -6
  31. data/lib/prawn/images.rb +22 -15
  32. data/lib/prawn/images/image.rb +0 -1
  33. data/lib/prawn/images/jpg.rb +26 -22
  34. data/lib/prawn/images/png.rb +60 -57
  35. data/lib/prawn/measurement_extensions.rb +8 -9
  36. data/lib/prawn/measurements.rb +14 -15
  37. data/lib/prawn/outline.rb +96 -78
  38. data/lib/prawn/repeater.rb +12 -10
  39. data/lib/prawn/security.rb +66 -48
  40. data/lib/prawn/security/arcfour.rb +1 -3
  41. data/lib/prawn/soft_mask.rb +23 -25
  42. data/lib/prawn/stamp.rb +16 -12
  43. data/lib/prawn/text.rb +59 -45
  44. data/lib/prawn/text/box.rb +9 -8
  45. data/lib/prawn/text/formatted.rb +4 -6
  46. data/lib/prawn/text/formatted/arranger.rb +51 -30
  47. data/lib/prawn/text/formatted/box.rb +112 -88
  48. data/lib/prawn/text/formatted/fragment.rb +10 -15
  49. data/lib/prawn/text/formatted/line_wrap.rb +118 -61
  50. data/lib/prawn/text/formatted/parser.rb +134 -110
  51. data/lib/prawn/text/formatted/wrap.rb +42 -32
  52. data/lib/prawn/transformation_stack.rb +3 -4
  53. data/lib/prawn/utilities.rb +6 -21
  54. data/lib/prawn/version.rb +1 -3
  55. data/lib/prawn/view.rb +4 -2
  56. data/manual/basic_concepts/adding_pages.rb +4 -7
  57. data/manual/basic_concepts/basic_concepts.rb +29 -22
  58. data/manual/basic_concepts/creation.rb +8 -11
  59. data/manual/basic_concepts/cursor.rb +2 -5
  60. data/manual/basic_concepts/measurement.rb +3 -6
  61. data/manual/basic_concepts/origin.rb +3 -6
  62. data/manual/basic_concepts/other_cursor_helpers.rb +9 -12
  63. data/manual/basic_concepts/view.rb +20 -16
  64. data/manual/bounding_box/bounding_box.rb +27 -24
  65. data/manual/bounding_box/bounds.rb +9 -12
  66. data/manual/bounding_box/canvas.rb +2 -5
  67. data/manual/bounding_box/creation.rb +4 -7
  68. data/manual/bounding_box/indentation.rb +12 -15
  69. data/manual/bounding_box/nesting.rb +22 -17
  70. data/manual/bounding_box/russian_boxes.rb +8 -9
  71. data/manual/bounding_box/stretchy.rb +10 -13
  72. data/manual/contents.rb +26 -22
  73. data/manual/cover.rb +22 -20
  74. data/manual/document_and_page_options/background.rb +9 -13
  75. data/manual/document_and_page_options/document_and_page_options.rb +23 -20
  76. data/manual/document_and_page_options/metadata.rb +16 -16
  77. data/manual/document_and_page_options/page_margins.rb +16 -20
  78. data/manual/document_and_page_options/page_size.rb +11 -12
  79. data/manual/document_and_page_options/print_scaling.rb +15 -15
  80. data/manual/example_helper.rb +2 -4
  81. data/manual/graphics/blend_mode.rb +10 -9
  82. data/manual/graphics/circle_and_ellipse.rb +2 -5
  83. data/manual/graphics/color.rb +5 -9
  84. data/manual/graphics/common_lines.rb +5 -8
  85. data/manual/graphics/fill_and_stroke.rb +2 -5
  86. data/manual/graphics/fill_rules.rb +7 -10
  87. data/manual/graphics/gradients.rb +25 -21
  88. data/manual/graphics/graphics.rb +49 -43
  89. data/manual/graphics/helper.rb +10 -9
  90. data/manual/graphics/line_width.rb +5 -7
  91. data/manual/graphics/lines_and_curves.rb +5 -8
  92. data/manual/graphics/polygon.rb +4 -8
  93. data/manual/graphics/rectangle.rb +2 -5
  94. data/manual/graphics/rotate.rb +4 -7
  95. data/manual/graphics/scale.rb +12 -15
  96. data/manual/graphics/soft_masks.rb +1 -4
  97. data/manual/graphics/stroke_cap.rb +3 -6
  98. data/manual/graphics/stroke_dash.rb +9 -12
  99. data/manual/graphics/stroke_join.rb +2 -5
  100. data/manual/graphics/translate.rb +7 -10
  101. data/manual/graphics/transparency.rb +5 -8
  102. data/manual/how_to_read_this_manual.rb +4 -6
  103. data/manual/images/absolute_position.rb +4 -7
  104. data/manual/images/fit.rb +5 -8
  105. data/manual/images/horizontal.rb +6 -9
  106. data/manual/images/images.rb +25 -23
  107. data/manual/images/plain_image.rb +3 -6
  108. data/manual/images/scale.rb +7 -10
  109. data/manual/images/vertical.rb +10 -13
  110. data/manual/images/width_and_height.rb +8 -11
  111. data/manual/layout/boxes.rb +3 -6
  112. data/manual/layout/content.rb +5 -8
  113. data/manual/layout/layout.rb +16 -16
  114. data/manual/layout/simple_grid.rb +4 -7
  115. data/manual/outline/add_subsection_to.rb +18 -21
  116. data/manual/outline/insert_section_after.rb +13 -16
  117. data/manual/outline/outline.rb +19 -17
  118. data/manual/outline/sections_and_pages.rb +15 -18
  119. data/manual/repeatable_content/alternate_page_numbering.rb +19 -17
  120. data/manual/repeatable_content/page_numbering.rb +15 -16
  121. data/manual/repeatable_content/repeatable_content.rb +23 -19
  122. data/manual/repeatable_content/repeater.rb +12 -15
  123. data/manual/repeatable_content/stamp.rb +12 -15
  124. data/manual/security/encryption.rb +7 -10
  125. data/manual/security/permissions.rb +17 -14
  126. data/manual/security/security.rb +17 -16
  127. data/manual/table.rb +2 -4
  128. data/manual/text/alignment.rb +14 -17
  129. data/manual/text/color.rb +10 -11
  130. data/manual/text/column_box.rb +5 -8
  131. data/manual/text/fallback_fonts.rb +23 -21
  132. data/manual/text/font.rb +9 -12
  133. data/manual/text/font_size.rb +11 -14
  134. data/manual/text/font_style.rb +4 -7
  135. data/manual/text/formatted_callbacks.rb +23 -21
  136. data/manual/text/formatted_text.rb +31 -25
  137. data/manual/text/free_flowing_text.rb +18 -21
  138. data/manual/text/inline.rb +16 -19
  139. data/manual/text/kerning_and_character_spacing.rb +12 -15
  140. data/manual/text/leading.rb +5 -8
  141. data/manual/text/line_wrapping.rb +33 -17
  142. data/manual/text/paragraph_indentation.rb +11 -14
  143. data/manual/text/positioned_text.rb +13 -16
  144. data/manual/text/registering_families.rb +16 -19
  145. data/manual/text/rendering_and_color.rb +7 -10
  146. data/manual/text/right_to_left_text.rb +24 -19
  147. data/manual/text/rotation.rb +26 -23
  148. data/manual/text/single_usage.rb +6 -9
  149. data/manual/text/text.rb +56 -52
  150. data/manual/text/text_box_excess.rb +18 -17
  151. data/manual/text/text_box_extensions.rb +16 -15
  152. data/manual/text/text_box_overflow.rb +15 -18
  153. data/manual/text/utf8.rb +9 -12
  154. data/manual/text/win_ansi_charset.rb +18 -19
  155. data/prawn.gemspec +37 -27
  156. data/spec/extensions/encoding_helpers.rb +0 -2
  157. data/spec/manual_spec.rb +33 -0
  158. data/spec/prawn/document/bounding_box_spec.rb +546 -0
  159. data/spec/prawn/document/column_box_spec.rb +73 -0
  160. data/spec/prawn/document/security_spec.rb +173 -0
  161. data/spec/prawn/document_annotations_spec.rb +74 -0
  162. data/spec/prawn/document_destinations_spec.rb +13 -0
  163. data/spec/prawn/document_grid_spec.rb +96 -0
  164. data/spec/prawn/document_reference_spec.rb +25 -0
  165. data/spec/prawn/document_span_spec.rb +34 -0
  166. data/spec/prawn/document_spec.rb +751 -0
  167. data/spec/prawn/font_metric_cache_spec.rb +52 -0
  168. data/spec/prawn/font_spec.rb +513 -0
  169. data/spec/prawn/graphics/blend_mode_spec.rb +61 -0
  170. data/spec/prawn/graphics/transparency_spec.rb +79 -0
  171. data/spec/prawn/graphics_spec.rb +817 -0
  172. data/spec/prawn/graphics_stroke_styles_spec.rb +227 -0
  173. data/spec/{image_handler_spec.rb → prawn/image_handler_spec.rb} +13 -15
  174. data/spec/prawn/images/jpg_spec.rb +18 -0
  175. data/spec/prawn/images/png_spec.rb +281 -0
  176. data/spec/prawn/images_spec.rb +170 -0
  177. data/spec/prawn/measurements_extensions_spec.rb +22 -0
  178. data/spec/prawn/outline_spec.rb +408 -0
  179. data/spec/prawn/repeater_spec.rb +163 -0
  180. data/spec/prawn/soft_mask_spec.rb +72 -0
  181. data/spec/prawn/stamp_spec.rb +168 -0
  182. data/spec/prawn/text/box_spec.rb +1113 -0
  183. data/spec/prawn/text/formatted/arranger_spec.rb +464 -0
  184. data/spec/prawn/text/formatted/box_spec.rb +825 -0
  185. data/spec/prawn/text/formatted/fragment_spec.rb +341 -0
  186. data/spec/prawn/text/formatted/line_wrap_spec.rb +491 -0
  187. data/spec/prawn/text/formatted/parser_spec.rb +667 -0
  188. data/spec/prawn/text_draw_text_spec.rb +147 -0
  189. data/spec/prawn/text_rendering_mode_spec.rb +42 -0
  190. data/spec/prawn/text_spacing_spec.rb +93 -0
  191. data/spec/prawn/text_spec.rb +601 -0
  192. data/spec/prawn/text_with_inline_formatting_spec.rb +33 -0
  193. data/spec/{transformation_stack_spec.rb → prawn/transformation_stack_spec.rb} +21 -20
  194. data/spec/prawn/view_spec.rb +45 -0
  195. data/spec/spec_helper.rb +16 -16
  196. metadata +96 -151
  197. metadata.gz.sig +1 -0
  198. data/data/images/16bit.alpha +0 -0
  199. data/data/images/16bit.color +0 -0
  200. data/data/images/16bit.png +0 -0
  201. data/data/images/arrow.png +0 -0
  202. data/data/images/arrow2.png +0 -0
  203. data/data/images/blend_modes_bottom_layer.jpg +0 -0
  204. data/data/images/blend_modes_top_layer.jpg +0 -0
  205. data/data/images/dice.alpha +0 -0
  206. data/data/images/dice.color +0 -0
  207. data/data/images/dice.png +0 -0
  208. data/data/images/dice_interlaced.png +0 -0
  209. data/data/images/fractal.jpg +0 -0
  210. data/data/images/indexed_color.dat +0 -0
  211. data/data/images/indexed_color.png +0 -0
  212. data/data/images/indexed_transparency.png +0 -0
  213. data/data/images/indexed_transparency_alpha.dat +0 -0
  214. data/data/images/indexed_transparency_color.dat +0 -0
  215. data/data/images/letterhead.jpg +0 -0
  216. data/data/images/license.md +0 -8
  217. data/data/images/page_white_text.alpha +0 -0
  218. data/data/images/page_white_text.color +0 -0
  219. data/data/images/page_white_text.png +0 -0
  220. data/data/images/pigs.jpg +0 -0
  221. data/data/images/prawn.png +0 -0
  222. data/data/images/ruport.png +0 -0
  223. data/data/images/ruport_data.dat +0 -0
  224. data/data/images/ruport_transparent.png +0 -0
  225. data/data/images/ruport_type0.png +0 -0
  226. data/data/images/stef.jpg +0 -0
  227. data/data/images/tru256.bmp +0 -0
  228. data/data/images/web-links.dat +0 -1
  229. data/data/images/web-links.png +0 -0
  230. data/data/pdfs/complex_template.pdf +0 -0
  231. data/data/pdfs/contains_ttf_font.pdf +0 -0
  232. data/data/pdfs/encrypted.pdf +0 -0
  233. data/data/pdfs/form.pdf +1 -819
  234. data/data/pdfs/hexagon.pdf +0 -61
  235. data/data/pdfs/indirect_reference.pdf +0 -86
  236. data/data/pdfs/multipage_template.pdf +0 -127
  237. data/data/pdfs/nested_pages.pdf +0 -118
  238. data/data/pdfs/page_without_mediabox.pdf +0 -193
  239. data/data/pdfs/resources_as_indirect_object.pdf +0 -83
  240. data/data/pdfs/two_hexagons.pdf +0 -90
  241. data/data/pdfs/version_1_6.pdf +0 -61
  242. data/data/shift_jis_text.txt +0 -1
  243. data/spec/acceptance/png_spec.rb +0 -35
  244. data/spec/annotations_spec.rb +0 -67
  245. data/spec/blend_mode_spec.rb +0 -71
  246. data/spec/bounding_box_spec.rb +0 -501
  247. data/spec/column_box_spec.rb +0 -59
  248. data/spec/destinations_spec.rb +0 -13
  249. data/spec/document_spec.rb +0 -738
  250. data/spec/font_metric_cache_spec.rb +0 -52
  251. data/spec/font_spec.rb +0 -475
  252. data/spec/formatted_text_arranger_spec.rb +0 -452
  253. data/spec/formatted_text_box_spec.rb +0 -716
  254. data/spec/formatted_text_fragment_spec.rb +0 -299
  255. data/spec/graphics_spec.rb +0 -705
  256. data/spec/grid_spec.rb +0 -95
  257. data/spec/images_spec.rb +0 -167
  258. data/spec/inline_formatted_text_parser_spec.rb +0 -568
  259. data/spec/jpg_spec.rb +0 -23
  260. data/spec/line_wrap_spec.rb +0 -366
  261. data/spec/measurement_units_spec.rb +0 -22
  262. data/spec/outline_spec.rb +0 -409
  263. data/spec/png_spec.rb +0 -257
  264. data/spec/reference_spec.rb +0 -25
  265. data/spec/repeater_spec.rb +0 -154
  266. data/spec/security_spec.rb +0 -151
  267. data/spec/soft_mask_spec.rb +0 -78
  268. data/spec/span_spec.rb +0 -43
  269. data/spec/stamp_spec.rb +0 -179
  270. data/spec/stroke_styles_spec.rb +0 -208
  271. data/spec/text_at_spec.rb +0 -142
  272. data/spec/text_box_spec.rb +0 -1042
  273. data/spec/text_rendering_mode_spec.rb +0 -45
  274. data/spec/text_spacing_spec.rb +0 -93
  275. data/spec/text_spec.rb +0 -543
  276. data/spec/text_with_inline_formatting_spec.rb +0 -35
  277. data/spec/transparency_spec.rb +0 -91
  278. data/spec/view_spec.rb +0 -42
@@ -0,0 +1,825 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prawn::Text::Formatted::Box do
4
+ let(:pdf) { create_pdf }
5
+
6
+ describe 'wrapping' do
7
+ it 'does not wrap between two fragments' do
8
+ texts = [
9
+ { text: 'Hello ' },
10
+ { text: 'World' },
11
+ { text: '2', styles: [:superscript] }
12
+ ]
13
+ text_box = described_class.new(
14
+ texts,
15
+ document: pdf,
16
+ width: pdf.width_of('Hello World')
17
+ )
18
+ text_box.render
19
+ expect(text_box.text).to eq("Hello\nWorld2")
20
+ end
21
+
22
+ it 'does not raise an Encoding::CompatibilityError when keeping a TTF and '\
23
+ 'an AFM font together' do
24
+ file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
25
+
26
+ pdf.font_families['Kai'] = {
27
+ normal: { file: file, font: 'Kai' }
28
+ }
29
+
30
+ texts = [
31
+ { text: 'Hello ' },
32
+ { text: '再见', font: 'Kai' },
33
+ { text: 'World' }
34
+ ]
35
+ text_box = described_class.new(
36
+ texts,
37
+ document: pdf,
38
+ width: pdf.width_of('Hello World')
39
+ )
40
+
41
+ text_box.render
42
+ end
43
+
44
+ it 'wraps between two fragments when the preceding fragment ends with '\
45
+ 'a white space' do
46
+ texts = [
47
+ { text: 'Hello ' },
48
+ { text: 'World ' },
49
+ { text: '2', styles: [:superscript] }
50
+ ]
51
+ text_box = described_class.new(
52
+ texts,
53
+ document: pdf,
54
+ width: pdf.width_of('Hello World')
55
+ )
56
+ text_box.render
57
+ expect(text_box.text).to eq("Hello World\n2")
58
+
59
+ texts = [
60
+ { text: 'Hello ' },
61
+ { text: "World\n" },
62
+ { text: '2', styles: [:superscript] }
63
+ ]
64
+ text_box = described_class.new(
65
+ texts,
66
+ document: pdf,
67
+ width: pdf.width_of('Hello World')
68
+ )
69
+ text_box.render
70
+ expect(text_box.text).to eq("Hello World\n2")
71
+ end
72
+
73
+ it 'wraps between two fragments when the final fragment begins with '\
74
+ 'a white space' do
75
+ texts = [
76
+ { text: 'Hello ' },
77
+ { text: 'World' },
78
+ { text: ' 2', styles: [:superscript] }
79
+ ]
80
+ text_box = described_class.new(
81
+ texts,
82
+ document: pdf,
83
+ width: pdf.width_of('Hello World')
84
+ )
85
+ text_box.render
86
+ expect(text_box.text).to eq("Hello World\n2")
87
+
88
+ texts = [
89
+ { text: 'Hello ' },
90
+ { text: 'World' },
91
+ { text: "\n2", styles: [:superscript] }
92
+ ]
93
+ text_box = described_class.new(
94
+ texts,
95
+ document: pdf,
96
+ width: pdf.width_of('Hello World')
97
+ )
98
+ text_box.render
99
+ expect(text_box.text).to eq("Hello World\n2")
100
+ end
101
+
102
+ it 'properlies handle empty slices using default encoding' do
103
+ texts = [{
104
+ text: 'Noua Delineatio Geographica generalis | Apostolicarum ' \
105
+ 'peregrinationum | S FRANCISCI XAUERII | Indiarum & Iaponiæ Apostoli',
106
+ font: 'Courier', size: 10
107
+ }]
108
+ text_box = described_class.new(
109
+ texts,
110
+ document: pdf,
111
+ width: pdf.width_of('Noua Delineatio Geographica gen')
112
+ )
113
+ expect do
114
+ text_box.render
115
+ end.to_not raise_error
116
+ expect(text_box.text).to eq(
117
+ "Noua Delineatio Geographica\ngeneralis | Apostolicarum\n" \
118
+ "peregrinationum | S FRANCISCI\nXAUERII | Indiarum & Iaponi\346\n" \
119
+ 'Apostoli'
120
+ )
121
+ end
122
+ end
123
+
124
+ describe 'Text::Formatted::Box with :fallback_fonts option that includes' \
125
+ 'a Chinese font and set of Chinese glyphs not in the current font' do
126
+ it 'changes the font to the Chinese font for the Chinese glyphs' do
127
+ file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
128
+ pdf.font_families['Kai'] = {
129
+ normal: { file: file, font: 'Kai' }
130
+ }
131
+ formatted_text = [
132
+ { text: 'hello你好' },
133
+ { text: '再见goodbye' }
134
+ ]
135
+ pdf.formatted_text_box(formatted_text, fallback_fonts: ['Kai'])
136
+
137
+ text = PDF::Inspector::Text.analyze(pdf.render)
138
+
139
+ fonts_used = text.font_settings.map { |e| e[:name] }
140
+ expect(fonts_used.length).to eq(4)
141
+ expect(fonts_used[0]).to eq(:Helvetica)
142
+ expect(fonts_used[1].to_s).to match(/GBZenKai-Medium/)
143
+ expect(fonts_used[2].to_s).to match(/GBZenKai-Medium/)
144
+ expect(fonts_used[3]).to eq(:Helvetica)
145
+
146
+ expect(text.strings[0]).to eq('hello')
147
+ expect(text.strings[1]).to eq('你好')
148
+ expect(text.strings[2]).to eq('再见')
149
+ expect(text.strings[3]).to eq('goodbye')
150
+ end
151
+ end
152
+
153
+ describe 'Text::Formatted::Box with :fallback_fonts option that includes' \
154
+ 'an AFM font and Win-Ansi glyph not in the current Chinese font' do
155
+ it 'changes the font to the AFM font for the Win-Ansi glyph' do
156
+ file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
157
+ pdf.font_families['Kai'] = {
158
+ normal: { file: file, font: 'Kai' }
159
+ }
160
+ pdf.font('Kai')
161
+ formatted_text = [
162
+ { text: 'hello你好' },
163
+ { text: '再见€' }
164
+ ]
165
+ pdf.formatted_text_box(formatted_text, fallback_fonts: ['Helvetica'])
166
+
167
+ text = PDF::Inspector::Text.analyze(pdf.render)
168
+
169
+ fonts_used = text.font_settings.map { |e| e[:name] }
170
+ expect(fonts_used.length).to eq(4)
171
+ expect(fonts_used[0].to_s).to match(/GBZenKai-Medium/)
172
+ expect(fonts_used[1].to_s).to match(/GBZenKai-Medium/)
173
+ expect(fonts_used[2].to_s).to match(/GBZenKai-Medium/)
174
+ expect(fonts_used[3]).to eq(:Helvetica)
175
+
176
+ expect(text.strings[0]).to eq('hello')
177
+ expect(text.strings[1]).to eq('你好')
178
+ expect(text.strings[2]).to eq('再见')
179
+ expect(text.strings[3]).to eq('€')
180
+ end
181
+ end
182
+
183
+ describe 'Text::Formatted::Box with :fallback_fonts option and fragment ' \
184
+ 'level font' do
185
+ it 'uses the fragment level font except for glyphs not in that font' do
186
+ file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
187
+ pdf.font_families['Kai'] = {
188
+ normal: { file: file, font: 'Kai' }
189
+ }
190
+
191
+ file = "#{Prawn::DATADIR}/fonts/DejaVuSans.ttf"
192
+ pdf.font_families['DejaVu Sans'] = {
193
+ normal: { file: file }
194
+ }
195
+
196
+ formatted_text = [
197
+ { text: 'hello你好' },
198
+ { text: '再见goodbye', font: 'Times-Roman' }
199
+ ]
200
+ pdf.formatted_text_box(formatted_text, fallback_fonts: ['Kai'])
201
+
202
+ text = PDF::Inspector::Text.analyze(pdf.render)
203
+
204
+ fonts_used = text.font_settings.map { |e| e[:name] }
205
+ expect(fonts_used.length).to eq(4)
206
+ expect(fonts_used[0]).to eq(:Helvetica)
207
+ expect(fonts_used[1].to_s).to match(/GBZenKai-Medium/)
208
+ expect(fonts_used[2].to_s).to match(/GBZenKai-Medium/)
209
+ expect(fonts_used[3]).to eq(:"Times-Roman")
210
+
211
+ expect(text.strings[0]).to eq('hello')
212
+ expect(text.strings[1]).to eq('你好')
213
+ expect(text.strings[2]).to eq('再见')
214
+ expect(text.strings[3]).to eq('goodbye')
215
+ end
216
+ end
217
+
218
+ describe 'Text::Formatted::Box' do
219
+ let(:formatted_text) { [{ text: 'hello你好' }] }
220
+
221
+ before do
222
+ file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
223
+ pdf.font_families['Kai'] = {
224
+ normal: { file: file, font: 'Kai' }
225
+ }
226
+
227
+ file = "#{Prawn::DATADIR}/fonts/DejaVuSans.ttf"
228
+ pdf.font_families['DejaVu Sans'] = {
229
+ normal: { file: file }
230
+ }
231
+
232
+ pdf.fallback_fonts(['Kai'])
233
+ pdf.fallback_fonts = ['Kai']
234
+ end
235
+
236
+ it '#fallback_fonts should return the document-wide fallback fonts' do
237
+ expect(pdf.fallback_fonts).to eq(['Kai'])
238
+ end
239
+
240
+ it 'is able to set text fallback_fonts document-wide' do
241
+ pdf.formatted_text_box(formatted_text)
242
+
243
+ text = PDF::Inspector::Text.analyze(pdf.render)
244
+
245
+ fonts_used = text.font_settings.map { |e| e[:name] }
246
+ expect(fonts_used.length).to eq(2)
247
+ expect(fonts_used[0]).to eq(:Helvetica)
248
+ expect(fonts_used[1].to_s).to match(/GBZenKai-Medium/)
249
+ end
250
+
251
+ it 'is able to override document-wide fallback_fonts' do
252
+ pdf.fallback_fonts = ['DejaVu Sans']
253
+ pdf.formatted_text_box(formatted_text, fallback_fonts: ['Kai'])
254
+
255
+ text = PDF::Inspector::Text.analyze(pdf.render)
256
+
257
+ fonts_used = text.font_settings.map { |e| e[:name] }
258
+ expect(fonts_used.length).to eq(2)
259
+ expect(fonts_used[0]).to eq(:Helvetica)
260
+ expect(fonts_used[1]).to match(/Kai/)
261
+ end
262
+
263
+ it 'should omit the fallback fonts overhead when passing an empty array ' \
264
+ 'as the :fallback_fonts' do
265
+ pdf.font('Kai')
266
+
267
+ box = described_class.new(
268
+ formatted_text,
269
+ document: pdf,
270
+ fallback_fonts: []
271
+ )
272
+
273
+ allow(box).to receive(:process_fallback_fonts)
274
+ box.render
275
+ expect(box).to_not have_received(:process_fallback_fonts)
276
+ end
277
+
278
+ it 'is able to clear document-wide fallback_fonts' do
279
+ pdf.fallback_fonts([])
280
+ box = described_class.new(formatted_text, document: pdf)
281
+
282
+ pdf.font('Kai')
283
+
284
+ allow(box).to receive(:process_fallback_fonts)
285
+ box.render
286
+ expect(box).to_not have_received(:process_fallback_fonts)
287
+ end
288
+ end
289
+
290
+ describe 'Text::Formatted::Box with :fallback_fonts option ' \
291
+ 'with glyphs not in the primary or the fallback fonts' do
292
+ it 'raises an exception' do
293
+ formatted_text = [{ text: 'hello world. 世界你好。' }]
294
+
295
+ expect do
296
+ pdf.formatted_text_box(formatted_text, fallback_fonts: ['Courier'])
297
+ end.to raise_error(Prawn::Errors::IncompatibleStringEncoding)
298
+ end
299
+ end
300
+
301
+ describe 'Text::Formatted::Box#extensions' do
302
+ it 'is able to override default line wrapping' do
303
+ described_class.extensions << TestFormattedWrapOverride
304
+ pdf.formatted_text_box([{ text: 'hello world' }], {})
305
+ described_class.extensions.delete(TestFormattedWrapOverride)
306
+ text = PDF::Inspector::Text.analyze(pdf.render)
307
+ expect(text.strings[0]).to eq('all your base are belong to us')
308
+ end
309
+
310
+ it 'overrides Text::Formatted::Box line wrapping does not affect ' \
311
+ 'Text::Box wrapping' do
312
+ described_class.extensions << TestFormattedWrapOverride
313
+ pdf.text_box('hello world', {})
314
+ described_class.extensions.delete(TestFormattedWrapOverride)
315
+ text = PDF::Inspector::Text.analyze(pdf.render)
316
+ expect(text.strings[0]).to eq('hello world')
317
+ end
318
+
319
+ it "overring Text::Box line wrapping doesn't override Text::Box wrapping" do
320
+ Prawn::Text::Box.extensions << TestFormattedWrapOverride
321
+ pdf.text_box('hello world', {})
322
+ Prawn::Text::Box.extensions.delete(TestFormattedWrapOverride)
323
+ text = PDF::Inspector::Text.analyze(pdf.render)
324
+ expect(text.strings[0]).to eq('all your base are belong to us')
325
+ end
326
+ end
327
+
328
+ describe 'Text::Formatted::Box#render' do
329
+ it 'handles newlines' do
330
+ array = [{ text: "hello\nworld" }]
331
+ options = { document: pdf }
332
+ text_box = described_class.new(array, options)
333
+ text_box.render
334
+ expect(text_box.text).to eq("hello\nworld")
335
+ end
336
+
337
+ it 'omits spaces from the beginning of the line' do
338
+ array = [{ text: " hello\n world" }]
339
+ options = { document: pdf }
340
+ text_box = described_class.new(array, options)
341
+ text_box.render
342
+ expect(text_box.text).to eq("hello\nworld")
343
+ end
344
+
345
+ it 'is okay printing a line of whitespace' do
346
+ array = [{ text: "hello\n \nworld" }]
347
+ options = { document: pdf }
348
+ text_box = described_class.new(array, options)
349
+ text_box.render
350
+ expect(text_box.text).to eq("hello\n\nworld")
351
+
352
+ array = [
353
+ { text: 'hello' + ' ' * 500 },
354
+ { text: ' ' * 500 },
355
+ { text: ' ' * 500 + "\n" },
356
+ { text: 'world' }
357
+ ]
358
+ options = { document: pdf }
359
+ text_box = described_class.new(array, options)
360
+ text_box.render
361
+ expect(text_box.text).to eq("hello\n\nworld")
362
+ end
363
+
364
+ it 'enables fragment level direction setting' do
365
+ number_of_hellos = 18
366
+ array = [
367
+ { text: 'hello ' * number_of_hellos },
368
+ { text: 'world', direction: :ltr },
369
+ { text: ', how are you?' }
370
+ ]
371
+ options = { document: pdf, direction: :rtl }
372
+ text_box = described_class.new(array, options)
373
+ text_box.render
374
+ text = PDF::Inspector::Text.analyze(pdf.render)
375
+ expect(text.strings[0]).to eq('era woh ,')
376
+ expect(text.strings[1]).to eq('world')
377
+ expect(text.strings[2]).to eq(' olleh' * number_of_hellos)
378
+ expect(text.strings[3]).to eq('?uoy')
379
+ end
380
+ end
381
+
382
+ describe 'Text::Formatted::Box#render' do
383
+ it 'is able to perform fragment callbacks' do
384
+ callback_object = TestFragmentCallback.new('something', 7, document: pdf)
385
+ allow(callback_object).to receive(:render_behind)
386
+ allow(callback_object).to receive(:render_in_front)
387
+ array = [
388
+ { text: 'hello world ' },
389
+ { text: 'callback now', callback: callback_object }
390
+ ]
391
+ text_box = described_class.new(array, document: pdf)
392
+ text_box.render
393
+
394
+ expect(callback_object).to have_received(:render_behind).with(
395
+ kind_of(Prawn::Text::Formatted::Fragment)
396
+ )
397
+ expect(callback_object).to have_received(:render_in_front).with(
398
+ kind_of(Prawn::Text::Formatted::Fragment)
399
+ )
400
+ end
401
+
402
+ it 'is able to perform fragment callbacks on multiple objects' do
403
+ callback_object = TestFragmentCallback.new('something', 7, document: pdf)
404
+ allow(callback_object).to receive(:render_behind)
405
+ allow(callback_object).to receive(:render_in_front)
406
+
407
+ callback_object2 = TestFragmentCallback.new(
408
+ 'something else', 14, document: pdf
409
+ )
410
+ allow(callback_object2).to receive(:render_behind)
411
+ allow(callback_object2).to receive(:render_in_front)
412
+
413
+ array = [
414
+ { text: 'hello world ' },
415
+ { text: 'callback now', callback: [callback_object, callback_object2] }
416
+ ]
417
+ text_box = described_class.new(array, document: pdf)
418
+ text_box.render
419
+
420
+ expect(callback_object).to have_received(:render_behind).with(
421
+ kind_of(Prawn::Text::Formatted::Fragment)
422
+ )
423
+ expect(callback_object).to have_received(:render_in_front).with(
424
+ kind_of(Prawn::Text::Formatted::Fragment)
425
+ )
426
+ expect(callback_object2).to have_received(:render_behind).with(
427
+ kind_of(Prawn::Text::Formatted::Fragment)
428
+ )
429
+ expect(callback_object2).to have_received(:render_in_front).with(
430
+ kind_of(Prawn::Text::Formatted::Fragment)
431
+ )
432
+ end
433
+
434
+ it 'fragment callbacks is able to define only the callback they need' do
435
+ behind = TestFragmentCallbackBehind.new(
436
+ 'something', 7,
437
+ document: pdf
438
+ )
439
+ in_front = TestFragmentCallbackInFront.new(
440
+ 'something', 7,
441
+ document: pdf
442
+ )
443
+ array = [
444
+ { text: 'hello world ' },
445
+ { text: 'callback now', callback: [behind, in_front] }
446
+ ]
447
+ text_box = described_class.new(array, document: pdf)
448
+
449
+ text_box.render # trigger callbacks
450
+ end
451
+
452
+ it 'is able to set the font' do
453
+ array = [
454
+ { text: 'this contains ' },
455
+ {
456
+ text: 'Times-Bold',
457
+ styles: [:bold],
458
+ font: 'Times-Roman'
459
+ },
460
+ { text: ' text' }
461
+ ]
462
+ text_box = described_class.new(array, document: pdf)
463
+ text_box.render
464
+ contents = PDF::Inspector::Text.analyze(pdf.render)
465
+ fonts = contents.font_settings.map { |e| e[:name] }
466
+ expect(fonts).to eq([:Helvetica, :"Times-Bold", :Helvetica])
467
+ expect(contents.strings[0]).to eq('this contains ')
468
+ expect(contents.strings[1]).to eq('Times-Bold')
469
+ expect(contents.strings[2]).to eq(' text')
470
+ end
471
+
472
+ it 'is able to set bold' do
473
+ array = [
474
+ { text: 'this contains ' },
475
+ { text: 'bold', styles: [:bold] },
476
+ { text: ' text' }
477
+ ]
478
+ text_box = described_class.new(array, document: pdf)
479
+ text_box.render
480
+ contents = PDF::Inspector::Text.analyze(pdf.render)
481
+ fonts = contents.font_settings.map { |e| e[:name] }
482
+ expect(fonts).to eq([:Helvetica, :"Helvetica-Bold", :Helvetica])
483
+ expect(contents.strings[0]).to eq('this contains ')
484
+ expect(contents.strings[1]).to eq('bold')
485
+ expect(contents.strings[2]).to eq(' text')
486
+ end
487
+
488
+ it 'is able to set italics' do
489
+ array = [
490
+ { text: 'this contains ' },
491
+ { text: 'italic', styles: [:italic] },
492
+ { text: ' text' }
493
+ ]
494
+ text_box = described_class.new(array, document: pdf)
495
+ text_box.render
496
+ contents = PDF::Inspector::Text.analyze(pdf.render)
497
+ fonts = contents.font_settings.map { |e| e[:name] }
498
+ expect(fonts).to eq([:Helvetica, :"Helvetica-Oblique", :Helvetica])
499
+ end
500
+
501
+ it 'is able to set subscript' do
502
+ array = [
503
+ { text: 'this contains ' },
504
+ { text: 'subscript', size: 18, styles: [:subscript] },
505
+ { text: ' text' }
506
+ ]
507
+ text_box = described_class.new(array, document: pdf)
508
+ text_box.render
509
+ contents = PDF::Inspector::Text.analyze(pdf.render)
510
+ expect(contents.font_settings[0][:size]).to eq(12)
511
+ expect(contents.font_settings[1][:size])
512
+ .to be_within(0.0001).of(18 * 0.583)
513
+ end
514
+
515
+ it 'is able to set superscript' do
516
+ array = [
517
+ { text: 'this contains ' },
518
+ { text: 'superscript', size: 18, styles: [:superscript] },
519
+ { text: ' text' }
520
+ ]
521
+ text_box = described_class.new(array, document: pdf)
522
+ text_box.render
523
+ contents = PDF::Inspector::Text.analyze(pdf.render)
524
+ expect(contents.font_settings[0][:size]).to eq(12)
525
+ expect(contents.font_settings[1][:size])
526
+ .to be_within(0.0001).of(18 * 0.583)
527
+ end
528
+
529
+ it 'is able to set compound bold and italic text' do
530
+ array = [
531
+ { text: 'this contains ' },
532
+ { text: 'bold italic', styles: [:bold, :italic] },
533
+ { text: ' text' }
534
+ ]
535
+ text_box = described_class.new(array, document: pdf)
536
+ text_box.render
537
+ contents = PDF::Inspector::Text.analyze(pdf.render)
538
+ fonts = contents.font_settings.map { |e| e[:name] }
539
+ expect(fonts).to eq([:Helvetica, :"Helvetica-BoldOblique", :Helvetica])
540
+ end
541
+ it 'is able to underline' do
542
+ array = [
543
+ { text: 'this contains ' },
544
+ { text: 'underlined', styles: [:underline] },
545
+ { text: ' text' }
546
+ ]
547
+ text_box = described_class.new(array, document: pdf)
548
+ text_box.render
549
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
550
+ expect(line_drawing.points.length).to eq(2)
551
+ end
552
+
553
+ it 'is able to strikethrough' do
554
+ array = [
555
+ { text: 'this contains ' },
556
+ { text: 'struckthrough', styles: [:strikethrough] },
557
+ { text: ' text' }
558
+ ]
559
+ text_box = described_class.new(array, document: pdf)
560
+ text_box.render
561
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
562
+ expect(line_drawing.points.length).to eq(2)
563
+ end
564
+
565
+ it 'is able to add URL links' do
566
+ allow(pdf).to receive(:link_annotation)
567
+ array = [
568
+ { text: 'click ' },
569
+ { text: 'here', link: 'http://example.com' },
570
+ { text: ' to visit' }
571
+ ]
572
+ text_box = described_class.new(array, document: pdf)
573
+ text_box.render
574
+
575
+ expect(pdf).to have_received(:link_annotation).with(
576
+ kind_of(Array),
577
+ Border: [0, 0, 0],
578
+ A: {
579
+ Type: :Action,
580
+ S: :URI,
581
+ URI: 'http://example.com'
582
+ }
583
+ )
584
+ end
585
+
586
+ it 'is able to add destination links' do
587
+ allow(pdf).to receive(:link_annotation)
588
+ array = [
589
+ { text: 'Go to the ' },
590
+ { text: 'Table of Contents', anchor: 'ToC' }
591
+ ]
592
+ text_box = described_class.new(array, document: pdf)
593
+ text_box.render
594
+
595
+ expect(pdf).to have_received(:link_annotation).with(
596
+ kind_of(Array),
597
+ Border: [0, 0, 0],
598
+ Dest: 'ToC'
599
+ )
600
+ end
601
+
602
+ it 'is able to add local actions' do
603
+ allow(pdf).to receive(:link_annotation)
604
+ array = [
605
+ { text: 'click ' },
606
+ { text: 'here', local: '../example.pdf' },
607
+ { text: ' to open a local file' }
608
+ ]
609
+ text_box = described_class.new(array, document: pdf)
610
+ text_box.render
611
+
612
+ expect(pdf).to have_received(:link_annotation).with(
613
+ kind_of(Array),
614
+ Border: [0, 0, 0],
615
+ A: {
616
+ Type: :Action,
617
+ S: :Launch,
618
+ F: '../example.pdf',
619
+ NewWindow: true
620
+ }
621
+ )
622
+ end
623
+
624
+ it 'is able to set font size' do
625
+ array = [
626
+ { text: 'this contains ' },
627
+ { text: 'sized', size: 24 },
628
+ { text: ' text' }
629
+ ]
630
+ text_box = described_class.new(array, document: pdf)
631
+ text_box.render
632
+ contents = PDF::Inspector::Text.analyze(pdf.render)
633
+ expect(contents.font_settings[0][:size]).to eq(12)
634
+ expect(contents.font_settings[1][:size]).to eq(24)
635
+ end
636
+
637
+ it 'sets the baseline based on the tallest fragment on a given line' do
638
+ array = [
639
+ { text: 'this contains ' },
640
+ { text: 'sized', size: 24 },
641
+ { text: ' text' }
642
+ ]
643
+ text_box = described_class.new(array, document: pdf)
644
+ text_box.render
645
+ pdf.font_size(24) do
646
+ expect(text_box.height).to be_within(0.001)
647
+ .of(pdf.font.ascender + pdf.font.descender)
648
+ end
649
+ end
650
+
651
+ it 'is able to set color via an rgb hex string' do
652
+ array = [{
653
+ text: 'rgb',
654
+ color: 'ff0000'
655
+ }]
656
+ text_box = described_class.new(array, document: pdf)
657
+ text_box.render
658
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
659
+ expect(colors.fill_color_count).to eq(2)
660
+ expect(colors.stroke_color_count).to eq(2)
661
+ end
662
+
663
+ it 'is able to set color using a cmyk array' do
664
+ array = [{
665
+ text: 'cmyk',
666
+ color: [100, 0, 0, 0]
667
+ }]
668
+ text_box = described_class.new(array, document: pdf)
669
+ text_box.render
670
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
671
+ expect(colors.fill_color_count).to eq(2)
672
+ expect(colors.stroke_color_count).to eq(2)
673
+ end
674
+ end
675
+
676
+ describe 'Text::Formatted::Box#render(:dry_run => true)' do
677
+ it 'does not change the graphics state of the document' do
678
+ state_before = PDF::Inspector::Graphics::Color.analyze(pdf.render)
679
+ fill_color_count = state_before.fill_color_count
680
+ stroke_color_count = state_before.stroke_color_count
681
+ stroke_color_space_count = state_before.stroke_color_space_count
682
+
683
+ array = [{
684
+ text: 'Foo',
685
+ color: [0, 0, 0, 100]
686
+ }]
687
+ options = { document: pdf }
688
+ text_box = described_class.new(array, options)
689
+ text_box.render(dry_run: true)
690
+
691
+ state_after = PDF::Inspector::Graphics::Color.analyze(pdf.render)
692
+ expect(state_after.fill_color_count).to eq(fill_color_count)
693
+ expect(state_after.stroke_color_count).to eq(stroke_color_count)
694
+ expect(state_after.stroke_color_space_count)
695
+ .to eq(stroke_color_space_count)
696
+ end
697
+ end
698
+
699
+ describe 'Text::Formatted::Box#render with fragment level '\
700
+ ':character_spacing option' do
701
+ it 'draws the character spacing to the document' do
702
+ array = [{
703
+ text: 'hello world',
704
+ character_spacing: 7
705
+ }]
706
+ options = { document: pdf }
707
+ text_box = described_class.new(array, options)
708
+ text_box.render
709
+ contents = PDF::Inspector::Text.analyze(pdf.render)
710
+ expect(contents.character_spacing[0]).to eq(7)
711
+ end
712
+
713
+ it 'lays out text properly' do
714
+ array = [{
715
+ text: 'hello world',
716
+ font: 'Courier',
717
+ character_spacing: 10
718
+ }]
719
+ options = {
720
+ document: pdf,
721
+ width: 100,
722
+ overflow: :expand
723
+ }
724
+ text_box = described_class.new(array, options)
725
+ text_box.render
726
+ expect(text_box.text).to eq("hello\nworld")
727
+ end
728
+ end
729
+
730
+ describe 'Text::Formatted::Box#render with :align => :justify' do
731
+ it 'does not justify the last line of a paragraph' do
732
+ array = [
733
+ { text: 'hello world ' },
734
+ { text: "\n" },
735
+ { text: 'goodbye' }
736
+ ]
737
+ options = { document: pdf, align: :justify }
738
+ text_box = described_class.new(array, options)
739
+ text_box.render
740
+ contents = PDF::Inspector::Text.analyze(pdf.render)
741
+ expect(contents.word_spacing).to be_empty
742
+ end
743
+ end
744
+
745
+ describe 'Text::Formatted::Box#render with :valign => :center' do
746
+ it 'has a bottom gap equal to baseline and bottom of box' do
747
+ box_height = 100
748
+ y = 450
749
+ array = [{ text: 'Vertical Align' }]
750
+ options = {
751
+ document: pdf,
752
+ valign: :center,
753
+ at: [0, y],
754
+ width: 100,
755
+ height: box_height,
756
+ size: 16
757
+ }
758
+ text_box = described_class.new(array, options)
759
+ text_box.render
760
+ line_padding = (box_height - text_box.height + text_box.descender) * 0.5
761
+ baseline = y - line_padding
762
+
763
+ expect(text_box.at[1]).to be_within(0.01).of(baseline)
764
+ end
765
+ end
766
+
767
+ describe 'Text::Formatted::Box#render with :valign => :bottom' do
768
+ it 'does not render a gap between the text and bottom of box' do
769
+ box_height = 100
770
+ y = 450
771
+ array = [{ text: 'Vertical Align' }]
772
+ options = {
773
+ document: pdf,
774
+ valign: :bottom,
775
+ at: [0, y],
776
+ width: 100,
777
+ height: box_height,
778
+ size: 16
779
+ }
780
+ text_box = described_class.new(array, options)
781
+ text_box.render
782
+ top_padding = y - (box_height - text_box.height)
783
+
784
+ expect(text_box.at[1]).to be_within(0.01).of(top_padding)
785
+ end
786
+ end
787
+
788
+ class TestFragmentCallback
789
+ def initialize(_string, _number, _options); end
790
+
791
+ def render_behind(fragment); end
792
+
793
+ def render_in_front(fragment); end
794
+ end
795
+
796
+ class TestFragmentCallbackBehind
797
+ def initialize(_string, _number, _options); end
798
+
799
+ def render_behind(fragment); end
800
+ end
801
+
802
+ class TestFragmentCallbackInFront
803
+ def initialize(_string, _number, _options); end
804
+
805
+ def render_in_front(fragment); end
806
+ end
807
+
808
+ # rubocop: disable RSpec/InstanceVariable
809
+ module TestFormattedWrapOverride
810
+ def wrap(_array)
811
+ initialize_wrap([{ text: 'all your base are belong to us' }])
812
+ @line_wrap.wrap_line(
813
+ document: @document,
814
+ kerning: @kerning,
815
+ width: 10_000,
816
+ arranger: @arranger
817
+ )
818
+ fragment = @arranger.retrieve_fragment
819
+ format_and_draw_fragment(fragment, 0, @line_wrap.width, 0)
820
+
821
+ []
822
+ end
823
+ end
824
+ # rubocop: enable RSpec/InstanceVariable
825
+ end