prawn 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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