prawn 2.1.0 → 2.2.0

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