prawn 2.0.2 → 2.3.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 (277) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/GPLv2 +20 -21
  5. data/Gemfile +3 -9
  6. data/Rakefile +20 -23
  7. data/lib/prawn.rb +37 -49
  8. data/lib/prawn/document.rb +181 -133
  9. data/lib/prawn/document/bounding_box.rb +41 -29
  10. data/lib/prawn/document/column_box.rb +7 -7
  11. data/lib/prawn/document/internals.rb +18 -8
  12. data/lib/prawn/document/span.rb +21 -16
  13. data/lib/prawn/encoding.rb +69 -68
  14. data/lib/prawn/errors.rb +12 -7
  15. data/lib/prawn/font.rb +115 -69
  16. data/lib/prawn/font_metric_cache.rb +14 -8
  17. data/lib/prawn/{font → fonts}/afm.rb +102 -68
  18. data/lib/prawn/{font → fonts}/dfont.rb +5 -11
  19. data/lib/prawn/fonts/otf.rb +11 -0
  20. data/lib/prawn/fonts/ttc.rb +36 -0
  21. data/lib/prawn/{font → fonts}/ttf.rb +87 -68
  22. data/lib/prawn/graphics.rb +120 -80
  23. data/lib/prawn/graphics/blend_mode.rb +65 -0
  24. data/lib/prawn/graphics/cap_style.rb +3 -3
  25. data/lib/prawn/graphics/color.rb +27 -25
  26. data/lib/prawn/graphics/dash.rb +23 -11
  27. data/lib/prawn/graphics/join_style.rb +9 -3
  28. data/lib/prawn/graphics/patterns.rb +197 -67
  29. data/lib/prawn/graphics/transformation.rb +17 -8
  30. data/lib/prawn/graphics/transparency.rb +17 -13
  31. data/lib/prawn/grid.rb +48 -47
  32. data/lib/prawn/image_handler.rb +5 -5
  33. data/lib/prawn/images.rb +39 -30
  34. data/lib/prawn/images/image.rb +2 -1
  35. data/lib/prawn/images/jpg.rb +28 -22
  36. data/lib/prawn/images/png.rb +107 -66
  37. data/lib/prawn/measurement_extensions.rb +10 -9
  38. data/lib/prawn/measurements.rb +19 -15
  39. data/lib/prawn/outline.rb +97 -77
  40. data/lib/prawn/repeater.rb +14 -10
  41. data/lib/prawn/security.rb +81 -61
  42. data/lib/prawn/security/arcfour.rb +2 -2
  43. data/lib/prawn/soft_mask.rb +26 -26
  44. data/lib/prawn/stamp.rb +20 -13
  45. data/lib/prawn/text.rb +68 -52
  46. data/lib/prawn/text/box.rb +11 -8
  47. data/lib/prawn/text/formatted.rb +5 -5
  48. data/lib/prawn/text/formatted/arranger.rb +78 -49
  49. data/lib/prawn/text/formatted/box.rb +134 -100
  50. data/lib/prawn/text/formatted/fragment.rb +11 -14
  51. data/lib/prawn/text/formatted/line_wrap.rb +121 -63
  52. data/lib/prawn/text/formatted/parser.rb +139 -117
  53. data/lib/prawn/text/formatted/wrap.rb +43 -31
  54. data/lib/prawn/transformation_stack.rb +44 -0
  55. data/lib/prawn/utilities.rb +7 -22
  56. data/lib/prawn/version.rb +2 -2
  57. data/lib/prawn/view.rb +17 -7
  58. data/manual/basic_concepts/adding_pages.rb +6 -7
  59. data/manual/basic_concepts/basic_concepts.rb +31 -22
  60. data/manual/basic_concepts/creation.rb +10 -11
  61. data/manual/basic_concepts/cursor.rb +4 -5
  62. data/manual/basic_concepts/measurement.rb +6 -7
  63. data/manual/basic_concepts/origin.rb +5 -6
  64. data/manual/basic_concepts/other_cursor_helpers.rb +11 -12
  65. data/manual/basic_concepts/view.rb +22 -16
  66. data/manual/bounding_box/bounding_box.rb +29 -24
  67. data/manual/bounding_box/bounds.rb +11 -12
  68. data/manual/bounding_box/canvas.rb +4 -5
  69. data/manual/bounding_box/creation.rb +6 -7
  70. data/manual/bounding_box/indentation.rb +14 -15
  71. data/manual/bounding_box/nesting.rb +24 -17
  72. data/manual/bounding_box/russian_boxes.rb +14 -13
  73. data/manual/bounding_box/stretchy.rb +12 -13
  74. data/manual/contents.rb +28 -22
  75. data/manual/cover.rb +33 -28
  76. data/manual/document_and_page_options/background.rb +11 -13
  77. data/manual/document_and_page_options/document_and_page_options.rb +25 -20
  78. data/manual/document_and_page_options/metadata.rb +18 -16
  79. data/manual/document_and_page_options/page_margins.rb +18 -20
  80. data/manual/document_and_page_options/page_size.rb +13 -12
  81. data/manual/document_and_page_options/print_scaling.rb +17 -15
  82. data/manual/example_helper.rb +5 -4
  83. data/manual/graphics/blend_mode.rb +52 -0
  84. data/manual/graphics/circle_and_ellipse.rb +4 -5
  85. data/manual/graphics/color.rb +7 -9
  86. data/manual/graphics/common_lines.rb +7 -8
  87. data/manual/graphics/fill_and_stroke.rb +4 -5
  88. data/manual/graphics/fill_rules.rb +9 -10
  89. data/manual/graphics/gradients.rb +27 -21
  90. data/manual/graphics/graphics.rb +48 -39
  91. data/manual/graphics/helper.rb +12 -9
  92. data/manual/graphics/line_width.rb +8 -7
  93. data/manual/graphics/lines_and_curves.rb +7 -8
  94. data/manual/graphics/polygon.rb +6 -8
  95. data/manual/graphics/rectangle.rb +4 -5
  96. data/manual/graphics/rotate.rb +6 -7
  97. data/manual/graphics/scale.rb +14 -15
  98. data/manual/graphics/soft_masks.rb +4 -5
  99. data/manual/graphics/stroke_cap.rb +6 -7
  100. data/manual/graphics/stroke_dash.rb +11 -12
  101. data/manual/graphics/stroke_join.rb +5 -6
  102. data/manual/graphics/translate.rb +9 -10
  103. data/manual/graphics/transparency.rb +7 -8
  104. data/manual/how_to_read_this_manual.rb +6 -6
  105. data/manual/images/absolute_position.rb +6 -7
  106. data/manual/images/fit.rb +7 -8
  107. data/manual/images/horizontal.rb +9 -10
  108. data/manual/images/images.rb +28 -24
  109. data/manual/images/plain_image.rb +5 -6
  110. data/manual/images/scale.rb +9 -10
  111. data/manual/images/vertical.rb +13 -14
  112. data/manual/images/width_and_height.rb +10 -11
  113. data/manual/layout/boxes.rb +5 -6
  114. data/manual/layout/content.rb +7 -8
  115. data/manual/layout/layout.rb +18 -16
  116. data/manual/layout/simple_grid.rb +6 -7
  117. data/manual/outline/add_subsection_to.rb +20 -21
  118. data/manual/outline/insert_section_after.rb +15 -16
  119. data/manual/outline/outline.rb +21 -17
  120. data/manual/outline/sections_and_pages.rb +17 -18
  121. data/manual/repeatable_content/alternate_page_numbering.rb +21 -17
  122. data/manual/repeatable_content/page_numbering.rb +17 -16
  123. data/manual/repeatable_content/repeatable_content.rb +25 -19
  124. data/manual/repeatable_content/repeater.rb +14 -15
  125. data/manual/repeatable_content/stamp.rb +14 -15
  126. data/manual/security/encryption.rb +9 -10
  127. data/manual/security/permissions.rb +19 -14
  128. data/manual/security/security.rb +19 -16
  129. data/manual/table.rb +3 -3
  130. data/manual/text/alignment.rb +16 -17
  131. data/manual/text/color.rb +12 -11
  132. data/manual/text/column_box.rb +9 -10
  133. data/manual/text/fallback_fonts.rb +25 -21
  134. data/manual/text/font.rb +11 -12
  135. data/manual/text/font_size.rb +13 -14
  136. data/manual/text/font_style.rb +7 -8
  137. data/manual/text/formatted_callbacks.rb +25 -21
  138. data/manual/text/formatted_text.rb +33 -25
  139. data/manual/text/free_flowing_text.rb +20 -21
  140. data/manual/text/inline.rb +18 -19
  141. data/manual/text/kerning_and_character_spacing.rb +14 -15
  142. data/manual/text/leading.rb +7 -8
  143. data/manual/text/line_wrapping.rb +37 -18
  144. data/manual/text/paragraph_indentation.rb +13 -14
  145. data/manual/text/positioned_text.rb +15 -16
  146. data/manual/text/registering_families.rb +20 -21
  147. data/manual/text/rendering_and_color.rb +9 -10
  148. data/manual/text/right_to_left_text.rb +26 -19
  149. data/manual/text/rotation.rb +28 -23
  150. data/manual/text/single_usage.rb +8 -9
  151. data/manual/text/text.rb +57 -52
  152. data/manual/text/text_box_excess.rb +20 -17
  153. data/manual/text/text_box_extensions.rb +18 -15
  154. data/manual/text/text_box_overflow.rb +18 -19
  155. data/manual/text/utf8.rb +11 -12
  156. data/manual/text/win_ansi_charset.rb +21 -19
  157. data/prawn.gemspec +45 -33
  158. data/spec/extensions/encoding_helpers.rb +3 -3
  159. data/spec/prawn/document/bounding_box_spec.rb +546 -0
  160. data/spec/prawn/document/column_box_spec.rb +75 -0
  161. data/spec/prawn/document/security_spec.rb +176 -0
  162. data/spec/prawn/document_annotations_spec.rb +76 -0
  163. data/spec/prawn/document_destinations_spec.rb +15 -0
  164. data/spec/prawn/document_grid_spec.rb +99 -0
  165. data/spec/prawn/document_reference_spec.rb +27 -0
  166. data/spec/prawn/document_span_spec.rb +36 -0
  167. data/spec/prawn/document_spec.rb +802 -0
  168. data/spec/prawn/font_metric_cache_spec.rb +54 -0
  169. data/spec/prawn/font_spec.rb +542 -0
  170. data/spec/prawn/graphics/blend_mode_spec.rb +63 -0
  171. data/spec/prawn/graphics/transparency_spec.rb +81 -0
  172. data/spec/prawn/graphics_spec.rb +837 -0
  173. data/spec/prawn/graphics_stroke_styles_spec.rb +229 -0
  174. data/spec/prawn/image_handler_spec.rb +53 -0
  175. data/spec/prawn/images/jpg_spec.rb +20 -0
  176. data/spec/prawn/images/png_spec.rb +283 -0
  177. data/spec/prawn/images_spec.rb +224 -0
  178. data/spec/prawn/measurements_extensions_spec.rb +24 -0
  179. data/spec/prawn/outline_spec.rb +412 -0
  180. data/spec/prawn/repeater_spec.rb +165 -0
  181. data/spec/prawn/soft_mask_spec.rb +74 -0
  182. data/spec/prawn/stamp_spec.rb +172 -0
  183. data/spec/prawn/text/box_spec.rb +1112 -0
  184. data/spec/prawn/text/formatted/arranger_spec.rb +466 -0
  185. data/spec/prawn/text/formatted/box_spec.rb +846 -0
  186. data/spec/prawn/text/formatted/fragment_spec.rb +343 -0
  187. data/spec/prawn/text/formatted/line_wrap_spec.rb +494 -0
  188. data/spec/prawn/text/formatted/parser_spec.rb +697 -0
  189. data/spec/prawn/text_draw_text_spec.rb +149 -0
  190. data/spec/prawn/text_rendering_mode_spec.rb +48 -0
  191. data/spec/prawn/text_spacing_spec.rb +95 -0
  192. data/spec/prawn/text_spec.rb +603 -0
  193. data/spec/prawn/text_with_inline_formatting_spec.rb +35 -0
  194. data/spec/prawn/transformation_stack_spec.rb +66 -0
  195. data/spec/prawn/view_spec.rb +63 -0
  196. data/spec/prawn_manual_spec.rb +35 -0
  197. data/spec/spec_helper.rb +19 -23
  198. metadata +145 -185
  199. metadata.gz.sig +4 -0
  200. data/data/images/16bit.alpha +0 -0
  201. data/data/images/16bit.color +0 -0
  202. data/data/images/16bit.png +0 -0
  203. data/data/images/arrow.png +0 -0
  204. data/data/images/arrow2.png +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/letterhead.jpg +0 -0
  213. data/data/images/license.md +0 -8
  214. data/data/images/page_white_text.alpha +0 -0
  215. data/data/images/page_white_text.color +0 -0
  216. data/data/images/page_white_text.png +0 -0
  217. data/data/images/pal_bk.png +0 -0
  218. data/data/images/pigs.jpg +0 -0
  219. data/data/images/prawn.png +0 -0
  220. data/data/images/ruport.png +0 -0
  221. data/data/images/ruport_data.dat +0 -0
  222. data/data/images/ruport_transparent.png +0 -0
  223. data/data/images/ruport_type0.png +0 -0
  224. data/data/images/stef.jpg +0 -0
  225. data/data/images/tru256.bmp +0 -0
  226. data/data/images/web-links.dat +0 -1
  227. data/data/images/web-links.png +0 -0
  228. data/data/pdfs/complex_template.pdf +0 -0
  229. data/data/pdfs/contains_ttf_font.pdf +0 -0
  230. data/data/pdfs/encrypted.pdf +0 -0
  231. data/data/pdfs/form.pdf +1 -819
  232. data/data/pdfs/hexagon.pdf +0 -61
  233. data/data/pdfs/indirect_reference.pdf +0 -86
  234. data/data/pdfs/multipage_template.pdf +0 -127
  235. data/data/pdfs/nested_pages.pdf +0 -118
  236. data/data/pdfs/page_without_mediabox.pdf +0 -193
  237. data/data/pdfs/resources_as_indirect_object.pdf +0 -83
  238. data/data/pdfs/two_hexagons.pdf +0 -90
  239. data/data/pdfs/version_1_6.pdf +0 -61
  240. data/data/shift_jis_text.txt +0 -1
  241. data/spec/acceptance/png.rb +0 -24
  242. data/spec/annotations_spec.rb +0 -67
  243. data/spec/bounding_box_spec.rb +0 -501
  244. data/spec/column_box_spec.rb +0 -59
  245. data/spec/destinations_spec.rb +0 -13
  246. data/spec/document_spec.rb +0 -742
  247. data/spec/extensions/mocha.rb +0 -45
  248. data/spec/font_metric_cache_spec.rb +0 -52
  249. data/spec/font_spec.rb +0 -475
  250. data/spec/formatted_text_arranger_spec.rb +0 -423
  251. data/spec/formatted_text_box_spec.rb +0 -716
  252. data/spec/formatted_text_fragment_spec.rb +0 -299
  253. data/spec/graphics_spec.rb +0 -666
  254. data/spec/grid_spec.rb +0 -95
  255. data/spec/image_handler_spec.rb +0 -53
  256. data/spec/images_spec.rb +0 -167
  257. data/spec/inline_formatted_text_parser_spec.rb +0 -568
  258. data/spec/jpg_spec.rb +0 -23
  259. data/spec/line_wrap_spec.rb +0 -366
  260. data/spec/measurement_units_spec.rb +0 -22
  261. data/spec/outline_spec.rb +0 -409
  262. data/spec/png_spec.rb +0 -235
  263. data/spec/reference_spec.rb +0 -25
  264. data/spec/repeater_spec.rb +0 -154
  265. data/spec/security_spec.rb +0 -151
  266. data/spec/soft_mask_spec.rb +0 -78
  267. data/spec/span_spec.rb +0 -43
  268. data/spec/stamp_spec.rb +0 -179
  269. data/spec/stroke_styles_spec.rb +0 -208
  270. data/spec/text_at_spec.rb +0 -142
  271. data/spec/text_box_spec.rb +0 -1038
  272. data/spec/text_rendering_mode_spec.rb +0 -45
  273. data/spec/text_spacing_spec.rb +0 -93
  274. data/spec/text_spec.rb +0 -549
  275. data/spec/text_with_inline_formatting_spec.rb +0 -35
  276. data/spec/transparency_spec.rb +0 -91
  277. data/spec/view_spec.rb +0 -42
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Repeater do
6
+ it 'creates a stamp and increments Prawn::Repeater.count on initialize' do
7
+ orig_count = described_class.count
8
+
9
+ doc = sample_document
10
+ allow(doc).to receive(:create_stamp).with("prawn_repeater(#{orig_count})")
11
+
12
+ repeater(doc, :all) { :do_nothing }
13
+
14
+ expect(doc).to have_received(:create_stamp)
15
+ .with("prawn_repeater(#{orig_count})")
16
+
17
+ expect(described_class.count).to eq(orig_count + 1)
18
+ end
19
+
20
+ it 'must provide an :all filter' do
21
+ doc = sample_document
22
+ r = repeater(doc, :all) { :do_nothing }
23
+
24
+ expect((1..doc.page_count).all? { |i| r.match?(i) }).to eq true
25
+ end
26
+
27
+ it 'must provide an :odd filter' do
28
+ doc = sample_document
29
+ r = repeater(doc, :odd) { :do_nothing }
30
+
31
+ odd, even = (1..doc.page_count).partition(&:odd?)
32
+
33
+ expect(odd.all? { |i| r.match?(i) }).to eq true
34
+ expect(even.any? { |i| r.match?(i) }).to eq false
35
+ end
36
+
37
+ it 'must be able to filter by an array of page numbers' do
38
+ doc = sample_document
39
+ r = repeater(doc, [1, 2, 7]) { :do_nothing }
40
+
41
+ expect((1..10).select { |i| r.match?(i) }).to eq([1, 2, 7])
42
+ end
43
+
44
+ it 'must be able to filter by a range of page numbers' do
45
+ doc = sample_document
46
+ r = repeater(doc, 2..4) { :do_nothing }
47
+
48
+ expect((1..10).select { |i| r.match?(i) }).to eq([2, 3, 4])
49
+ end
50
+
51
+ it 'must be able to filter by an arbitrary proc' do
52
+ doc = sample_document
53
+ r = repeater(doc, ->(x) { x == 1 || x % 3 == 0 })
54
+
55
+ expect((1..10).select { |i| r.match?(i) }).to eq([1, 3, 6, 9])
56
+ end
57
+
58
+ it 'must try to run a stamp if the page number matches' do
59
+ doc = sample_document
60
+ allow(doc).to receive(:stamp)
61
+
62
+ repeater(doc, :odd).run(3)
63
+ expect(doc).to have_received(:stamp)
64
+ end
65
+
66
+ it 'must not try to run a stamp unless the page number matches' do
67
+ doc = sample_document
68
+
69
+ allow(doc).to receive(:stamp)
70
+ repeater(doc, :odd).run(2)
71
+ expect(doc).to_not have_received(:stamp)
72
+ end
73
+
74
+ it 'must not try to run a stamp if dynamic is selected' do
75
+ doc = sample_document
76
+
77
+ allow(doc).to receive(:stamp)
78
+ (1..10).each { |p| repeater(doc, :all, true) { :do_nothing }.run(p) }
79
+ expect(doc).to_not have_received(:stamp)
80
+ end
81
+
82
+ it 'must try to run a block if the page number matches' do
83
+ doc = sample_document
84
+
85
+ allow(doc).to receive(:draw_text)
86
+ (1..10).each do |p|
87
+ repeater(doc, [1, 2], true) { doc.draw_text 'foo' }.run(p)
88
+ end
89
+ expect(doc).to have_received(:draw_text).twice
90
+ end
91
+
92
+ it 'must not try to run a block unless the page number matches' do
93
+ doc = sample_document
94
+
95
+ allow(doc).to receive(:draw_text)
96
+ repeater(doc, :odd, true) { doc.draw_text 'foo' }.run(2)
97
+ expect(doc).to_not have_received(:draw_text)
98
+ end
99
+
100
+ it 'must treat any block as a closure' do
101
+ doc = sample_document
102
+
103
+ page = 'Page' # ensure access to ivars
104
+ doc.repeat(:all, dynamic: true) do
105
+ doc.draw_text "#{page} #{doc.page_number}", at: [500, 0]
106
+ end
107
+
108
+ text = PDF::Inspector::Text.analyze(doc.render)
109
+ expect(text.strings).to eq((1..10).to_a.map { |p| "Page #{p}" })
110
+ end
111
+
112
+ it 'must treat any block as a closure (Document.new instance_eval form)' do
113
+ doc = Prawn::Document.new(skip_page_creation: true) do
114
+ 10.times { start_new_page }
115
+
116
+ page = 'Page'
117
+ repeat(:all, dynamic: true) do
118
+ # ensure self is accessible here
119
+ draw_text "#{page} #{page_number}", at: [500, 0]
120
+ end
121
+ end
122
+
123
+ text = PDF::Inspector::Text.analyze(doc.render)
124
+ expect(text.strings).to eq((1..10).to_a.map { |p| "Page #{p}" })
125
+ end
126
+
127
+ def sample_document
128
+ doc = Prawn::Document.new(skip_page_creation: true)
129
+ 10.times { |_e| doc.start_new_page }
130
+ doc
131
+ end
132
+
133
+ def repeater(*args, &block)
134
+ Prawn::Repeater.new(*args, &block)
135
+ end
136
+
137
+ describe 'graphic state' do
138
+ let(:pdf) { create_pdf }
139
+
140
+ it 'does not alter the graphic state stack color space' do
141
+ starting_color_space = pdf.state.page.graphic_state.color_space.dup
142
+ pdf.repeat :all do
143
+ pdf.text 'Testing', size: 24, style: :bold
144
+ end
145
+ expect(pdf.state.page.graphic_state.color_space)
146
+ .to eq(starting_color_space)
147
+ end
148
+
149
+ context 'with dynamic repeaters' do
150
+ it 'preserves the graphic state at creation time' do
151
+ pdf.repeat :all, dynamic: true do
152
+ pdf.text "fill_color: #{pdf.graphic_state.fill_color}"
153
+ pdf.text "cap_style: #{pdf.graphic_state.cap_style}"
154
+ end
155
+ pdf.fill_color '666666'
156
+ pdf.cap_style :round
157
+ text = PDF::Inspector::Text.analyze(pdf.render)
158
+ expect(text.strings.include?('fill_color: 666666')).to eq(false)
159
+ expect(text.strings.include?('fill_color: 000000')).to eq(true)
160
+ expect(text.strings.include?('cap_style: round')).to eq(false)
161
+ expect(text.strings.include?('cap_style: butt')).to eq(true)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::SoftMask do
6
+ let(:pdf) { create_pdf }
7
+
8
+ def make_soft_mask
9
+ pdf.save_graphics_state do
10
+ pdf.soft_mask do
11
+ if block_given?
12
+ yield
13
+ else
14
+ pdf.fill_color '808080'
15
+ pdf.fill_rectangle [100, 100], 200, 200
16
+ end
17
+ end
18
+
19
+ pdf.fill_color '000000'
20
+ pdf.fill_rectangle [0, 0], 200, 200
21
+ end
22
+ end
23
+
24
+ it 'has PDF version at least 1.4' do
25
+ make_soft_mask
26
+ str = pdf.render
27
+ expect(str[0, 8]).to eq('%PDF-1.4')
28
+ end
29
+
30
+ it 'creates a new extended graphics state for each unique soft mask' do
31
+ make_soft_mask do
32
+ pdf.fill_color '808080'
33
+ pdf.fill_rectangle [100, 100], 200, 200
34
+ end
35
+
36
+ make_soft_mask do
37
+ pdf.fill_color '808080'
38
+ pdf.fill_rectangle [10, 10], 200, 200
39
+ end
40
+
41
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
42
+ expect(extgstates.length).to eq(2)
43
+ end
44
+
45
+ it 'a new extended graphics state contains soft mask with drawing '\
46
+ 'instructions' do
47
+ make_soft_mask do
48
+ pdf.fill_color '808080'
49
+ pdf.fill_rectangle [100, 100], 200, 200
50
+ end
51
+
52
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates.first
53
+ expect(extgstate[:soft_mask][:G].data).to eq(
54
+ "q\n/DeviceRGB cs\n0.0 0.0 0.0 scn\n/DeviceRGB CS\n0.0 0.0 0.0 SCN\n"\
55
+ "1 w\n0 J\n0 j\n[] 0 d\n/DeviceRGB cs\n0.502 0.502 0.502 scn\n"\
56
+ "100.0 -100.0 200.0 200.0 re\nf\nQ\n"
57
+ )
58
+ end
59
+
60
+ it 'does not create duplicate extended graphics states' do
61
+ make_soft_mask do
62
+ pdf.fill_color '808080'
63
+ pdf.fill_rectangle [100, 100], 200, 200
64
+ end
65
+
66
+ make_soft_mask do
67
+ pdf.fill_color '808080'
68
+ pdf.fill_rectangle [100, 100], 200, 200
69
+ end
70
+
71
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
72
+ expect(extgstates.length).to eq(1)
73
+ end
74
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Stamp do
6
+ describe 'create_stamp before any page is added' do
7
+ let(:pdf) { Prawn::Document.new(skip_page_creation: true) }
8
+
9
+ it 'works with the font class' do
10
+ # If anything goes wrong, Prawn::Errors::NotOnPage will be raised
11
+ pdf.create_stamp('my_stamp') do
12
+ pdf.font.height
13
+ end
14
+ end
15
+
16
+ it 'works with setting color' do
17
+ # If anything goes wrong, Prawn::Errors::NotOnPage will be raised
18
+ pdf.create_stamp('my_stamp') do
19
+ pdf.fill_color = 'ff0000'
20
+ end
21
+ end
22
+ end
23
+
24
+ describe '#stamp_at' do
25
+ let(:pdf) { create_pdf }
26
+
27
+ it 'works' do
28
+ pdf.create_stamp('MyStamp')
29
+ pdf.stamp_at('MyStamp', [100, 200])
30
+ # I had modified PDF::Inspector::XObject to receive the
31
+ # invoke_xobject message and count the number of times it was
32
+ # called, but it was only called once, so I reverted checking the
33
+ # output with a regular expression
34
+ expect(pdf.render).to match(%r{/Stamp1 Do.*?}m)
35
+ end
36
+ end
37
+
38
+ describe 'Document with a stamp' do
39
+ let(:pdf) { create_pdf }
40
+
41
+ it 'raises NameTaken error when attempt to create stamp with '\
42
+ 'same name as an existing stamp' do
43
+ pdf.create_stamp('MyStamp')
44
+ expect do
45
+ pdf.create_stamp('MyStamp')
46
+ end.to raise_error(Prawn::Errors::NameTaken)
47
+ end
48
+
49
+ it 'raises InvalidName error when attempt to create stamp with '\
50
+ 'a blank name' do
51
+ expect do
52
+ pdf.create_stamp('')
53
+ end.to raise_error(Prawn::Errors::InvalidName)
54
+ end
55
+
56
+ it 'a new XObject should be defined for each stamp created' do
57
+ pdf.create_stamp('MyStamp')
58
+ pdf.create_stamp('AnotherStamp')
59
+ pdf.stamp('MyStamp')
60
+ pdf.stamp('AnotherStamp')
61
+
62
+ inspector = PDF::Inspector::XObject.analyze(pdf.render)
63
+ xobjects = inspector.page_xobjects.last
64
+ expect(xobjects.length).to eq(2)
65
+ end
66
+
67
+ it 'calling stamp with a name that does not match an existing stamp ' \
68
+ 'should raise_error UndefinedObjectName' do
69
+ pdf.create_stamp('MyStamp')
70
+ expect do
71
+ pdf.stamp('OtherStamp')
72
+ end.to raise_error(Prawn::Errors::UndefinedObjectName)
73
+ end
74
+
75
+ it 'stamp should be drawn into the document each time stamp is called' do
76
+ pdf.create_stamp('MyStamp')
77
+ pdf.stamp('MyStamp')
78
+ pdf.stamp('MyStamp')
79
+ pdf.stamp('MyStamp')
80
+ # I had modified PDF::Inspector::XObject to receive the
81
+ # invoke_xobject message and count the number of times it was
82
+ # called, but it was only called once, so I reverted checking the
83
+ # output with a regular expression
84
+ expect(pdf.render).to match(%r{(/Stamp1 Do.*?){3}}m)
85
+ end
86
+
87
+ it 'stamp should render clickable links' do
88
+ pdf.create_stamp 'bar' do
89
+ pdf.text '<b>Prawn</b> <link href="http://github.com">GitHub</link>',
90
+ inline_format: true
91
+ end
92
+ pdf.stamp 'bar'
93
+
94
+ output = pdf.render
95
+ objects = output.split('endobj')
96
+
97
+ objects.each do |obj|
98
+ next unless %r{/Type /Page$}.match?(obj)
99
+
100
+ # The page object must contain the annotation reference
101
+ # to render a clickable link
102
+ expect(obj).to match(%r{^/Annots \[\d \d .\]$})
103
+ end
104
+ end
105
+
106
+ it 'resources added during stamp creation should be added to the ' \
107
+ 'stamp XObject, not the page' do
108
+ pdf.create_stamp('MyStamp') do
109
+ pdf.transparent(0.5) { pdf.circle([100, 100], 10) }
110
+ end
111
+ pdf.stamp('MyStamp')
112
+
113
+ # Inspector::XObject does not give information about resources, so
114
+ # resorting to string matching
115
+
116
+ output = pdf.render
117
+ objects = output.split('endobj')
118
+ objects.each do |object|
119
+ if %r{/Type /Page$}.match?(object)
120
+ expect(object).to_not match(%r{/ExtGState})
121
+ elsif %r{/Type /XObject$}.match?(object)
122
+ expect(object).to match(%r{/ExtGState})
123
+ end
124
+ end
125
+ end
126
+
127
+ it 'stamp stream should be wrapped in a graphic state' do
128
+ pdf.create_stamp('MyStamp') do
129
+ pdf.text "This should have a 'q' before it and a 'Q' after it"
130
+ end
131
+ pdf.stamp('MyStamp')
132
+ stamps = PDF::Inspector::XObject.analyze(pdf.render)
133
+ expect(stamps.xobject_streams[:Stamp1].data.chomp).to match(/q(.|\s)*Q\Z/)
134
+ end
135
+
136
+ it 'does not add to the page graphic state stack' do
137
+ expect(pdf.state.page.stack.stack.size).to eq(1)
138
+
139
+ pdf.create_stamp('MyStamp') do
140
+ pdf.save_graphics_state
141
+ pdf.save_graphics_state
142
+ pdf.save_graphics_state
143
+ pdf.text "This should have a 'q' before it and a 'Q' after it"
144
+ pdf.restore_graphics_state
145
+ end
146
+ expect(pdf.state.page.stack.stack.size).to eq(1)
147
+ end
148
+
149
+ it 'is able to change fill and stroke colors within the stamp stream' do
150
+ pdf.create_stamp('MyStamp') do
151
+ pdf.fill_color(100, 100, 20, 0)
152
+ pdf.stroke_color(100, 100, 20, 0)
153
+ end
154
+ pdf.stamp('MyStamp')
155
+ stamps = PDF::Inspector::XObject.analyze(pdf.render)
156
+ stamp_stream = stamps.xobject_streams[:Stamp1].data
157
+ expect(stamp_stream).to include("/DeviceCMYK cs\n1.0 1.0 0.2 0.0 scn")
158
+ expect(stamp_stream).to include("/DeviceCMYK CS\n1.0 1.0 0.2 0.0 SCN")
159
+ end
160
+
161
+ it 'saves the color space even when same as current page color space' do
162
+ pdf.stroke_color(100, 100, 20, 0)
163
+ pdf.create_stamp('MyStamp') do
164
+ pdf.stroke_color(100, 100, 20, 0)
165
+ end
166
+ pdf.stamp('MyStamp')
167
+ stamps = PDF::Inspector::XObject.analyze(pdf.render)
168
+ stamp_stream = stamps.xobject_streams[:Stamp1].data
169
+ expect(stamp_stream).to include("/DeviceCMYK CS\n1.0 1.0 0.2 0.0 SCN")
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,1112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Text::Box do
6
+ let(:pdf) { create_pdf }
7
+
8
+ it 'is able to set leading document-wide' do
9
+ pdf.default_leading(7)
10
+ pdf.default_leading = 7
11
+ text_box = described_class.new('hello world', document: pdf)
12
+ expect(text_box.leading).to eq(7)
13
+ end
14
+
15
+ it 'option should be able to override document-wide leading' do
16
+ pdf.default_leading = 7
17
+ text_box = described_class.new(
18
+ 'hello world',
19
+ document: pdf,
20
+ leading: 20
21
+ )
22
+ expect(text_box.leading).to eq(20)
23
+ end
24
+
25
+ it 'is able to set text direction document-wide' do
26
+ pdf.text_direction(:rtl)
27
+ pdf.text_direction = :rtl
28
+ string = "Hello world, how are you?\nI'm fine, thank you."
29
+ text_box = described_class.new(string, document: pdf)
30
+ text_box.render
31
+ text = PDF::Inspector::Text.analyze(pdf.render)
32
+ expect(text.strings[0]).to eq('?uoy era woh ,dlrow olleH')
33
+ expect(text.strings[1]).to eq(".uoy knaht ,enif m'I")
34
+ end
35
+
36
+ it 'is able to reverse multi-byte text' do
37
+ pdf.text_direction(:rtl)
38
+ pdf.text_direction = :rtl
39
+ pdf.text_direction = :rtl
40
+ pdf.font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf", size: 16) do
41
+ pdf.text '写个小'
42
+ end
43
+ text = PDF::Inspector::Text.analyze(pdf.render)
44
+ expect(text.strings[0]).to eq('小个写')
45
+ end
46
+
47
+ it 'option should be able to override document-wide text direction' do
48
+ pdf.text_direction = :rtl
49
+ string = "Hello world, how are you?\nI'm fine, thank you."
50
+ text_box = described_class.new(
51
+ string,
52
+ document: pdf,
53
+ direction: :ltr
54
+ )
55
+ text_box.render
56
+ text = PDF::Inspector::Text.analyze(pdf.render)
57
+ expect(text.strings[0]).to eq('Hello world, how are you?')
58
+ expect(text.strings[1]).to eq("I'm fine, thank you.")
59
+ end
60
+
61
+ it 'only requires enough space for the descender and the ascender '\
62
+ 'when determining whether a line can fit' do
63
+ text = 'Oh hai text rect'
64
+ options = {
65
+ document: pdf,
66
+ height: pdf.font.ascender + pdf.font.descender
67
+ }
68
+ text_box = described_class.new(text, options)
69
+ text_box.render
70
+ expect(text_box.text).to eq('Oh hai text rect')
71
+
72
+ text = "Oh hai text rect\nOh hai text rect"
73
+ options = {
74
+ document: pdf,
75
+ height: pdf.font.height + pdf.font.ascender + pdf.font.descender
76
+ }
77
+ text_box = described_class.new(text, options)
78
+ text_box.render
79
+ expect(text_box.text).to eq("Oh hai text rect\nOh hai text rect")
80
+ end
81
+
82
+ describe '#nothing_printed?' do
83
+ it 'returns true when nothing printed' do
84
+ string = "Hello world, how are you?\nI'm fine, thank you."
85
+ text_box = described_class.new(string, height: 2, document: pdf)
86
+ text_box.render
87
+ expect(text_box.nothing_printed?).to eq true
88
+ end
89
+
90
+ it 'returns false when something printed' do
91
+ string = "Hello world, how are you?\nI'm fine, thank you."
92
+ text_box = described_class.new(string, height: 14, document: pdf)
93
+ text_box.render
94
+ expect(text_box.nothing_printed?).to eq false
95
+ end
96
+ end
97
+
98
+ describe '#everything_printed?' do
99
+ it 'returns false when not everything printed' do
100
+ string = "Hello world, how are you?\nI'm fine, thank you."
101
+ text_box = described_class.new(string, height: 14, document: pdf)
102
+ text_box.render
103
+ expect(text_box.everything_printed?).to eq false
104
+ end
105
+
106
+ it 'returns true when everything printed' do
107
+ string = "Hello world, how are you?\nI'm fine, thank you."
108
+ text_box = described_class.new(string, document: pdf)
109
+ text_box.render
110
+ expect(text_box.everything_printed?).to eq true
111
+ end
112
+ end
113
+
114
+ describe '#line_gap' do
115
+ it '==S the line gap of the font when using a single font and font size' do
116
+ string = "Hello world, how are you?\nI'm fine, thank you."
117
+ text_box = described_class.new(string, document: pdf)
118
+ text_box.render
119
+ expect(text_box.line_gap).to be_within(0.0001).of(pdf.font.line_gap)
120
+ end
121
+ end
122
+
123
+ describe '#render with :align => :justify' do
124
+ it 'draws the word spacing to the document' do
125
+ string = 'hello world ' * 20
126
+ options = { document: pdf, align: :justify }
127
+ text_box = described_class.new(string, options)
128
+ text_box.render
129
+ contents = PDF::Inspector::Text.analyze(pdf.render)
130
+ expect(contents.word_spacing[0]).to be > 0
131
+ end
132
+
133
+ it 'does not justify the last line of a paragraph' do
134
+ string = 'hello world '
135
+ options = { document: pdf, align: :justify }
136
+ text_box = described_class.new(string, options)
137
+ text_box.render
138
+ contents = PDF::Inspector::Text.analyze(pdf.render)
139
+ expect(contents.word_spacing).to be_empty
140
+ end
141
+ end
142
+
143
+ describe '#height without leading' do
144
+ it 'is the sum of the height of each line, not including the space below '\
145
+ 'the last line' do
146
+ text = "Oh hai text rect.\nOh hai text rect."
147
+ options = { document: pdf }
148
+ text_box = described_class.new(text, options)
149
+ text_box.render
150
+ expect(text_box.height).to be_within(0.001)
151
+ .of(pdf.font.height * 2 - pdf.font.line_gap)
152
+ end
153
+ end
154
+
155
+ describe '#height with leading' do
156
+ it 'is the sum of the height of each line plus leading, but not including '\
157
+ 'the space below the last line' do
158
+ text = "Oh hai text rect.\nOh hai text rect."
159
+ leading = 12
160
+ options = { document: pdf, leading: leading }
161
+ text_box = described_class.new(text, options)
162
+ text_box.render
163
+ expect(text_box.height).to be_within(0.001).of(
164
+ (pdf.font.height + leading) * 2 - pdf.font.line_gap - leading
165
+ )
166
+ end
167
+ end
168
+
169
+ context 'with :draw_text_callback' do
170
+ it 'hits the callback whenever text is drawn' do
171
+ draw_block = instance_spy('Draw block')
172
+
173
+ pdf.text_box 'this text is long enough to span two lines',
174
+ width: 150,
175
+ draw_text_callback: ->(text, _) { draw_block.kick(text) }
176
+
177
+ expect(draw_block).to have_received(:kick)
178
+ .with('this text is long enough to')
179
+ expect(draw_block).to have_received(:kick).with('span two lines')
180
+ end
181
+
182
+ it 'hits the callback once per fragment for :inline_format' do
183
+ draw_block = instance_spy('Draw block')
184
+
185
+ pdf.text_box 'this text has <b>fancy</b> formatting',
186
+ inline_format: true, width: 500,
187
+ draw_text_callback: ->(text, _) { draw_block.kick(text) }
188
+
189
+ expect(draw_block).to have_received(:kick).with('this text has ')
190
+ expect(draw_block).to have_received(:kick).with('fancy')
191
+ expect(draw_block).to have_received(:kick).with(' formatting')
192
+ end
193
+
194
+ it 'does not call #draw_text!' do
195
+ allow(pdf).to receive(:draw_text!)
196
+ pdf.text_box 'some text', width: 500,
197
+ draw_text_callback: ->(_, _) {}
198
+ expect(pdf).to_not have_received(:draw_text!)
199
+ end
200
+ end
201
+
202
+ describe '#valid_options' do
203
+ it 'returns an array' do
204
+ text_box = described_class.new('', document: pdf)
205
+ expect(text_box.valid_options).to be_a_kind_of(Array)
206
+ end
207
+ end
208
+
209
+ describe '#render' do
210
+ it 'does not fail if height is smaller than 1 line' do
211
+ text = 'Oh hai text rect. ' * 10
212
+ options = {
213
+ height: pdf.font.height * 0.5,
214
+ document: pdf
215
+ }
216
+ text_box = described_class.new(text, options)
217
+ text_box.render
218
+ expect(text_box.text).to eq('')
219
+ end
220
+
221
+ it 'draws content to the page' do
222
+ text = 'Oh hai text rect. ' * 10
223
+ options = { document: pdf }
224
+ text_box = described_class.new(text, options)
225
+ text_box.render
226
+ text = PDF::Inspector::Text.analyze(pdf.render)
227
+ expect(text.strings).to_not be_empty
228
+ end
229
+
230
+ it 'does not draw a transformation matrix' do
231
+ text = 'Oh hai text rect. ' * 10
232
+ options = { document: pdf }
233
+ text_box = described_class.new(text, options)
234
+ text_box.render
235
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
236
+ expect(matrices.matrices.length).to eq(0)
237
+ end
238
+ end
239
+
240
+ describe '#render(:single_line => true)' do
241
+ it 'draws only one line to the page' do
242
+ text = 'Oh hai text rect. ' * 10
243
+ options = {
244
+ document: pdf,
245
+ single_line: true
246
+ }
247
+ text_box = described_class.new(text, options)
248
+ text_box.render
249
+ text = PDF::Inspector::Text.analyze(pdf.render)
250
+ expect(text.strings.length).to eq(1)
251
+ end
252
+ end
253
+
254
+ describe '#render(:dry_run => true)' do
255
+ it 'does not draw any content to the page' do
256
+ text = 'Oh hai text rect. ' * 10
257
+ options = { document: pdf }
258
+ text_box = described_class.new(text, options)
259
+ text_box.render(dry_run: true)
260
+ text = PDF::Inspector::Text.analyze(pdf.render)
261
+ expect(text.strings).to be_empty
262
+ end
263
+
264
+ it 'subsequent calls to render do not raise an ArgumentError exception' do
265
+ text = '™©'
266
+ options = { document: pdf }
267
+ text_box = described_class.new(text, options)
268
+ text_box.render(dry_run: true)
269
+
270
+ expect do
271
+ text_box.render
272
+ end.to_not raise_exception
273
+ end
274
+ end
275
+
276
+ describe '#render(:valign => :bottom)' do
277
+ it '#at should be the same from one dry run to the next' do
278
+ text = 'this is center text ' * 12
279
+ options = {
280
+ width: 162,
281
+ valign: :bottom,
282
+ document: pdf
283
+ }
284
+ text_box = described_class.new(text, options)
285
+
286
+ text_box.render(dry_run: true)
287
+ original_at = text_box.at.dup
288
+
289
+ text_box.render(dry_run: true)
290
+ expect(text_box.at).to eq(original_at)
291
+ end
292
+ end
293
+
294
+ describe '#render(:valign => :center)' do
295
+ it '#at should be the same from one dry run to the next' do
296
+ text = 'this is center text ' * 12
297
+ options = {
298
+ width: 162,
299
+ valign: :center,
300
+ document: pdf
301
+ }
302
+ text_box = described_class.new(text, options)
303
+
304
+ text_box.render(dry_run: true)
305
+ original_at = text_box.at.dup
306
+
307
+ text_box.render(dry_run: true)
308
+ expect(text_box.at).to eq(original_at)
309
+ end
310
+ end
311
+
312
+ describe '#render with :rotate option of 30)' do
313
+ let(:angle) { 30 }
314
+ let(:x) { 300 }
315
+ let(:y) { 70 }
316
+ let(:width) { 100 }
317
+ let(:height) { 50 }
318
+ let(:cos) { Math.cos(angle * Math::PI / 180) }
319
+ let(:sin) { Math.sin(angle * Math::PI / 180) }
320
+ let(:text) { 'Oh hai text rect. ' * 10 }
321
+ let(:options) do
322
+ {
323
+ document: pdf,
324
+ rotate: angle,
325
+ at: [x, y],
326
+ width: width,
327
+ height: height
328
+ }
329
+ end
330
+
331
+ context 'with :rotate_around option of :center' do
332
+ it 'draws content to the page rotated about the center of the text' do
333
+ options[:rotate_around] = :center
334
+ text_box = described_class.new(text, options)
335
+ text_box.render
336
+
337
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
338
+ x_ = x + width / 2
339
+ y_ = y - height / 2
340
+ x_prime = x_ * cos - y_ * sin
341
+ y_prime = x_ * sin + y_ * cos
342
+ expect(matrices.matrices[0]).to eq([
343
+ 1, 0, 0, 1,
344
+ reduce_precision(x_ - x_prime),
345
+ reduce_precision(y_ - y_prime)
346
+ ])
347
+ expect(matrices.matrices[1]).to eq([
348
+ reduce_precision(cos),
349
+ reduce_precision(sin),
350
+ reduce_precision(-sin),
351
+ reduce_precision(cos),
352
+ 0, 0
353
+ ])
354
+
355
+ text = PDF::Inspector::Text.analyze(pdf.render)
356
+ expect(text.strings).to_not be_empty
357
+ end
358
+ end
359
+
360
+ context 'with :rotate_around option of :upper_left' do
361
+ it 'draws content to the page rotated about the upper left corner of '\
362
+ 'the text' do
363
+ options[:rotate_around] = :upper_left
364
+ text_box = described_class.new(text, options)
365
+ text_box.render
366
+
367
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
368
+ x_prime = x * cos - y * sin
369
+ y_prime = x * sin + y * cos
370
+ expect(matrices.matrices[0]).to eq([
371
+ 1, 0, 0, 1,
372
+ reduce_precision(x - x_prime),
373
+ reduce_precision(y - y_prime)
374
+ ])
375
+ expect(matrices.matrices[1]).to eq([
376
+ reduce_precision(cos),
377
+ reduce_precision(sin),
378
+ reduce_precision(-sin),
379
+ reduce_precision(cos),
380
+ 0, 0
381
+ ])
382
+
383
+ text = PDF::Inspector::Text.analyze(pdf.render)
384
+ expect(text.strings).to_not be_empty
385
+ end
386
+ end
387
+
388
+ context 'with default :rotate_around' do
389
+ it 'draws content to the page rotated about the upper left corner of '\
390
+ 'the text' do
391
+ text_box = described_class.new(text, options)
392
+ text_box.render
393
+
394
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
395
+ x_prime = x * cos - y * sin
396
+ y_prime = x * sin + y * cos
397
+ expect(matrices.matrices[0]).to eq([
398
+ 1, 0, 0, 1,
399
+ reduce_precision(x - x_prime),
400
+ reduce_precision(y - y_prime)
401
+ ])
402
+ expect(matrices.matrices[1]).to eq([
403
+ reduce_precision(cos),
404
+ reduce_precision(sin),
405
+ reduce_precision(-sin),
406
+ reduce_precision(cos),
407
+ 0, 0
408
+ ])
409
+
410
+ text = PDF::Inspector::Text.analyze(pdf.render)
411
+ expect(text.strings).to_not be_empty
412
+ end
413
+ end
414
+
415
+ context 'with :rotate_around option of :upper_right' do
416
+ it 'draws content to the page rotated about the upper right corner of '\
417
+ 'the text' do
418
+ options[:rotate_around] = :upper_right
419
+ text_box = described_class.new(text, options)
420
+ text_box.render
421
+
422
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
423
+ x_ = x + width
424
+ y_ = y
425
+ x_prime = x_ * cos - y_ * sin
426
+ y_prime = x_ * sin + y_ * cos
427
+ expect(matrices.matrices[0]).to eq([
428
+ 1, 0, 0, 1,
429
+ reduce_precision(x_ - x_prime),
430
+ reduce_precision(y_ - y_prime)
431
+ ])
432
+ expect(matrices.matrices[1]).to eq([
433
+ reduce_precision(cos),
434
+ reduce_precision(sin),
435
+ reduce_precision(-sin),
436
+ reduce_precision(cos),
437
+ 0, 0
438
+ ])
439
+
440
+ text = PDF::Inspector::Text.analyze(pdf.render)
441
+ expect(text.strings).to_not be_empty
442
+ end
443
+ end
444
+
445
+ context 'with :rotate_around option of :lower_right' do
446
+ it 'draws content to the page rotated about the lower right corner of '\
447
+ 'the text' do
448
+ options[:rotate_around] = :lower_right
449
+ text_box = described_class.new(text, options)
450
+ text_box.render
451
+
452
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
453
+ x_ = x + width
454
+ y_ = y - height
455
+ x_prime = x_ * cos - y_ * sin
456
+ y_prime = x_ * sin + y_ * cos
457
+ expect(matrices.matrices[0]).to eq([
458
+ 1, 0, 0, 1,
459
+ reduce_precision(x_ - x_prime),
460
+ reduce_precision(y_ - y_prime)
461
+ ])
462
+ expect(matrices.matrices[1]).to eq([
463
+ reduce_precision(cos),
464
+ reduce_precision(sin),
465
+ reduce_precision(-sin),
466
+ reduce_precision(cos),
467
+ 0, 0
468
+ ])
469
+
470
+ text = PDF::Inspector::Text.analyze(pdf.render)
471
+ expect(text.strings).to_not be_empty
472
+ end
473
+ end
474
+
475
+ context 'with :rotate_around option of :lower_left' do
476
+ it 'draws content to the page rotated about the lower left corner of '\
477
+ 'the text' do
478
+ options[:rotate_around] = :lower_left
479
+ text_box = described_class.new(text, options)
480
+ text_box.render
481
+
482
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
483
+ x_ = x
484
+ y_ = y - height
485
+ x_prime = x_ * cos - y_ * sin
486
+ y_prime = x_ * sin + y_ * cos
487
+ expect(matrices.matrices[0]).to eq([
488
+ 1, 0, 0, 1,
489
+ reduce_precision(x_ - x_prime),
490
+ reduce_precision(y_ - y_prime)
491
+ ])
492
+ expect(matrices.matrices[1]).to eq([
493
+ reduce_precision(cos),
494
+ reduce_precision(sin),
495
+ reduce_precision(-sin),
496
+ reduce_precision(cos),
497
+ 0, 0
498
+ ])
499
+
500
+ text = PDF::Inspector::Text.analyze(pdf.render)
501
+ expect(text.strings).to_not be_empty
502
+ end
503
+ end
504
+ end
505
+
506
+ describe 'default height' do
507
+ it 'is the height from the bottom bound to document.y' do
508
+ target_height = pdf.y - pdf.bounds.bottom
509
+ text = "Oh hai\n" * 60
510
+ text_box = described_class.new(text, document: pdf)
511
+ text_box.render
512
+ expect(text_box.height).to be_within(pdf.font.height).of(target_height)
513
+ end
514
+
515
+ it 'uses the margin-box bottom if only in a stretchy bbox' do
516
+ pdf.bounding_box([0, pdf.cursor], width: pdf.bounds.width) do
517
+ target_height = pdf.y - pdf.bounds.bottom
518
+ text = "Oh hai\n" * 60
519
+ text_box = described_class.new(text, document: pdf)
520
+ text_box.render
521
+ expect(text_box.height).to be_within(pdf.font.height).of(target_height)
522
+ end
523
+ end
524
+
525
+ it 'uses the parent-box bottom if in a stretchy bbox and overflow is '\
526
+ ':expand, even with an explicit height' do
527
+ pdf.bounding_box([0, pdf.cursor], width: pdf.bounds.width) do
528
+ target_height = pdf.y - pdf.bounds.bottom
529
+ text = "Oh hai\n" * 60
530
+ text_box = described_class.new(
531
+ text,
532
+ document: pdf,
533
+ height: 100,
534
+ overflow: :expand
535
+ )
536
+ text_box.render
537
+ expect(text_box.height).to be_within(pdf.font.height).of(target_height)
538
+ end
539
+ end
540
+
541
+ it 'uses the innermost non-stretchy bbox, not the margin box' do
542
+ pdf.bounding_box(
543
+ [0, pdf.cursor],
544
+ width: pdf.bounds.width,
545
+ height: 200
546
+ ) do
547
+ pdf.bounding_box([0, pdf.cursor], width: pdf.bounds.width) do
548
+ text = "Oh hai\n" * 60
549
+ text_box = described_class.new(text, document: pdf)
550
+ text_box.render
551
+ expect(text_box.height).to be_within(pdf.font.height).of(200)
552
+ end
553
+ end
554
+ end
555
+ end
556
+
557
+ describe 'default at' do
558
+ it 'is the left corner of the bounds, and the current document.y' do
559
+ target_at = [pdf.bounds.left, pdf.y]
560
+ text = 'Oh hai text rect. ' * 100
561
+ options = { document: pdf }
562
+ text_box = described_class.new(text, options)
563
+ text_box.render
564
+ expect(text_box.at).to eq(target_at)
565
+ end
566
+ end
567
+
568
+ context 'with text than can fit in the box' do
569
+ let(:text) { 'Oh hai text rect. ' * 10 }
570
+ let(:options) do
571
+ {
572
+ width: 162.0,
573
+ height: 162.0,
574
+ document: pdf
575
+ }
576
+ end
577
+
578
+ it 'printed text should match requested text, except that preceding and ' \
579
+ 'trailing white space will be stripped from each line, and newlines ' \
580
+ 'may be inserted' do
581
+ text_box = described_class.new(' ' + text, options)
582
+ text_box.render
583
+ expect(text_box.text.tr("\n", ' ')).to eq(text.strip)
584
+ end
585
+
586
+ it 'render returns an empty string because no text remains unprinted' do
587
+ text_box = described_class.new(text, options)
588
+ expect(text_box.render).to eq('')
589
+ end
590
+
591
+ it 'is truncated when the leading is set high enough to prevent all the '\
592
+ 'lines from being printed' do
593
+ options[:leading] = 40
594
+ text_box = described_class.new(text, options)
595
+ text_box.render
596
+ expect(text_box.text.tr("\n", ' ')).to_not eq(text.strip)
597
+ end
598
+ end
599
+
600
+ context 'with text that fits exactly in the box' do
601
+ let(:lines) { 3 }
602
+ let(:interlines) { lines - 1 }
603
+ let(:text) { (1..lines).to_a.join("\n") }
604
+ let(:options) do
605
+ {
606
+ width: 162.0,
607
+ height: pdf.font.ascender + pdf.font.height * interlines +
608
+ pdf.font.descender,
609
+ document: pdf
610
+ }
611
+ end
612
+
613
+ it 'has the expected height' do
614
+ expected_height = options.delete(:height)
615
+ text_box = described_class.new(text, options)
616
+ text_box.render
617
+ expect(text_box.height).to be_within(0.0001).of(expected_height)
618
+ end
619
+
620
+ it 'prints everything' do
621
+ text_box = described_class.new(text, options)
622
+ text_box.render
623
+ expect(text_box.text).to eq(text)
624
+ end
625
+
626
+ describe 'with leading' do
627
+ before do
628
+ options[:leading] = 15
629
+ end
630
+
631
+ it 'does not overflow when enough height is added' do
632
+ options[:height] += options[:leading] * interlines
633
+ text_box = described_class.new(text, options)
634
+ text_box.render
635
+ expect(text_box.text).to eq(text)
636
+ end
637
+
638
+ it 'overflows when insufficient height is added' do
639
+ options[:height] += options[:leading] * interlines - 1
640
+ text_box = described_class.new(text, options)
641
+ text_box.render
642
+ expect(text_box.text).to_not eq(text)
643
+ end
644
+ end
645
+
646
+ context 'with negative leading' do
647
+ before do
648
+ options[:leading] = -4
649
+ end
650
+
651
+ it 'does not overflow when enough height is removed' do
652
+ options[:height] += options[:leading] * interlines
653
+ text_box = described_class.new(text, options)
654
+ text_box.render
655
+ expect(text_box.text).to eq(text)
656
+ end
657
+
658
+ it 'overflows when too much height is removed' do
659
+ options[:height] += options[:leading] * interlines - 1
660
+ text_box = described_class.new(text, options)
661
+ text_box.render
662
+ expect(text_box.text).to_not eq(text)
663
+ end
664
+ end
665
+ end
666
+
667
+ context 'when printing UTF-8 string with higher bit characters' do
668
+ let(:text) { '©' }
669
+
670
+ let(:text_box) do
671
+ # not enough height to print any text, so we can directly compare against
672
+ # the input string
673
+ bounding_height = 1.0
674
+ options = {
675
+ height: bounding_height,
676
+ document: pdf
677
+ }
678
+ described_class.new(text, options)
679
+ end
680
+
681
+ before do
682
+ file = "#{Prawn::DATADIR}/fonts/Panic+Sans.dfont"
683
+ pdf.font_families['Panic Sans'] = {
684
+ normal: { file: file, font: 'PanicSans' },
685
+ italic: { file: file, font: 'PanicSans-Italic' },
686
+ bold: { file: file, font: 'PanicSans-Bold' },
687
+ bold_italic: { file: file, font: 'PanicSans-BoldItalic' }
688
+ }
689
+ end
690
+
691
+ describe 'when using a TTF font' do
692
+ it 'unprinted text should be in UTF-8 encoding' do
693
+ pdf.font('Panic Sans')
694
+ remaining_text = text_box.render
695
+ expect(remaining_text).to eq(text)
696
+ end
697
+ end
698
+
699
+ describe 'when using an AFM font' do
700
+ it 'unprinted text should be in UTF-8 encoding' do
701
+ remaining_text = text_box.render
702
+ expect(remaining_text).to eq(text)
703
+ end
704
+ end
705
+ end
706
+
707
+ context 'with more text than can fit in the box' do
708
+ let(:text) { 'Oh hai text rect. ' * 30 }
709
+ let(:bounding_height) { 162.0 }
710
+ let(:options) do
711
+ {
712
+ width: 162.0,
713
+ height: bounding_height,
714
+ document: pdf
715
+ }
716
+ end
717
+
718
+ context 'when truncated overflow' do
719
+ let(:text_box) do
720
+ described_class.new(text, options.merge(overflow: :truncate))
721
+ end
722
+
723
+ it 'is truncated' do
724
+ text_box.render
725
+ expect(text_box.text.tr("\n", ' ')).to_not eq(text.strip)
726
+ end
727
+
728
+ it 'render does not return an empty string because some text remains '\
729
+ 'unprinted' do
730
+ expect(text_box.render).to_not be_empty
731
+ end
732
+
733
+ it '#height should be no taller than the specified height' do
734
+ text_box.render
735
+ expect(text_box.height).to be <= bounding_height
736
+ end
737
+
738
+ it '#height should be within one font height of the specified height' do
739
+ text_box.render
740
+ expect(bounding_height).to be_within(pdf.font.height)
741
+ .of(text_box.height)
742
+ end
743
+
744
+ context 'with :rotate option' do
745
+ it 'unrendered text should be the same as when not rotated' do
746
+ remaining_text = text_box.render
747
+
748
+ rotate = 30
749
+ x = 300
750
+ y = 70
751
+ options[:document] = pdf
752
+ options[:rotate] = rotate
753
+ options[:at] = [x, y]
754
+ rotated_text_box = described_class.new(text, options)
755
+ expect(rotated_text_box.render).to eq(remaining_text)
756
+ end
757
+ end
758
+ end
759
+
760
+ context 'when truncated with text and size taken from the manual' do
761
+ it 'returns the right text' do
762
+ text = 'This is the beginning of the text. It will be cut somewhere ' \
763
+ 'and the rest of the text will procede to be rendered this time by '\
764
+ 'calling another method.' + ' . ' * 50
765
+ options[:width] = 300
766
+ options[:height] = 50
767
+ options[:size] = 18
768
+ text_box = described_class.new(text, options)
769
+ remaining_text = text_box.render
770
+ expect(remaining_text).to eq(
771
+ 'text will procede to be rendered this time by calling another ' \
772
+ 'method. . . . . . . . . . . . . . . . . . . . ' \
773
+ '. . . . . . . . . . . . . . . . . . . . . . ' \
774
+ '. . . . . . . . . '
775
+ )
776
+ end
777
+ end
778
+
779
+ context 'when expand overflow' do
780
+ let(:text_box) do
781
+ described_class.new(text, options.merge(overflow: :expand))
782
+ end
783
+
784
+ it 'height expands to encompass all the text '\
785
+ '(but not exceed the height of the page)' do
786
+ text_box.render
787
+ expect(text_box.height).to be > bounding_height
788
+ end
789
+
790
+ it 'displays the entire string (as long as there was space remaining on '\
791
+ 'the page to print all the text)' do
792
+ text_box.render
793
+ expect(text_box.text.tr("\n", ' ')).to eq(text.strip)
794
+ end
795
+
796
+ it 'render returns an empty string because no text remains unprinted '\
797
+ '(as long as there was space remaining on the page to print all '\
798
+ 'the text)' do
799
+ expect(text_box.render).to eq('')
800
+ end
801
+ end
802
+
803
+ context 'when shrink_to_fit overflow' do
804
+ let(:text_box) do
805
+ described_class.new(
806
+ text,
807
+ options.merge(
808
+ overflow: :shrink_to_fit,
809
+ min_font_size: 2
810
+ )
811
+ )
812
+ end
813
+
814
+ it 'displays the entire text' do
815
+ text_box.render
816
+ expect(text_box.text.tr("\n", ' ')).to eq(text.strip)
817
+ end
818
+
819
+ it 'render returns an empty string because no text remains unprinted' do
820
+ expect(text_box.render).to eq('')
821
+ end
822
+
823
+ it 'does not drop below the minimum font size' do
824
+ options[:overflow] = :shrink_to_fit
825
+ options[:min_font_size] = 10.1
826
+ text_box = described_class.new(text, options)
827
+ text_box.render
828
+
829
+ actual_text = PDF::Inspector::Text.analyze(pdf.render)
830
+ expect(actual_text.font_settings[0][:size]).to eq(10.1)
831
+ end
832
+ end
833
+ end
834
+
835
+ context 'with enough space to fit the text but using the ' \
836
+ 'shrink_to_fit overflow' do
837
+ it 'does not shrink the text when there is no need to' do
838
+ bounding_height = 162.0
839
+ options = {
840
+ width: 162.0,
841
+ height: bounding_height,
842
+ overflow: :shrink_to_fit,
843
+ min_font_size: 5,
844
+ document: pdf
845
+ }
846
+ text_box = described_class.new("hello\nworld", options)
847
+ text_box.render
848
+
849
+ text = PDF::Inspector::Text.analyze(pdf.render)
850
+ expect(text.font_settings[0][:size]).to eq(12)
851
+ end
852
+ end
853
+
854
+ context 'with a solid block of Chinese characters' do
855
+ it 'printed text should match requested text, except for newlines' do
856
+ text = '写中国字' * 10
857
+ options = {
858
+ width: 162.0,
859
+ height: 162.0,
860
+ document: pdf,
861
+ overflow: :truncate
862
+ }
863
+ pdf.font "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
864
+ text_box = described_class.new(text, options)
865
+ text_box.render
866
+ expect(text_box.text.delete("\n")).to eq(text)
867
+ end
868
+ end
869
+
870
+ describe 'drawing bounding boxes' do
871
+ it 'restores the margin box when bounding box exits' do
872
+ margin_box = pdf.bounds
873
+
874
+ pdf.text_box 'Oh hai text box. ' * 11, height: pdf.font.height * 10
875
+
876
+ expect(pdf.bounds).to eq(margin_box)
877
+ end
878
+ end
879
+
880
+ describe '#render with :character_spacing option' do
881
+ it 'draws the character spacing to the document' do
882
+ string = 'hello world'
883
+ options = { document: pdf, character_spacing: 10 }
884
+ text_box = described_class.new(string, options)
885
+ text_box.render
886
+ contents = PDF::Inspector::Text.analyze(pdf.render)
887
+ expect(contents.character_spacing[0]).to eq(10)
888
+ end
889
+
890
+ it 'takes character spacing into account when wrapping' do
891
+ pdf.font 'Courier'
892
+ text_box = described_class.new(
893
+ 'hello world',
894
+ width: 100,
895
+ overflow: :expand,
896
+ character_spacing: 10,
897
+ document: pdf
898
+ )
899
+ text_box.render
900
+ expect(text_box.text).to eq("hello\nworld")
901
+ end
902
+ end
903
+
904
+ describe 'wrapping' do
905
+ it 'wraps text' do
906
+ text = 'Please wrap this text about HERE. ' \
907
+ 'More text that should be wrapped'
908
+ expect = "Please wrap this text about\n"\
909
+ "HERE. More text that should be\nwrapped"
910
+
911
+ pdf.font 'Courier'
912
+ text_box = described_class.new(
913
+ text,
914
+ width: 220,
915
+ overflow: :expand,
916
+ document: pdf
917
+ )
918
+ text_box.render
919
+ expect(text_box.text).to eq(expect)
920
+ end
921
+
922
+ # white space was being stripped after the entire line was generated,
923
+ # meaning that leading white space characters reduced the amount of space on
924
+ # the line for other characters, so wrapping "hello hello" resulted in
925
+ # "hello\n\nhello", rather than "hello\nhello"
926
+ #
927
+ it 'white space at beginning of line should not be taken into account ' \
928
+ 'when computing line width' do
929
+ text = 'hello hello'
930
+ expect = "hello\nhello"
931
+
932
+ pdf.font 'Courier'
933
+ text_box = described_class.new(
934
+ text,
935
+ width: 40,
936
+ overflow: :expand,
937
+ document: pdf
938
+ )
939
+ text_box.render
940
+ expect(text_box.text).to eq(expect)
941
+ end
942
+
943
+ it 'respects end of line when wrapping text' do
944
+ text = "Please wrap only before\nTHIS word. Don't wrap this"
945
+ expect = text
946
+
947
+ pdf.font 'Courier'
948
+ text_box = described_class.new(
949
+ text,
950
+ width: 220,
951
+ overflow: :expand,
952
+ document: pdf
953
+ )
954
+ text_box.render
955
+ expect(text_box.text).to eq(expect)
956
+ end
957
+
958
+ it 'respects multiple newlines when wrapping text' do
959
+ text = "Please wrap only before THIS\n\nword. Don't wrap this"
960
+ expect = "Please wrap only before\nTHIS\n\nword. Don't wrap this"
961
+
962
+ pdf.font 'Courier'
963
+ text_box = described_class.new(
964
+ text,
965
+ width: 200,
966
+ overflow: :expand,
967
+ document: pdf
968
+ )
969
+ text_box.render
970
+ expect(text_box.text).to eq(expect)
971
+ end
972
+
973
+ it 'respects multiple newlines when wrapping text when those newlines '\
974
+ 'coincide with a line break' do
975
+ text = "Please wrap only before\n\nTHIS word. Don't wrap this"
976
+ expect = text
977
+
978
+ pdf.font 'Courier'
979
+ text_box = described_class.new(
980
+ text,
981
+ width: 220,
982
+ overflow: :expand,
983
+ document: pdf
984
+ )
985
+ text_box.render
986
+ expect(text_box.text).to eq(expect)
987
+ end
988
+
989
+ it 'respects initial newlines' do
990
+ text = "\nThis should be on line 2"
991
+ expect = text
992
+
993
+ pdf.font 'Courier'
994
+ text_box = described_class.new(
995
+ text,
996
+ width: 220,
997
+ overflow: :expand,
998
+ document: pdf
999
+ )
1000
+ text_box.render
1001
+ expect(text_box.text).to eq(expect)
1002
+ end
1003
+
1004
+ it 'wraps lines comprised of a single word of the bounds when '\
1005
+ 'wrapping text' do
1006
+ text = 'You_can_wrap_this_text_HERE'
1007
+ expect = "You_can_wrap_this_text_HE\nRE"
1008
+
1009
+ pdf.font 'Courier'
1010
+ text_box = described_class.new(
1011
+ text,
1012
+ width: 180,
1013
+ overflow: :expand,
1014
+ document: pdf
1015
+ )
1016
+ text_box.render
1017
+ expect(text_box.text).to eq(expect)
1018
+ end
1019
+
1020
+ it 'wraps lines comprised of a single non-alpha word of the bounds when '\
1021
+ 'wrapping text' do
1022
+ text = '©' * 30
1023
+
1024
+ pdf.font 'Courier'
1025
+ text_box = described_class.new(
1026
+ text, width: 180,
1027
+ overflow: :expand,
1028
+ document: pdf
1029
+ )
1030
+
1031
+ text_box.render
1032
+
1033
+ expected = +'©' * 25 + "\n" + '©' * 5
1034
+ expected = pdf.font.normalize_encoding(expected)
1035
+ expected = expected.force_encoding(Encoding::UTF_8)
1036
+ expect(text_box.text).to eq(expected)
1037
+ end
1038
+
1039
+ it 'wraps non-unicode strings using single-byte word-wrapping' do
1040
+ text = 'continúa esforzandote ' * 5
1041
+ text_box = described_class.new(
1042
+ text, width: 180,
1043
+ document: pdf
1044
+ )
1045
+ text_box.render
1046
+ results_with_accent = text_box.text
1047
+
1048
+ text = 'continua esforzandote ' * 5
1049
+ text_box = described_class.new(
1050
+ text, width: 180,
1051
+ document: pdf
1052
+ )
1053
+ text_box.render
1054
+ results_without_accent = text_box.text
1055
+
1056
+ expect(first_line(results_with_accent).length)
1057
+ .to eq(first_line(results_without_accent).length)
1058
+ end
1059
+
1060
+ it 'allows you to disable wrapping by char' do
1061
+ text = 'You_cannot_wrap_this_text_at_all_because_we_are_disabling_' \
1062
+ 'wrapping_by_char_and_there_are_no_word_breaks'
1063
+
1064
+ pdf.font 'Courier'
1065
+ text_box = described_class.new(
1066
+ text,
1067
+ width: 180,
1068
+ overflow: :shrink_to_fit,
1069
+ disable_wrap_by_char: true,
1070
+ document: pdf
1071
+ )
1072
+ expect { text_box.render }.to raise_error(Prawn::Errors::CannotFit)
1073
+ end
1074
+
1075
+ it 'retains full words with :shrink_to_fit if char wrapping is disabled' do
1076
+ text = 'Wrapped_words'
1077
+ expect = 'Wrapped_words'
1078
+
1079
+ pdf.font 'Courier'
1080
+ text_box = described_class.new(
1081
+ text,
1082
+ width: 50,
1083
+ height: 50,
1084
+ size: 50,
1085
+ overflow: :shrink_to_fit,
1086
+ disable_wrap_by_char: true,
1087
+ document: pdf
1088
+ )
1089
+ text_box.render
1090
+ expect(text_box.text).to eq(expect)
1091
+ end
1092
+ end
1093
+
1094
+ describe 'Text::Box#render with :mode option' do
1095
+ it 'alters the text rendering mode of the document' do
1096
+ string = 'hello world'
1097
+ options = { document: pdf, mode: :fill_stroke }
1098
+ text_box = described_class.new(string, options)
1099
+ text_box.render
1100
+ contents = PDF::Inspector::Text.analyze(pdf.render)
1101
+ expect(contents.text_rendering_mode).to eq([2, 0])
1102
+ end
1103
+ end
1104
+
1105
+ def reduce_precision(float)
1106
+ float.round(5)
1107
+ end
1108
+
1109
+ def first_line(str)
1110
+ str.each_line { |line| return line }
1111
+ end
1112
+ end