prawn 2.0.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (277) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/GPLv2 +20 -21
  5. data/Gemfile +3 -9
  6. data/Rakefile +20 -23
  7. data/lib/prawn.rb +37 -49
  8. data/lib/prawn/document.rb +181 -133
  9. data/lib/prawn/document/bounding_box.rb +41 -29
  10. data/lib/prawn/document/column_box.rb +7 -7
  11. data/lib/prawn/document/internals.rb +18 -8
  12. data/lib/prawn/document/span.rb +21 -16
  13. data/lib/prawn/encoding.rb +69 -68
  14. data/lib/prawn/errors.rb +12 -7
  15. data/lib/prawn/font.rb +115 -69
  16. data/lib/prawn/font_metric_cache.rb +14 -8
  17. data/lib/prawn/{font → fonts}/afm.rb +102 -68
  18. data/lib/prawn/{font → fonts}/dfont.rb +5 -11
  19. data/lib/prawn/fonts/otf.rb +11 -0
  20. data/lib/prawn/fonts/ttc.rb +36 -0
  21. data/lib/prawn/{font → fonts}/ttf.rb +87 -68
  22. data/lib/prawn/graphics.rb +120 -80
  23. data/lib/prawn/graphics/blend_mode.rb +65 -0
  24. data/lib/prawn/graphics/cap_style.rb +3 -3
  25. data/lib/prawn/graphics/color.rb +27 -25
  26. data/lib/prawn/graphics/dash.rb +23 -11
  27. data/lib/prawn/graphics/join_style.rb +9 -3
  28. data/lib/prawn/graphics/patterns.rb +197 -67
  29. data/lib/prawn/graphics/transformation.rb +17 -8
  30. data/lib/prawn/graphics/transparency.rb +17 -13
  31. data/lib/prawn/grid.rb +48 -47
  32. data/lib/prawn/image_handler.rb +5 -5
  33. data/lib/prawn/images.rb +39 -30
  34. data/lib/prawn/images/image.rb +2 -1
  35. data/lib/prawn/images/jpg.rb +28 -22
  36. data/lib/prawn/images/png.rb +107 -66
  37. data/lib/prawn/measurement_extensions.rb +10 -9
  38. data/lib/prawn/measurements.rb +19 -15
  39. data/lib/prawn/outline.rb +97 -77
  40. data/lib/prawn/repeater.rb +14 -10
  41. data/lib/prawn/security.rb +81 -61
  42. data/lib/prawn/security/arcfour.rb +2 -2
  43. data/lib/prawn/soft_mask.rb +26 -26
  44. data/lib/prawn/stamp.rb +20 -13
  45. data/lib/prawn/text.rb +68 -52
  46. data/lib/prawn/text/box.rb +11 -8
  47. data/lib/prawn/text/formatted.rb +5 -5
  48. data/lib/prawn/text/formatted/arranger.rb +78 -49
  49. data/lib/prawn/text/formatted/box.rb +134 -100
  50. data/lib/prawn/text/formatted/fragment.rb +11 -14
  51. data/lib/prawn/text/formatted/line_wrap.rb +121 -63
  52. data/lib/prawn/text/formatted/parser.rb +139 -117
  53. data/lib/prawn/text/formatted/wrap.rb +43 -31
  54. data/lib/prawn/transformation_stack.rb +44 -0
  55. data/lib/prawn/utilities.rb +7 -22
  56. data/lib/prawn/version.rb +2 -2
  57. data/lib/prawn/view.rb +17 -7
  58. data/manual/basic_concepts/adding_pages.rb +6 -7
  59. data/manual/basic_concepts/basic_concepts.rb +31 -22
  60. data/manual/basic_concepts/creation.rb +10 -11
  61. data/manual/basic_concepts/cursor.rb +4 -5
  62. data/manual/basic_concepts/measurement.rb +6 -7
  63. data/manual/basic_concepts/origin.rb +5 -6
  64. data/manual/basic_concepts/other_cursor_helpers.rb +11 -12
  65. data/manual/basic_concepts/view.rb +22 -16
  66. data/manual/bounding_box/bounding_box.rb +29 -24
  67. data/manual/bounding_box/bounds.rb +11 -12
  68. data/manual/bounding_box/canvas.rb +4 -5
  69. data/manual/bounding_box/creation.rb +6 -7
  70. data/manual/bounding_box/indentation.rb +14 -15
  71. data/manual/bounding_box/nesting.rb +24 -17
  72. data/manual/bounding_box/russian_boxes.rb +14 -13
  73. data/manual/bounding_box/stretchy.rb +12 -13
  74. data/manual/contents.rb +28 -22
  75. data/manual/cover.rb +33 -28
  76. data/manual/document_and_page_options/background.rb +11 -13
  77. data/manual/document_and_page_options/document_and_page_options.rb +25 -20
  78. data/manual/document_and_page_options/metadata.rb +18 -16
  79. data/manual/document_and_page_options/page_margins.rb +18 -20
  80. data/manual/document_and_page_options/page_size.rb +13 -12
  81. data/manual/document_and_page_options/print_scaling.rb +17 -15
  82. data/manual/example_helper.rb +5 -4
  83. data/manual/graphics/blend_mode.rb +52 -0
  84. data/manual/graphics/circle_and_ellipse.rb +4 -5
  85. data/manual/graphics/color.rb +7 -9
  86. data/manual/graphics/common_lines.rb +7 -8
  87. data/manual/graphics/fill_and_stroke.rb +4 -5
  88. data/manual/graphics/fill_rules.rb +9 -10
  89. data/manual/graphics/gradients.rb +27 -21
  90. data/manual/graphics/graphics.rb +48 -39
  91. data/manual/graphics/helper.rb +12 -9
  92. data/manual/graphics/line_width.rb +8 -7
  93. data/manual/graphics/lines_and_curves.rb +7 -8
  94. data/manual/graphics/polygon.rb +6 -8
  95. data/manual/graphics/rectangle.rb +4 -5
  96. data/manual/graphics/rotate.rb +6 -7
  97. data/manual/graphics/scale.rb +14 -15
  98. data/manual/graphics/soft_masks.rb +4 -5
  99. data/manual/graphics/stroke_cap.rb +6 -7
  100. data/manual/graphics/stroke_dash.rb +11 -12
  101. data/manual/graphics/stroke_join.rb +5 -6
  102. data/manual/graphics/translate.rb +9 -10
  103. data/manual/graphics/transparency.rb +7 -8
  104. data/manual/how_to_read_this_manual.rb +6 -6
  105. data/manual/images/absolute_position.rb +6 -7
  106. data/manual/images/fit.rb +7 -8
  107. data/manual/images/horizontal.rb +9 -10
  108. data/manual/images/images.rb +28 -24
  109. data/manual/images/plain_image.rb +5 -6
  110. data/manual/images/scale.rb +9 -10
  111. data/manual/images/vertical.rb +13 -14
  112. data/manual/images/width_and_height.rb +10 -11
  113. data/manual/layout/boxes.rb +5 -6
  114. data/manual/layout/content.rb +7 -8
  115. data/manual/layout/layout.rb +18 -16
  116. data/manual/layout/simple_grid.rb +6 -7
  117. data/manual/outline/add_subsection_to.rb +20 -21
  118. data/manual/outline/insert_section_after.rb +15 -16
  119. data/manual/outline/outline.rb +21 -17
  120. data/manual/outline/sections_and_pages.rb +17 -18
  121. data/manual/repeatable_content/alternate_page_numbering.rb +21 -17
  122. data/manual/repeatable_content/page_numbering.rb +17 -16
  123. data/manual/repeatable_content/repeatable_content.rb +25 -19
  124. data/manual/repeatable_content/repeater.rb +14 -15
  125. data/manual/repeatable_content/stamp.rb +14 -15
  126. data/manual/security/encryption.rb +9 -10
  127. data/manual/security/permissions.rb +19 -14
  128. data/manual/security/security.rb +19 -16
  129. data/manual/table.rb +3 -3
  130. data/manual/text/alignment.rb +16 -17
  131. data/manual/text/color.rb +12 -11
  132. data/manual/text/column_box.rb +9 -10
  133. data/manual/text/fallback_fonts.rb +25 -21
  134. data/manual/text/font.rb +11 -12
  135. data/manual/text/font_size.rb +13 -14
  136. data/manual/text/font_style.rb +7 -8
  137. data/manual/text/formatted_callbacks.rb +25 -21
  138. data/manual/text/formatted_text.rb +33 -25
  139. data/manual/text/free_flowing_text.rb +20 -21
  140. data/manual/text/inline.rb +18 -19
  141. data/manual/text/kerning_and_character_spacing.rb +14 -15
  142. data/manual/text/leading.rb +7 -8
  143. data/manual/text/line_wrapping.rb +37 -18
  144. data/manual/text/paragraph_indentation.rb +13 -14
  145. data/manual/text/positioned_text.rb +15 -16
  146. data/manual/text/registering_families.rb +20 -21
  147. data/manual/text/rendering_and_color.rb +9 -10
  148. data/manual/text/right_to_left_text.rb +26 -19
  149. data/manual/text/rotation.rb +28 -23
  150. data/manual/text/single_usage.rb +8 -9
  151. data/manual/text/text.rb +57 -52
  152. data/manual/text/text_box_excess.rb +20 -17
  153. data/manual/text/text_box_extensions.rb +18 -15
  154. data/manual/text/text_box_overflow.rb +18 -19
  155. data/manual/text/utf8.rb +11 -12
  156. data/manual/text/win_ansi_charset.rb +21 -19
  157. data/prawn.gemspec +45 -33
  158. data/spec/extensions/encoding_helpers.rb +3 -3
  159. data/spec/prawn/document/bounding_box_spec.rb +546 -0
  160. data/spec/prawn/document/column_box_spec.rb +75 -0
  161. data/spec/prawn/document/security_spec.rb +176 -0
  162. data/spec/prawn/document_annotations_spec.rb +76 -0
  163. data/spec/prawn/document_destinations_spec.rb +15 -0
  164. data/spec/prawn/document_grid_spec.rb +99 -0
  165. data/spec/prawn/document_reference_spec.rb +27 -0
  166. data/spec/prawn/document_span_spec.rb +36 -0
  167. data/spec/prawn/document_spec.rb +802 -0
  168. data/spec/prawn/font_metric_cache_spec.rb +54 -0
  169. data/spec/prawn/font_spec.rb +542 -0
  170. data/spec/prawn/graphics/blend_mode_spec.rb +63 -0
  171. data/spec/prawn/graphics/transparency_spec.rb +81 -0
  172. data/spec/prawn/graphics_spec.rb +837 -0
  173. data/spec/prawn/graphics_stroke_styles_spec.rb +229 -0
  174. data/spec/prawn/image_handler_spec.rb +53 -0
  175. data/spec/prawn/images/jpg_spec.rb +20 -0
  176. data/spec/prawn/images/png_spec.rb +283 -0
  177. data/spec/prawn/images_spec.rb +224 -0
  178. data/spec/prawn/measurements_extensions_spec.rb +24 -0
  179. data/spec/prawn/outline_spec.rb +412 -0
  180. data/spec/prawn/repeater_spec.rb +165 -0
  181. data/spec/prawn/soft_mask_spec.rb +74 -0
  182. data/spec/prawn/stamp_spec.rb +172 -0
  183. data/spec/prawn/text/box_spec.rb +1112 -0
  184. data/spec/prawn/text/formatted/arranger_spec.rb +466 -0
  185. data/spec/prawn/text/formatted/box_spec.rb +846 -0
  186. data/spec/prawn/text/formatted/fragment_spec.rb +343 -0
  187. data/spec/prawn/text/formatted/line_wrap_spec.rb +494 -0
  188. data/spec/prawn/text/formatted/parser_spec.rb +697 -0
  189. data/spec/prawn/text_draw_text_spec.rb +149 -0
  190. data/spec/prawn/text_rendering_mode_spec.rb +48 -0
  191. data/spec/prawn/text_spacing_spec.rb +95 -0
  192. data/spec/prawn/text_spec.rb +603 -0
  193. data/spec/prawn/text_with_inline_formatting_spec.rb +35 -0
  194. data/spec/prawn/transformation_stack_spec.rb +66 -0
  195. data/spec/prawn/view_spec.rb +63 -0
  196. data/spec/prawn_manual_spec.rb +35 -0
  197. data/spec/spec_helper.rb +19 -23
  198. metadata +145 -185
  199. metadata.gz.sig +4 -0
  200. data/data/images/16bit.alpha +0 -0
  201. data/data/images/16bit.color +0 -0
  202. data/data/images/16bit.png +0 -0
  203. data/data/images/arrow.png +0 -0
  204. data/data/images/arrow2.png +0 -0
  205. data/data/images/dice.alpha +0 -0
  206. data/data/images/dice.color +0 -0
  207. data/data/images/dice.png +0 -0
  208. data/data/images/dice_interlaced.png +0 -0
  209. data/data/images/fractal.jpg +0 -0
  210. data/data/images/indexed_color.dat +0 -0
  211. data/data/images/indexed_color.png +0 -0
  212. data/data/images/letterhead.jpg +0 -0
  213. data/data/images/license.md +0 -8
  214. data/data/images/page_white_text.alpha +0 -0
  215. data/data/images/page_white_text.color +0 -0
  216. data/data/images/page_white_text.png +0 -0
  217. data/data/images/pal_bk.png +0 -0
  218. data/data/images/pigs.jpg +0 -0
  219. data/data/images/prawn.png +0 -0
  220. data/data/images/ruport.png +0 -0
  221. data/data/images/ruport_data.dat +0 -0
  222. data/data/images/ruport_transparent.png +0 -0
  223. data/data/images/ruport_type0.png +0 -0
  224. data/data/images/stef.jpg +0 -0
  225. data/data/images/tru256.bmp +0 -0
  226. data/data/images/web-links.dat +0 -1
  227. data/data/images/web-links.png +0 -0
  228. data/data/pdfs/complex_template.pdf +0 -0
  229. data/data/pdfs/contains_ttf_font.pdf +0 -0
  230. data/data/pdfs/encrypted.pdf +0 -0
  231. data/data/pdfs/form.pdf +1 -819
  232. data/data/pdfs/hexagon.pdf +0 -61
  233. data/data/pdfs/indirect_reference.pdf +0 -86
  234. data/data/pdfs/multipage_template.pdf +0 -127
  235. data/data/pdfs/nested_pages.pdf +0 -118
  236. data/data/pdfs/page_without_mediabox.pdf +0 -193
  237. data/data/pdfs/resources_as_indirect_object.pdf +0 -83
  238. data/data/pdfs/two_hexagons.pdf +0 -90
  239. data/data/pdfs/version_1_6.pdf +0 -61
  240. data/data/shift_jis_text.txt +0 -1
  241. data/spec/acceptance/png.rb +0 -24
  242. data/spec/annotations_spec.rb +0 -67
  243. data/spec/bounding_box_spec.rb +0 -501
  244. data/spec/column_box_spec.rb +0 -59
  245. data/spec/destinations_spec.rb +0 -13
  246. data/spec/document_spec.rb +0 -742
  247. data/spec/extensions/mocha.rb +0 -45
  248. data/spec/font_metric_cache_spec.rb +0 -52
  249. data/spec/font_spec.rb +0 -475
  250. data/spec/formatted_text_arranger_spec.rb +0 -423
  251. data/spec/formatted_text_box_spec.rb +0 -716
  252. data/spec/formatted_text_fragment_spec.rb +0 -299
  253. data/spec/graphics_spec.rb +0 -666
  254. data/spec/grid_spec.rb +0 -95
  255. data/spec/image_handler_spec.rb +0 -53
  256. data/spec/images_spec.rb +0 -167
  257. data/spec/inline_formatted_text_parser_spec.rb +0 -568
  258. data/spec/jpg_spec.rb +0 -23
  259. data/spec/line_wrap_spec.rb +0 -366
  260. data/spec/measurement_units_spec.rb +0 -22
  261. data/spec/outline_spec.rb +0 -409
  262. data/spec/png_spec.rb +0 -235
  263. data/spec/reference_spec.rb +0 -25
  264. data/spec/repeater_spec.rb +0 -154
  265. data/spec/security_spec.rb +0 -151
  266. data/spec/soft_mask_spec.rb +0 -78
  267. data/spec/span_spec.rb +0 -43
  268. data/spec/stamp_spec.rb +0 -179
  269. data/spec/stroke_styles_spec.rb +0 -208
  270. data/spec/text_at_spec.rb +0 -142
  271. data/spec/text_box_spec.rb +0 -1038
  272. data/spec/text_rendering_mode_spec.rb +0 -45
  273. data/spec/text_spacing_spec.rb +0 -93
  274. data/spec/text_spec.rb +0 -549
  275. data/spec/text_with_inline_formatting_spec.rb +0 -35
  276. data/spec/transparency_spec.rb +0 -91
  277. data/spec/view_spec.rb +0 -42
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Graphics::BlendMode do
6
+ def make_blend_mode(blend_mode)
7
+ pdf.blend_mode(blend_mode) do
8
+ yield if block_given?
9
+ end
10
+ end
11
+
12
+ let(:pdf) { create_pdf }
13
+
14
+ it 'the PDF version should be at least 1.4' do
15
+ make_blend_mode(:Multiply)
16
+ str = pdf.render
17
+ expect(str[0, 8]).to eq('%PDF-1.4')
18
+ end
19
+
20
+ it 'a new extended graphics state should be created for ' \
21
+ 'each unique blend mode setting' do
22
+ make_blend_mode(:Multiply) do
23
+ make_blend_mode(:Screen)
24
+ end
25
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
26
+ expect(extgstates.length).to eq(2)
27
+ end
28
+
29
+ it 'a new extended graphics state should not be created for ' \
30
+ 'each duplicate blend mode setting' do
31
+ make_blend_mode(:Multiply) do
32
+ make_blend_mode(:Multiply)
33
+ end
34
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
35
+ expect(extgstates.length).to eq(1)
36
+ end
37
+
38
+ it 'setting the blend mode with only one parameter sets a single '\
39
+ 'blend mode value' do
40
+ make_blend_mode(:Multiply)
41
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates.first
42
+ expect(extgstate[:blend_mode]).to eq(:Multiply)
43
+ end
44
+
45
+ it 'setting the blend mode with multiple parameters sets an array of '\
46
+ 'blend modes' do
47
+ make_blend_mode(%i[Multiply Screen Overlay])
48
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates.first
49
+ expect(extgstate[:blend_mode]).to eq(%i[Multiply Screen Overlay])
50
+ end
51
+
52
+ describe 'with more than one page' do
53
+ it 'the extended graphic state resource should be added to both pages' do
54
+ make_blend_mode(:Multiply)
55
+ pdf.start_new_page
56
+ make_blend_mode(:Multiply)
57
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
58
+ extgstate = extgstates[0]
59
+ expect(extgstates.length).to eq(2)
60
+ expect(extgstate[:blend_mode]).to eq(:Multiply)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Graphics::Transparency do
6
+ def make_transparent(opacity, stroke_opacity = opacity)
7
+ pdf.transparent(opacity, stroke_opacity) do
8
+ yield if block_given?
9
+ end
10
+ end
11
+
12
+ let(:pdf) { create_pdf }
13
+
14
+ it 'the PDF version should be at least 1.4' do
15
+ make_transparent(0.5)
16
+ str = pdf.render
17
+ expect(str[0, 8]).to eq('%PDF-1.4')
18
+ end
19
+
20
+ it 'a new extended graphics state should be created for ' \
21
+ 'each unique transparency setting' do
22
+ make_transparent(0.5, 0.2) do
23
+ make_transparent(0.5, 0.75)
24
+ end
25
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
26
+ expect(extgstates.length).to eq(2)
27
+ end
28
+
29
+ it 'a new extended graphics state should not be created for ' \
30
+ 'each duplicate transparency setting' do
31
+ make_transparent(0.5, 0.75) do
32
+ make_transparent(0.5, 0.75)
33
+ end
34
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
35
+ expect(extgstates.length).to eq(1)
36
+ end
37
+
38
+ it 'setting the transparency with only one parameter sets the ' \
39
+ 'transparency for both the fill and the stroke' do
40
+ make_transparent(0.5)
41
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates[0]
42
+ expect(extgstate[:opacity]).to eq(0.5)
43
+ expect(extgstate[:stroke_opacity]).to eq(0.5)
44
+ end
45
+
46
+ it 'setting the transparency with a numerical parameter and ' \
47
+ 'a :stroke should set the fill transparency to the numerical parameter ' \
48
+ 'and the stroke transparency to the option' do
49
+ make_transparent(0.5, 0.2)
50
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates[0]
51
+ expect(extgstate[:opacity]).to eq(0.5)
52
+ expect(extgstate[:stroke_opacity]).to eq(0.2)
53
+ end
54
+
55
+ it 'does not allow negative values' do
56
+ make_transparent(-0.5, -0.2)
57
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates[0]
58
+ expect(extgstate[:opacity]).to eq(0.0)
59
+ expect(extgstate[:stroke_opacity]).to eq(0.0)
60
+ end
61
+
62
+ it 'does not allow too big values' do
63
+ make_transparent(2.0, 3.0)
64
+ extgstate = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates[0]
65
+ expect(extgstate[:opacity]).to eq(1.0)
66
+ expect(extgstate[:stroke_opacity]).to eq(1.0)
67
+ end
68
+
69
+ describe 'with more than one page' do
70
+ it 'the extended graphic state resource should be added to both pages' do
71
+ make_transparent(0.5, 0.2)
72
+ pdf.start_new_page
73
+ make_transparent(0.5, 0.2)
74
+ extgstates = PDF::Inspector::ExtGState.analyze(pdf.render).extgstates
75
+ extgstate = extgstates[0]
76
+ expect(extgstates.length).to eq(2)
77
+ expect(extgstate[:opacity]).to eq(0.5)
78
+ expect(extgstate[:stroke_opacity]).to eq(0.2)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,837 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Prawn::Graphics do
6
+ let(:pdf) { create_pdf }
7
+
8
+ describe 'When drawing a line' do
9
+ it 'draws a line from (100,600) to (100,500)' do
10
+ pdf.line([100, 600], [100, 500])
11
+
12
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
13
+
14
+ expect(line_drawing.points).to eq([[100, 600], [100, 500]])
15
+ end
16
+
17
+ it 'draws two lines at (100,600) to (100,500) and (75,100) to (50,125)' do
18
+ pdf.line(100, 600, 100, 500)
19
+ pdf.line(75, 100, 50, 125)
20
+
21
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
22
+
23
+ expect(line_drawing.points).to eq(
24
+ [[100.0, 600.0], [100.0, 500.0], [75.0, 100.0], [50.0, 125.0]]
25
+ )
26
+ end
27
+
28
+ it 'properlies set line width via line_width=' do
29
+ pdf.line_width = 10
30
+ line = PDF::Inspector::Graphics::Line.analyze(pdf.render)
31
+ expect(line.widths.first).to eq(10)
32
+ end
33
+
34
+ it 'properlies set line width via line_width(width)' do
35
+ pdf.line_width(10)
36
+ line = PDF::Inspector::Graphics::Line.analyze(pdf.render)
37
+ expect(line.widths.first).to eq(10)
38
+ end
39
+
40
+ it 'carries the current line width settings over to new pages' do
41
+ pdf.line_width(10)
42
+ pdf.start_new_page
43
+ line = PDF::Inspector::Graphics::Line.analyze(pdf.render)
44
+ expect(line.widths.length).to eq(2)
45
+ expect(line.widths[1]).to eq(10)
46
+ end
47
+
48
+ describe '(Horizontally)' do
49
+ it 'draws from [x1,pdf.y],[x2,pdf.y]' do
50
+ pdf.horizontal_line(100, 150)
51
+ line = PDF::Inspector::Graphics::Line.analyze(pdf.render)
52
+ expect(line.points).to eq([
53
+ [100.0 + pdf.bounds.absolute_left, pdf.y],
54
+ [150.0 + pdf.bounds.absolute_left, pdf.y]
55
+ ])
56
+ end
57
+
58
+ it 'draws a line from (200, 250) to (300, 250)' do
59
+ pdf.horizontal_line(200, 300, at: 250)
60
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
61
+ expect(line_drawing.points).to eq([[200, 250], [300, 250]])
62
+ end
63
+ end
64
+
65
+ describe '(Vertically)' do
66
+ it 'draws a line from (350, 300) to (350, 400)' do
67
+ pdf.vertical_line(300, 400, at: 350)
68
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
69
+ expect(line_drawing.points).to eq([[350, 300], [350, 400]])
70
+ end
71
+
72
+ it 'requires a y coordinate' do
73
+ expect { pdf.vertical_line(400, 500) }
74
+ .to raise_error(ArgumentError)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'When drawing a polygon' do
80
+ it 'draws each line passed to polygon()' do
81
+ pdf.polygon([100, 500], [100, 400], [200, 400])
82
+
83
+ line_drawing = PDF::Inspector::Graphics::Line.analyze(pdf.render)
84
+ expect(line_drawing.points)
85
+ .to eq([[100, 500], [100, 400], [200, 400], [100, 500]])
86
+ end
87
+ end
88
+
89
+ describe 'When drawing a rectangle' do
90
+ it 'uses a point, width, and height for coords' do
91
+ pdf.rectangle [200, 200], 50, 100
92
+
93
+ rectangles = PDF::Inspector::Graphics::Rectangle.analyze(pdf.render)
94
+ .rectangles
95
+ # PDF uses bottom left corner
96
+ expect(rectangles[0][:point]).to eq([200, 100])
97
+ expect(rectangles[0][:width]).to eq(50)
98
+ expect(rectangles[0][:height]).to eq(100)
99
+ end
100
+ end
101
+
102
+ describe 'When drawing a curve' do
103
+ it 'draws a bezier curve from 50,50 to 100,100' do
104
+ pdf.move_to [50, 50]
105
+ pdf.curve_to [100, 100], bounds: [[20, 90], [90, 70]]
106
+ curve = PDF::Inspector::Graphics::Curve.analyze(pdf.render)
107
+ expect(curve.coords)
108
+ .to eq([50.0, 50.0, 20.0, 90.0, 90.0, 70.0, 100.0, 100.0])
109
+ end
110
+
111
+ it 'draws a bezier curve from 100,100 to 50,50' do
112
+ pdf.curve [100, 100], [50, 50], bounds: [[20, 90], [90, 75]]
113
+ curve = PDF::Inspector::Graphics::Curve.analyze(pdf.render)
114
+ expect(curve.coords)
115
+ .to eq([100.0, 100.0, 20.0, 90.0, 90.0, 75.0, 50.0, 50.0])
116
+ end
117
+ end
118
+
119
+ describe 'When drawing a rounded rectangle' do
120
+ before { pdf.rounded_rectangle([50, 550], 50, 100, 10) }
121
+
122
+ let(:original_point) do
123
+ curve = PDF::Inspector::Graphics::Curve.analyze(pdf.render)
124
+ curve_points = curve.coords.each_slice(2).to_a
125
+ curve_points.shift
126
+ end
127
+ let(:all_coords) do
128
+ curve = PDF::Inspector::Graphics::Curve.analyze(pdf.render)
129
+ curve_points = curve.coords.each_slice(2).to_a
130
+ curve_points.shift
131
+ curves = curve_points.each_slice(3).to_a
132
+ line_points = PDF::Inspector::Graphics::Line.analyze(pdf.render).points
133
+ line_points.shift
134
+ line_points.zip(curves).flatten.each_slice(2).to_a.unshift original_point
135
+ end
136
+
137
+ it 'draws a rectangle by connecting lines with rounded bezier curves' do
138
+ expect(all_coords).to eq(
139
+ [
140
+ [60.0, 550.0], [90.0, 550.0], [95.5228, 550.0], [100.0, 545.5228],
141
+ [100.0, 540.0], [100.0, 460.0], [100.0, 454.4772], [95.5228, 450.0],
142
+ [90.0, 450.0], [60.0, 450.0], [54.4772, 450.0], [50.0, 454.4772],
143
+ [50.0, 460.0], [50.0, 540.0], [50.0, 545.5228], [54.4772, 550.0],
144
+ [60.0, 550.0]
145
+ ]
146
+ )
147
+ end
148
+
149
+ it 'starts and end with the same point' do
150
+ expect(original_point).to eq(all_coords.last)
151
+ end
152
+ end
153
+
154
+ describe 'When drawing an ellipse' do
155
+ let(:curve) do
156
+ pdf.ellipse [100, 100], 25, 50
157
+ PDF::Inspector::Graphics::Curve.analyze(pdf.render)
158
+ end
159
+
160
+ it 'uses a Bézier approximation' do
161
+ expect(curve.coords).to eq([
162
+ 125.0, 100.0,
163
+
164
+ 125.0, 127.6142,
165
+ 113.8071, 150,
166
+ 100.0, 150.0,
167
+
168
+ 86.1929, 150.0,
169
+ 75.0, 127.6142,
170
+ 75.0, 100.0,
171
+
172
+ 75.0, 72.3858,
173
+ 86.1929, 50.0,
174
+ 100.0, 50.0,
175
+
176
+ 113.8071, 50.0,
177
+ 125.0, 72.3858,
178
+ 125.0, 100.0,
179
+
180
+ 100.0, 100.0
181
+ ])
182
+ end
183
+
184
+ it 'moves the pointer to the center of the ellipse after drawing' do
185
+ expect(curve.coords[-2..-1]).to eq([100, 100])
186
+ end
187
+ end
188
+
189
+ describe 'When drawing a circle' do
190
+ let(:curve) do
191
+ pdf.circle [100, 100], 25
192
+ pdf.ellipse [100, 100], 25, 25
193
+ PDF::Inspector::Graphics::Curve.analyze(pdf.render)
194
+ end
195
+
196
+ it 'strokes the same path as the equivalent ellipse' do
197
+ middle = curve.coords.length / 2
198
+ expect(curve.coords[0...middle]).to eq(curve.coords[middle..-1])
199
+ end
200
+ end
201
+
202
+ describe 'When filling' do
203
+ it 'defaults to the f operator (nonzero winding number rule)' do
204
+ allow(pdf.renderer).to receive(:add_content).with('f')
205
+ pdf.fill
206
+ expect(pdf.renderer).to have_received(:add_content).with('f')
207
+ end
208
+
209
+ it 'uses f* for :fill_rule => :even_odd' do
210
+ allow(pdf.renderer).to receive(:add_content).with('f*')
211
+ pdf.fill(fill_rule: :even_odd)
212
+ expect(pdf.renderer).to have_received(:add_content).with('f*')
213
+ end
214
+
215
+ it 'uses b by default for fill_and_stroke (nonzero winding number)' do
216
+ allow(pdf.renderer).to receive(:add_content).with('b')
217
+ pdf.fill_and_stroke
218
+ expect(pdf.renderer).to have_received(:add_content).with('b')
219
+ end
220
+
221
+ it 'uses b* for fill_and_stroke(:fill_rule => :even_odd)' do
222
+ allow(pdf.renderer).to receive(:add_content).with('b*')
223
+ pdf.fill_and_stroke(fill_rule: :even_odd)
224
+ expect(pdf.renderer).to have_received(:add_content).with('b*')
225
+ end
226
+ end
227
+
228
+ describe 'When setting colors' do
229
+ it 'sets stroke colors' do
230
+ pdf.stroke_color 'ffcccc'
231
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
232
+ # 100% red, 80% green, 80% blue
233
+ expect(colors.stroke_color).to eq([1.0, 0.8, 0.8])
234
+ end
235
+
236
+ it 'sets fill colors' do
237
+ pdf.fill_color 'ccff00'
238
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
239
+ # 80% red, 100% green, 0% blue
240
+ expect(colors.fill_color).to eq([0.8, 1.0, 0])
241
+ end
242
+
243
+ it 'raises an error for a color with a leading #' do
244
+ expect { pdf.fill_color '#ccff00' }.to raise_error(ArgumentError)
245
+ end
246
+
247
+ it 'raises an error for a color string that is not a hex' do
248
+ expect { pdf.fill_color 'zcff00' }.to raise_error(ArgumentError)
249
+ end
250
+
251
+ it 'raises an error for a color string with invalid characters' do
252
+ expect { pdf.fill_color 'f0f0f?' }.to raise_error(ArgumentError)
253
+ end
254
+
255
+ it 'resets the colors on each new page if they have been defined' do
256
+ pdf.fill_color 'ccff00'
257
+
258
+ pdf.start_new_page
259
+ pdf.stroke_color 'ff00cc'
260
+
261
+ pdf.start_new_page
262
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
263
+ expect(colors.fill_color_count).to eq(3)
264
+ expect(colors.stroke_color_count).to eq(2)
265
+
266
+ expect(colors.fill_color).to eq([0.8, 1.0, 0.0])
267
+ expect(colors.stroke_color).to eq([1.0, 0.0, 0.8])
268
+ end
269
+
270
+ it 'sets the color space when setting colors on new pages to please fussy '\
271
+ 'readers' do
272
+ pdf.stroke_color '000000'
273
+ pdf.stroke { pdf.rectangle([10, 10], 10, 10) }
274
+ pdf.start_new_page
275
+ pdf.stroke_color '000000'
276
+ pdf.stroke { pdf.rectangle([10, 10], 10, 10) }
277
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
278
+ expect(colors.stroke_color_space_count[:DeviceRGB]).to eq(2)
279
+ end
280
+ end
281
+
282
+ describe 'Patterns' do
283
+ describe 'linear gradients' do
284
+ it 'creates a /Pattern resource' do
285
+ pdf.fill_gradient(
286
+ [0, pdf.bounds.height],
287
+ [pdf.bounds.width, pdf.bounds.height],
288
+ 'FF0000', '0000FF'
289
+ )
290
+
291
+ grad = PDF::Inspector::Graphics::Pattern.analyze(pdf.render)
292
+ pattern = grad.patterns.values.first
293
+
294
+ expect(pattern).to_not be_nil
295
+ expect(pattern[:Shading][:ShadingType]).to eq(2)
296
+ expect(pattern[:Shading][:Coords]).to eq([0, 0, pdf.bounds.width, 0])
297
+ expect(pattern[:Shading][:Function][:C0].zip([1, 0, 0]).all? do |x1, x2|
298
+ (x1 - x2).abs < 0.01
299
+ end).to eq true
300
+ expect(pattern[:Shading][:Function][:C1].zip([0, 0, 1]).all? do |x1, x2|
301
+ (x1 - x2).abs < 0.01
302
+ end).to eq true
303
+ end
304
+
305
+ it 'creates a unique ID for each pattern resource' do
306
+ pdf.fill_gradient(
307
+ [256, 512],
308
+ [356, 512],
309
+ 'ffffff', 'fe00ff'
310
+ )
311
+ pdf.fill_gradient(
312
+ [256, 256],
313
+ [356, 256],
314
+ 'ffffff', '0000ff'
315
+ )
316
+
317
+ str = pdf.render
318
+ pattern_ids = str.scan(/SP\h{40}\s+scn/)
319
+ expect(pattern_ids.uniq.length).to eq 2
320
+ end
321
+
322
+ it 'fill_gradient should set fill color to the pattern' do
323
+ pdf.fill_gradient(
324
+ [0, pdf.bounds.height],
325
+ [pdf.bounds.width, pdf.bounds.height],
326
+ 'FF0000', '0000FF'
327
+ )
328
+
329
+ str = pdf.render
330
+ expect(str).to match(%r{/Pattern\s+cs\s*/SP\h{40}\s+scn})
331
+ end
332
+
333
+ it 'stroke_gradient should set stroke color to the pattern' do
334
+ pdf.stroke_gradient(
335
+ [0, pdf.bounds.height],
336
+ [pdf.bounds.width, pdf.bounds.height],
337
+ 'FF0000', '0000FF'
338
+ )
339
+
340
+ str = pdf.render
341
+ expect(str).to match(%r{/Pattern\s+CS\s*/SP\h{40}\s+SCN})
342
+ end
343
+
344
+ it 'uses a stitching function to render a gradient with multiple stops' do
345
+ pdf.fill_gradient(
346
+ from: [0, pdf.bounds.height],
347
+ to: [pdf.bounds.width, pdf.bounds.height],
348
+ stops: { 0 => 'FF0000', 0.8 => '00FF00', 1 => '0000FF' }
349
+ )
350
+
351
+ grad = PDF::Inspector::Graphics::Pattern.analyze(pdf.render)
352
+ pattern = grad.patterns.values.first
353
+
354
+ expect(pattern).to_not be_nil
355
+
356
+ stitching = pattern[:Shading][:Function]
357
+ expect(stitching[:FunctionType]).to eq(3)
358
+ expect(stitching[:Functions]).to be_an(Array)
359
+ expect(stitching[:Functions].map { |f| f[:C0] })
360
+ .to eq([[1, 0, 0], [0, 1, 0]])
361
+ expect(stitching[:Functions].map { |f| f[:C1] })
362
+ .to eq([[0, 1, 0], [0, 0, 1]])
363
+ expect(stitching[:Bounds]).to eq([0.8])
364
+ expect(stitching[:Encode]).to eq([0, 1, 0, 1])
365
+ end
366
+
367
+ it 'uses a stitching function to render a gradient with equally spaced '\
368
+ 'stops' do
369
+ pdf.fill_gradient(
370
+ from: [0, pdf.bounds.height],
371
+ to: [pdf.bounds.width, pdf.bounds.height],
372
+ stops: %w[FF0000 00FF00 0000FF]
373
+ )
374
+
375
+ grad = PDF::Inspector::Graphics::Pattern.analyze(pdf.render)
376
+ pattern = grad.patterns.values.first
377
+
378
+ expect(pattern).to_not be_nil
379
+
380
+ stitching = pattern[:Shading][:Function]
381
+ expect(stitching[:FunctionType]).to eq(3)
382
+ expect(stitching[:Functions]).to be_an(Array)
383
+ expect(stitching[:Functions].map { |f| f[:C0] })
384
+ .to eq([[1, 0, 0], [0, 1, 0]])
385
+ expect(stitching[:Functions].map { |f| f[:C1] })
386
+ .to eq([[0, 1, 0], [0, 0, 1]])
387
+ expect(stitching[:Bounds]).to eq([0.5])
388
+ end
389
+ end
390
+
391
+ describe 'radial gradients' do
392
+ it 'creates a /Pattern resource' do
393
+ pdf.fill_gradient(
394
+ [0, pdf.bounds.height], 10,
395
+ [pdf.bounds.width, pdf.bounds.height], 20,
396
+ 'FF0000', '0000FF'
397
+ )
398
+
399
+ grad = PDF::Inspector::Graphics::Pattern.analyze(pdf.render)
400
+ pattern = grad.patterns.values.first
401
+
402
+ expect(pattern).to_not be_nil
403
+ expect(pattern[:Shading][:ShadingType]).to eq(3)
404
+ expect(pattern[:Shading][:Coords])
405
+ .to eq([0, 0, 10, pdf.bounds.width, 0, 20])
406
+ expect(pattern[:Shading][:Function][:C0].zip([1, 0, 0]).all? do |x1, x2|
407
+ (x1 - x2).abs < 0.01
408
+ end).to eq true
409
+ expect(pattern[:Shading][:Function][:C1].zip([0, 0, 1]).all? do |x1, x2|
410
+ (x1 - x2).abs < 0.01
411
+ end).to eq true
412
+ end
413
+
414
+ it 'fill_gradient should set fill color to the pattern' do
415
+ pdf.fill_gradient(
416
+ [0, pdf.bounds.height], 10,
417
+ [pdf.bounds.width, pdf.bounds.height], 20,
418
+ 'FF0000', '0000FF'
419
+ )
420
+
421
+ str = pdf.render
422
+ expect(str).to match(%r{/Pattern\s+cs\s*/SP\h{40}\s+scn})
423
+ end
424
+
425
+ it 'stroke_gradient should set stroke color to the pattern' do
426
+ pdf.stroke_gradient(
427
+ [0, pdf.bounds.height], 10,
428
+ [pdf.bounds.width, pdf.bounds.height], 20,
429
+ 'FF0000', '0000FF'
430
+ )
431
+
432
+ str = pdf.render
433
+ expect(str).to match(%r{/Pattern\s+CS\s*/SP\h{40}\s+SCN})
434
+ end
435
+ end
436
+
437
+ describe 'gradient transformations' do
438
+ subject(:transformations) do
439
+ pdf.scale 2 do
440
+ pdf.translate 40, 40 do
441
+ pdf.fill_gradient [0, 10], [15, 15], 'FF0000', '0000FF', opts
442
+ pdf.fill_gradient [0, 10], 15, [15, 15], 25, 'FF0000', '0000FF',
443
+ opts
444
+ end
445
+ end
446
+
447
+ grad = PDF::Inspector::Graphics::Pattern.analyze(pdf.render)
448
+ grad.patterns.values.map { |pattern| pattern[:Matrix] }.uniq
449
+ end
450
+
451
+ context 'when :apply_transformations is true' do
452
+ let(:opts) { { apply_transformations: true } }
453
+
454
+ it 'uses the transformation stack to translate user co-ordinates to '\
455
+ 'document co-ordinates required by /Pattern' do
456
+ expect(transformations).to eq([[2, 0, 0, 2, 80, 100]])
457
+ end
458
+ end
459
+
460
+ context 'when :apply_transformations is false' do
461
+ let(:opts) { { apply_transformations: false } }
462
+
463
+ it "doesn't transform the gradient" do
464
+ expect(transformations).to eq([[1, 0, 0, 1, 0, 10]])
465
+ end
466
+ end
467
+
468
+ context 'when :apply_transformations is unset' do
469
+ let(:opts) { {} }
470
+
471
+ it "doesn't transform the gradient and displays a warning" do
472
+ allow(pdf).to receive(:warn).twice
473
+ expect(transformations).to eq([[1, 0, 0, 1, 0, 10]])
474
+ expect(pdf).to have_received(:warn).twice
475
+ end
476
+ end
477
+ end
478
+ end
479
+
480
+ describe 'When using painting shortcuts' do
481
+ it 'converts stroke_some_method(args) into some_method(args); stroke' do
482
+ allow(pdf).to receive(:line_to).with([100, 100])
483
+ allow(pdf).to receive(:stroke)
484
+
485
+ pdf.stroke_line_to [100, 100]
486
+
487
+ expect(pdf).to have_received(:line_to).with([100, 100])
488
+ expect(pdf).to have_received(:stroke)
489
+ end
490
+
491
+ it 'converts fill_some_method(args) into some_method(args); fill' do
492
+ allow(pdf).to receive(:line_to).with([100, 100])
493
+ allow(pdf).to receive(:fill)
494
+
495
+ pdf.fill_line_to [100, 100]
496
+
497
+ expect(pdf).to have_received(:line_to).with([100, 100])
498
+ expect(pdf).to have_received(:fill)
499
+ end
500
+
501
+ it 'does not break method_missing' do
502
+ expect { pdf.i_have_a_pretty_girlfriend_named_jia }
503
+ .to raise_error(NoMethodError)
504
+ end
505
+ end
506
+
507
+ describe 'When using graphics states' do
508
+ it 'adds the right content on save_graphics_state' do
509
+ allow(pdf.renderer).to receive(:add_content).with('q')
510
+
511
+ pdf.save_graphics_state
512
+
513
+ expect(pdf.renderer).to have_received(:add_content).with('q')
514
+ end
515
+
516
+ it 'adds the right content on restore_graphics_state' do
517
+ allow(pdf.renderer).to receive(:add_content).with('Q')
518
+
519
+ pdf.restore_graphics_state
520
+
521
+ expect(pdf.renderer).to have_received(:add_content).with('Q')
522
+ end
523
+
524
+ it 'saves and restore when save_graphics_state is used with a block' do
525
+ allow(pdf.renderer).to receive(:add_content).with('q')
526
+ allow(pdf).to receive(:foo)
527
+ allow(pdf.renderer).to receive(:add_content).with('Q')
528
+
529
+ pdf.save_graphics_state do
530
+ pdf.foo
531
+ end
532
+
533
+ expect(pdf.renderer).to have_received(:add_content).with('q').ordered
534
+ expect(pdf).to have_received(:foo).ordered
535
+ expect(pdf.renderer).to have_received(:add_content).with('Q').ordered
536
+ end
537
+
538
+ it 'adds the previous color space when restoring to a graphic state with '\
539
+ 'different color space' do
540
+ pdf.stroke_color '000000'
541
+ pdf.save_graphics_state
542
+ pdf.stroke_color 0, 0, 0, 0
543
+ pdf.restore_graphics_state
544
+ pdf.stroke_color 0, 0, 100, 0
545
+ expect(pdf.graphic_state.color_space).to eq(stroke: :DeviceCMYK)
546
+ colors = PDF::Inspector::Graphics::Color.analyze(pdf.render)
547
+ expect(colors.color_space).to eq(:DeviceCMYK)
548
+ expect(colors.stroke_color_space_count[:DeviceCMYK]).to eq(2)
549
+ end
550
+
551
+ it 'uses the correct dash setting after restoring and starting new page' do
552
+ pdf.dash 5
553
+ pdf.save_graphics_state
554
+ pdf.dash 10
555
+ expect(pdf.graphic_state.dash[:dash]).to eq(10)
556
+ pdf.restore_graphics_state
557
+ pdf.start_new_page
558
+ expect(pdf.graphic_state.dash[:dash]).to eq(5)
559
+ end
560
+
561
+ it 'rounds dash values to four decimal places' do
562
+ pdf.dash 5.12345
563
+ expect(pdf.graphic_state.dash_setting).to eq('[5.1235 5.1235] 0.0 d')
564
+ end
565
+
566
+ it 'raises an error when dash is called w. a zero length or space' do
567
+ expect { pdf.dash(0) }.to raise_error(ArgumentError)
568
+ expect { pdf.dash([0]) }.to raise_error(ArgumentError)
569
+ expect { pdf.dash([0, 0]) }.to raise_error(ArgumentError)
570
+ end
571
+
572
+ it 'raises an error when dash is called w. negative lengths' do
573
+ expect { pdf.dash(-1) }.to raise_error(ArgumentError)
574
+ expect { pdf.dash([1, -3]) }.to raise_error(ArgumentError)
575
+ end
576
+
577
+ it 'the current graphic state keeps track of previous unchanged settings' do
578
+ pdf.stroke_color '000000'
579
+ pdf.save_graphics_state
580
+ pdf.dash 5
581
+ pdf.save_graphics_state
582
+ pdf.cap_style :round
583
+ pdf.save_graphics_state
584
+ pdf.fill_color 0, 0, 100, 0
585
+ pdf.save_graphics_state
586
+
587
+ expect(pdf.graphic_state.stroke_color).to eq('000000')
588
+ expect(pdf.graphic_state.join_style).to eq(:miter)
589
+ expect(pdf.graphic_state.fill_color).to eq([0, 0, 100, 0])
590
+ expect(pdf.graphic_state.cap_style).to eq(:round)
591
+ expect(pdf.graphic_state.color_space)
592
+ .to eq(fill: :DeviceCMYK, stroke: :DeviceRGB)
593
+ expect(pdf.graphic_state.dash).to eq(space: 5, phase: 0, dash: 5)
594
+ expect(pdf.graphic_state.line_width).to eq(1)
595
+ end
596
+
597
+ it "doesn't add extra graphic space closings when rendering multiple " \
598
+ 'times' do
599
+ pdf.render
600
+ state = PDF::Inspector::Graphics::State.analyze(pdf.render)
601
+ expect(state.save_graphics_state_count).to eq(1)
602
+ expect(state.restore_graphics_state_count).to eq(1)
603
+ end
604
+
605
+ it 'adds extra graphic state enclosings when content is added on multiple '\
606
+ 'renderings' do
607
+ pdf.render
608
+ pdf.text 'Adding a bit more content'
609
+ state = PDF::Inspector::Graphics::State.analyze(pdf.render)
610
+ expect(state.save_graphics_state_count).to eq(2)
611
+ expect(state.restore_graphics_state_count).to eq(2)
612
+ end
613
+
614
+ it 'adds extra graphic state enclosings when new settings are applied on '\
615
+ 'multiple renderings' do
616
+ pdf.render
617
+ pdf.stroke_color 0, 0, 0, 0
618
+ state = PDF::Inspector::Graphics::State.analyze(pdf.render)
619
+ expect(state.save_graphics_state_count).to eq(2)
620
+ expect(state.restore_graphics_state_count).to eq(2)
621
+ end
622
+
623
+ it 'raise_errors error if closing an empty graphic stack' do
624
+ expect do
625
+ pdf.render
626
+ pdf.restore_graphics_state
627
+ end.to raise_error(PDF::Core::Errors::EmptyGraphicStateStack)
628
+ end
629
+
630
+ it 'copies mutable attributes when passing a previous_state to '\
631
+ 'the initializer' do
632
+ new_state = PDF::Core::GraphicState.new(pdf.graphic_state)
633
+
634
+ %i[color_space dash fill_color stroke_color].each do |attr|
635
+ expect(new_state.send(attr)).to eq(pdf.graphic_state.send(attr))
636
+ expect(new_state.send(attr)).to_not equal(pdf.graphic_state.send(attr))
637
+ end
638
+ end
639
+
640
+ it 'copies mutable attributes when duping' do
641
+ new_state = pdf.graphic_state.dup
642
+
643
+ %i[color_space dash fill_color stroke_color].each do |attr|
644
+ expect(new_state.send(attr)).to eq(pdf.graphic_state.send(attr))
645
+ expect(new_state.send(attr)).to_not equal(pdf.graphic_state.send(attr))
646
+ end
647
+ end
648
+ end
649
+
650
+ describe 'When using transformation matrix' do
651
+ # Note: The (approximate) number of significant decimal digits of precision
652
+ # in fractional part is 5 (PDF Reference, Third Edition, p. 706)
653
+
654
+ it 'sends the right content on transformation_matrix' do
655
+ allow(pdf.renderer).to receive(:add_content)
656
+ .with('1.0 0.0 0.12346 -1.0 5.5 20.0 cm')
657
+ pdf.transformation_matrix 1, 0, 0.123456789, -1.0, 5.5, 20
658
+ expect(pdf.renderer).to have_received(:add_content)
659
+ .with('1.0 0.0 0.12346 -1.0 5.5 20.0 cm')
660
+ end
661
+
662
+ it 'uses fixed digits with very small number' do
663
+ values = Array.new(6, 0.000000000001)
664
+ string = Array.new(6, '0.0').join ' '
665
+ allow(pdf.renderer).to receive(:add_content).with("#{string} cm")
666
+ pdf.transformation_matrix(*values)
667
+ expect(pdf.renderer).to have_received(:add_content).with("#{string} cm")
668
+ end
669
+
670
+ it 'is received by the inspector' do
671
+ pdf.transformation_matrix 1, 0, 0, -1, 5.5, 20
672
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
673
+ expect(matrices.matrices).to eq([[1, 0, 0, -1, 5.5, 20]])
674
+ end
675
+
676
+ it 'saves the graphics state inside the given block' do
677
+ values = Array.new(6, 0.000000000001)
678
+ string = Array.new(6, '0.0').join ' '
679
+
680
+ allow(pdf).to receive(:save_graphics_state).with(no_args)
681
+ allow(pdf.renderer).to receive(:add_content).with(any_args).twice
682
+ allow(pdf.renderer).to receive(:add_content).with("#{string} cm")
683
+ allow(pdf).to receive(:do_something)
684
+ allow(pdf).to receive(:restore_graphics_state).with(no_args)
685
+
686
+ pdf.transformation_matrix(*values) do
687
+ pdf.do_something
688
+ end
689
+
690
+ expect(pdf).to have_received(:save_graphics_state).with(no_args).ordered
691
+ expect(pdf.renderer).to have_received(:add_content).with("#{string} cm")
692
+ .ordered
693
+ expect(pdf).to have_received(:do_something).ordered
694
+ expect(pdf).to have_received(:restore_graphics_state).with(no_args)
695
+ .ordered
696
+ end
697
+ end
698
+
699
+ describe 'When using transformations shortcuts' do
700
+ let(:x) { 12 }
701
+ let(:y) { 54.32 }
702
+ let(:angle) { 12.32 }
703
+ let(:cos) { Math.cos(angle * Math::PI / 180) }
704
+ let(:sin) { Math.sin(angle * Math::PI / 180) }
705
+ let(:factor) { 0.12 }
706
+
707
+ describe '#rotate' do
708
+ it 'rotates' do
709
+ allow(pdf).to receive(:transformation_matrix)
710
+ .with(cos, sin, -sin, cos, 0, 0)
711
+ pdf.rotate(angle)
712
+ expect(pdf).to have_received(:transformation_matrix)
713
+ .with(cos, sin, -sin, cos, 0, 0)
714
+ end
715
+ end
716
+
717
+ describe '#rotate with :origin option' do
718
+ it 'rotates around the origin' do
719
+ x_prime = x * cos - y * sin
720
+ y_prime = x * sin + y * cos
721
+
722
+ pdf.rotate(angle, origin: [x, y]) { pdf.text('hello world') }
723
+
724
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
725
+ expect(matrices.matrices[0]).to eq([
726
+ 1, 0, 0, 1,
727
+ reduce_precision(x - x_prime),
728
+ reduce_precision(y - y_prime)
729
+ ])
730
+ expect(matrices.matrices[1]).to eq([
731
+ reduce_precision(cos),
732
+ reduce_precision(sin),
733
+ reduce_precision(-sin),
734
+ reduce_precision(cos),
735
+ 0, 0
736
+ ])
737
+ end
738
+
739
+ it 'rotates around the origin in a document with a margin' do
740
+ pdf = Prawn::Document.new
741
+
742
+ pdf.rotate(angle, origin: [x, y]) { pdf.text('hello world') }
743
+
744
+ x_ = x + pdf.bounds.absolute_left
745
+ y_ = y + pdf.bounds.absolute_bottom
746
+ x_prime = x_ * cos - y_ * sin
747
+ y_prime = x_ * sin + y_ * cos
748
+
749
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
750
+ expect(matrices.matrices[0]).to eq([
751
+ 1, 0, 0, 1,
752
+ reduce_precision(x_ - x_prime),
753
+ reduce_precision(y_ - y_prime)
754
+ ])
755
+ expect(matrices.matrices[1]).to eq([
756
+ reduce_precision(cos),
757
+ reduce_precision(sin),
758
+ reduce_precision(-sin),
759
+ reduce_precision(cos),
760
+ 0, 0
761
+ ])
762
+ end
763
+
764
+ it 'raise_errors BlockRequired if no block is given' do
765
+ expect do
766
+ pdf.rotate(angle, origin: [x, y])
767
+ end.to raise_error(Prawn::Errors::BlockRequired)
768
+ end
769
+ end
770
+
771
+ describe '#translate' do
772
+ it 'translates' do
773
+ x = 12
774
+ y = 54.32
775
+ allow(pdf).to receive(:transformation_matrix).with(1, 0, 0, 1, x, y)
776
+ pdf.translate(x, y)
777
+ expect(pdf).to have_received(:transformation_matrix)
778
+ .with(1, 0, 0, 1, x, y)
779
+ end
780
+ end
781
+
782
+ describe '#scale' do
783
+ it 'scales' do
784
+ allow(pdf).to receive(:transformation_matrix)
785
+ .with(factor, 0, 0, factor, 0, 0)
786
+ pdf.scale(factor)
787
+ expect(pdf).to have_received(:transformation_matrix)
788
+ .with(factor, 0, 0, factor, 0, 0)
789
+ end
790
+ end
791
+
792
+ describe '#scale with :origin option' do
793
+ it 'scales from the origin' do
794
+ x_prime = factor * x
795
+ y_prime = factor * y
796
+
797
+ pdf.scale(factor, origin: [x, y]) { pdf.text('hello world') }
798
+
799
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
800
+ expect(matrices.matrices[0]).to eq([
801
+ 1, 0, 0, 1,
802
+ reduce_precision(x - x_prime),
803
+ reduce_precision(y - y_prime)
804
+ ])
805
+ expect(matrices.matrices[1]).to eq([factor, 0, 0, factor, 0, 0])
806
+ end
807
+
808
+ it 'scales from the origin in a document with a margin' do
809
+ pdf = Prawn::Document.new
810
+ x_ = x + pdf.bounds.absolute_left
811
+ y_ = y + pdf.bounds.absolute_bottom
812
+ x_prime = factor * x_
813
+ y_prime = factor * y_
814
+
815
+ pdf.scale(factor, origin: [x, y]) { pdf.text('hello world') }
816
+
817
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(pdf.render)
818
+ expect(matrices.matrices[0]).to eq([
819
+ 1, 0, 0, 1,
820
+ reduce_precision(x_ - x_prime),
821
+ reduce_precision(y_ - y_prime)
822
+ ])
823
+ expect(matrices.matrices[1]).to eq([factor, 0, 0, factor, 0, 0])
824
+ end
825
+
826
+ it 'raise_errors BlockRequired if no block is given' do
827
+ expect do
828
+ pdf.scale(factor, origin: [x, y])
829
+ end.to raise_error(Prawn::Errors::BlockRequired)
830
+ end
831
+ end
832
+ end
833
+
834
+ def reduce_precision(float)
835
+ float.round(5)
836
+ end
837
+ end