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