prawn 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/data/images/blend_modes_bottom_layer.jpg +0 -0
  3. data/data/images/blend_modes_top_layer.jpg +0 -0
  4. data/data/images/indexed_transparency.png +0 -0
  5. data/data/images/indexed_transparency_alpha.dat +0 -0
  6. data/data/images/indexed_transparency_color.dat +0 -0
  7. data/lib/prawn.rb +2 -1
  8. data/lib/prawn/document.rb +1 -0
  9. data/lib/prawn/document/internals.rb +10 -2
  10. data/lib/prawn/font.rb +14 -1
  11. data/lib/prawn/graphics.rb +2 -0
  12. data/lib/prawn/graphics/blend_mode.rb +64 -0
  13. data/lib/prawn/graphics/patterns.rb +52 -16
  14. data/lib/prawn/graphics/transformation.rb +3 -0
  15. data/lib/prawn/images/png.rb +43 -5
  16. data/lib/prawn/text/formatted/arranger.rb +25 -17
  17. data/lib/prawn/text/formatted/line_wrap.rb +2 -3
  18. data/lib/prawn/transformation_stack.rb +42 -0
  19. data/lib/prawn/version.rb +1 -1
  20. data/manual/graphics/blend_mode.rb +49 -0
  21. data/manual/graphics/graphics.rb +1 -0
  22. data/manual/graphics/soft_masks.rb +1 -1
  23. data/prawn.gemspec +4 -5
  24. data/spec/acceptance/png_spec.rb +35 -0
  25. data/spec/blend_mode_spec.rb +71 -0
  26. data/spec/document_spec.rb +72 -76
  27. data/spec/font_spec.rb +11 -11
  28. data/spec/formatted_text_arranger_spec.rb +178 -149
  29. data/spec/formatted_text_box_spec.rb +23 -23
  30. data/spec/graphics_spec.rb +67 -28
  31. data/spec/image_handler_spec.rb +7 -7
  32. data/spec/images_spec.rb +1 -1
  33. data/spec/png_spec.rb +26 -4
  34. data/spec/repeater_spec.rb +9 -9
  35. data/spec/spec_helper.rb +1 -4
  36. data/spec/text_at_spec.rb +2 -2
  37. data/spec/text_box_spec.rb +20 -16
  38. data/spec/text_spec.rb +8 -14
  39. data/spec/transformation_stack_spec.rb +63 -0
  40. data/spec/view_spec.rb +10 -10
  41. metadata +27 -31
  42. data/data/images/pal_bk.png +0 -0
  43. data/spec/acceptance/png.rb +0 -24
  44. data/spec/extensions/mocha.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b05cc94288d323f650bc4a44529992282d42b69f
4
- data.tar.gz: 71cb2515eb6667207083899502a96487807f8667
3
+ metadata.gz: e103cb901b7728d21af3dfe412f3072063a86835
4
+ data.tar.gz: 132ff9e3bf2aeeeb1e9b574e5b84115192e576dc
5
5
  SHA512:
6
- metadata.gz: 4081995850066454b2ec29d3532ea5470812d8af92606a7e2d65d9514105242d3e4061927559e8280c180732c8223d11f37debc7423382ffeada0820dbfc8c8c
7
- data.tar.gz: f1ef9c7346cddd535db2acde3a3522bdc4a128dcd8da2eb45439278203ca982e22543de1667a0d7f6435824884f73eb8ed046f6dbf128fa9ad3d721792bc4add
6
+ metadata.gz: 0cad684b3b096da6cdceb69e47b78c5629251687d28c8a3ab4f824fc64b842daa11e2f62122033d9b27f1cd4afe809bfad1c3cc2c150e909cae09f5480a442ec
7
+ data.tar.gz: 5ea7ffa1b819a00d71897150c87afd0a7b7cc08e1afc8c4b41a425fd262f5276c961e969ea0e6e0178dde6f74ded16842076674abe39afbc5ca9bfaf513d5aab
@@ -23,7 +23,7 @@ module Prawn
23
23
 
24
24
  FLOAT_PRECISION = 1.0e-9
25
25
 
26
- # Whe set to true, Prawn will verify hash options to ensure only valid keys
26
+ # When set to true, Prawn will verify hash options to ensure only valid keys
27
27
  # are used. Off by default.
28
28
  #
29
29
  # Example:
@@ -76,6 +76,7 @@ require_relative "prawn/images/png"
76
76
  require_relative "prawn/stamp"
77
77
  require_relative "prawn/soft_mask"
78
78
  require_relative "prawn/security"
79
+ require_relative "prawn/transformation_stack"
79
80
  require_relative "prawn/document"
80
81
  require_relative "prawn/font"
81
82
  require_relative "prawn/measurements"
@@ -57,6 +57,7 @@ module Prawn
57
57
  include Prawn::Images
58
58
  include Prawn::Stamp
59
59
  include Prawn::SoftMask
60
+ include Prawn::TransformationStack
60
61
 
61
62
  # @group Extension API
62
63
 
@@ -24,10 +24,18 @@ module Prawn
24
24
  # Perhaps they will become part of the extension API?
25
25
  # Anyway, for now it's not clear what we should do w. them.
26
26
  delegate [ :graphic_state,
27
- :save_graphics_state,
28
- :restore_graphics_state,
29
27
  :on_page_create ] => :renderer
30
28
 
29
+ def save_graphics_state(state = nil, &block)
30
+ save_transformation_stack
31
+ renderer.save_graphics_state(state, &block)
32
+ end
33
+
34
+ def restore_graphics_state
35
+ restore_transformation_stack
36
+ renderer.restore_graphics_state
37
+ end
38
+
31
39
  # FIXME: This is a circular reference, because in theory Prawn should
32
40
  # be passing instances of renderer to PDF::Core::Page, but it's
33
41
  # passing Prawn::Document objects instead.
@@ -396,7 +396,20 @@ module Prawn
396
396
  # generate a font identifier that hasn't been used on the current page yet
397
397
  #
398
398
  def generate_unique_id
399
- :"F#{@document.font_registry.size + 1}"
399
+ key = nil
400
+ font_count = @document.font_registry.size + 1
401
+ loop do
402
+ key = :"F#{font_count}"
403
+ break if key_is_unique?(key)
404
+ font_count += 1
405
+ end
406
+ key
407
+ end
408
+
409
+ def key_is_unique?(test_key)
410
+ !@document.state.page.fonts.keys.any? do |key|
411
+ key.to_s.start_with?("#{test_key}.")
412
+ end
400
413
  end
401
414
 
402
415
  def size
@@ -6,6 +6,7 @@
6
6
  #
7
7
  # This is free software. Please see the LICENSE and COPYING files for details.
8
8
 
9
+ require_relative "graphics/blend_mode"
9
10
  require_relative "graphics/color"
10
11
  require_relative "graphics/dash"
11
12
  require_relative "graphics/cap_style"
@@ -22,6 +23,7 @@ module Prawn
22
23
  # ruby-pdf.rubyforge.org
23
24
  #
24
25
  module Graphics
26
+ include BlendMode
25
27
  include Color
26
28
  include Dash
27
29
  include CapStyle
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ #
3
+ # blend_mode.rb : Implements blend modes
4
+ #
5
+ # Contributed by John Ford. October, 2015
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ #
9
+
10
+ module Prawn
11
+ module Graphics
12
+ # The Prawn::BlendMode module is used to change the way
13
+ # two layers are blended together.
14
+ #
15
+ # Passing an array of blend modes is allowed. PDF viewers should
16
+ # blend layers based on the first recognized blend mode.
17
+ #
18
+ # Valid blend modes in v1.4 of the PDF spec include :Normal, :Multiply, :Screen,
19
+ # :Overlay, :Darken, :Lighten, :ColorDodge, :ColorBurn, :HardLight, :SoftLight,
20
+ # :Difference, :Exclusion, :Hue, :Saturation, :Color, and :Luminosity.
21
+ #
22
+ # Example:
23
+ # pdf.fill_color('0000ff')
24
+ # pdf.fill_rectangle([x, y+25], 50, 50)
25
+ # pdf.blend_mode(:Multiply) do
26
+ # pdf.fill_color('ff0000')
27
+ # pdf.fill_circle([x, y], 25)
28
+ # end
29
+ #
30
+ module BlendMode
31
+ # @group Stable API
32
+
33
+ def blend_mode(blend_mode = :Normal)
34
+ renderer.min_version(1.4)
35
+
36
+ save_graphics_state if block_given?
37
+ renderer.add_content "/#{blend_mode_dictionary_name(blend_mode)} gs"
38
+ if block_given?
39
+ yield
40
+ restore_graphics_state
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def blend_mode_dictionary_registry
47
+ @blend_mode_dictionary_registry ||= {}
48
+ end
49
+
50
+ def blend_mode_dictionary_name(blend_mode)
51
+ key = Array(blend_mode).join('')
52
+ dictionary_name = "BM#{key}"
53
+
54
+ dictionary = blend_mode_dictionary_registry[dictionary_name] ||= ref!(
55
+ :Type => :ExtGState,
56
+ :BM => blend_mode
57
+ )
58
+
59
+ page.ext_gstates.merge!(dictionary_name => dictionary)
60
+ dictionary_name
61
+ end
62
+ end
63
+ end
64
+ end
@@ -14,16 +14,30 @@ module Prawn
14
14
 
15
15
  # Sets the fill gradient from color1 to color2.
16
16
  # old arguments: point, width, height, color1, color2, options = {}
17
- # new arguments: from, to, color1, color1
18
- # or from, r1, to, r2, color1, color2
17
+ # new arguments: from, to, color1, color1, options = {}
18
+ # or from, r1, to, r2, color1, color2, options = {}
19
+ #
20
+ # Option :apply_transformations, if set true, will transform the
21
+ # gradient's co-ordinate space so it matches the current co-ordinate
22
+ # space of the document. This option will be the default from Prawn v3.
23
+ # The current default, false, will mean if you (for example) scale your
24
+ # document by 2 and put a gradient inside, you will have to manually
25
+ # multiply your co-ordinates by 2 so the gradient is correctly positioned.
19
26
  def fill_gradient(*args)
20
27
  set_gradient(:fill, *args)
21
28
  end
22
29
 
23
30
  # Sets the stroke gradient from color1 to color2.
24
31
  # old arguments: point, width, height, color1, color2, options = {}
25
- # new arguments: from, to, color1, color2
26
- # or from, r1, to, r2, color1, color2
32
+ # new arguments: from, to, color1, color2, options = {}
33
+ # or from, r1, to, r2, color1, color2, options = {}
34
+ #
35
+ # Option :apply_transformations, if set true, will transform the
36
+ # gradient's co-ordinate space so it matches the current co-ordinate
37
+ # space of the document. This option will be the default from Prawn v3.
38
+ # The current default, false, will mean if you (for example) scale your
39
+ # document by 2 and put a gradient inside, you will have to manually
40
+ # multiply your co-ordinates by 2 so the gradient is correctly positioned.
27
41
  def stroke_gradient(*args)
28
42
  set_gradient(:stroke, *args)
29
43
  end
@@ -31,15 +45,17 @@ module Prawn
31
45
  private
32
46
 
33
47
  def set_gradient(type, *grad)
48
+ opts = grad.last.is_a?(Hash) ? grad.pop : {}
49
+
34
50
  patterns = page.resources[:Pattern] ||= {}
35
51
 
36
- registry_key = gradient_registry_key grad
52
+ registry_key = gradient_registry_key grad, opts
37
53
 
38
54
  if patterns["SP#{registry_key}"]
39
55
  shading = patterns["SP#{registry_key}"]
40
56
  else
41
57
  unless shading = gradient_registry[registry_key]
42
- shading = gradient(*grad)
58
+ shading = gradient(grad, opts)
43
59
  gradient_registry[registry_key] = shading
44
60
  end
45
61
 
@@ -59,18 +75,20 @@ module Prawn
59
75
  renderer.add_content "/SP#{registry_key} #{operator}"
60
76
  end
61
77
 
62
- def gradient_registry_key(gradient)
78
+ def gradient_registry_key(gradient, opts)
79
+ _x1, _y1, x2, y2, transformation = gradient_coordinates(gradient, opts)
80
+
63
81
  if gradient[1].is_a?(Array) # axial
64
82
  [
65
- map_to_absolute(gradient[0]),
66
- map_to_absolute(gradient[1]),
83
+ transformation,
84
+ x2, y2,
67
85
  gradient[2], gradient[3]
68
86
  ]
69
87
  else # radial
70
88
  [
71
- map_to_absolute(gradient[0]),
89
+ transformation,
90
+ x2, y2,
72
91
  gradient[1],
73
- map_to_absolute(gradient[2]),
74
92
  gradient[3],
75
93
  gradient[4], gradient[5]
76
94
  ]
@@ -81,11 +99,15 @@ module Prawn
81
99
  @gradient_registry ||= {}
82
100
  end
83
101
 
84
- def gradient(*args)
102
+ def gradient(args, opts)
85
103
  if args.length != 4 && args.length != 6
86
104
  fail ArgumentError, "Unknown type of gradient: #{args.inspect}"
87
105
  end
88
106
 
107
+ if opts[:apply_transformations].nil? && current_transformation_matrix_with_translation(0, 0) != [1, 0, 0, 1, 0, 0]
108
+ warn "Gradients in Prawn 2.x and lower are not correctly positioned when a transformation has been made to the document. Pass 'apply_transformations: true' to correctly transform the gradient, or see https://github.com/prawnpdf/prawn/wiki/Gradient-Transformations for more information."
109
+ end
110
+
89
111
  color1 = normalize_color(args[-2]).dup.freeze
90
112
  color2 = normalize_color(args[-1]).dup.freeze
91
113
 
@@ -104,10 +126,12 @@ module Prawn
104
126
  :N => 1.0
105
127
  )
106
128
 
129
+ x1, y1, x2, y2, transformation = gradient_coordinates(args, opts)
130
+
107
131
  if args.length == 4
108
- coords = [0, 0, args[1].first - args[0].first, args[1].last - args[0].last]
132
+ coords = [0, 0, x2 - x1, y2 - y1]
109
133
  else
110
- coords = [0, 0, args[1], args[2].first - args[0].first, args[2].last - args[0].last, args[3]]
134
+ coords = [0, 0, args[1], x2 - x1, y2 - y1, args[3]]
111
135
  end
112
136
 
113
137
  shading = ref!(
@@ -121,10 +145,22 @@ module Prawn
121
145
  ref!(
122
146
  :PatternType => 2, # shading pattern
123
147
  :Shading => shading,
124
- :Matrix => [1, 0,
125
- 0, 1] + map_to_absolute(args[0])
148
+ :Matrix => transformation
126
149
  )
127
150
  end
151
+
152
+ def gradient_coordinates(args, opts)
153
+ x1, y1 = map_to_absolute(args[0])
154
+ x2, y2 = map_to_absolute(args[args.length == 4 ? 1 : 2])
155
+
156
+ transformation = if opts[:apply_transformations]
157
+ current_transformation_matrix_with_translation(x1, y1)
158
+ else
159
+ [1, 0, 0, 1, x1, y1]
160
+ end
161
+
162
+ [x1, y1, x2, y2, transformation]
163
+ end
128
164
  end
129
165
  end
130
166
  end
@@ -145,6 +145,9 @@ module Prawn
145
145
  def transformation_matrix(a, b, c, d, e, f)
146
146
  values = [a, b, c, d, e, f].map { |x| "%.5f" % x }.join(" ")
147
147
  save_graphics_state if block_given?
148
+
149
+ add_to_transformation_stack(a, b, c, d, e, f)
150
+
148
151
  renderer.add_content "#{values} cm"
149
152
  if block_given?
150
153
  yield
@@ -68,9 +68,7 @@ module Prawn
68
68
  @transparency = {}
69
69
  case @color_type
70
70
  when 3
71
- fail Errors::UnsupportedImageType,
72
- "Pallete-based transparency in PNG is not currently supported.\n" \
73
- "See https://github.com/prawnpdf/prawn/issues/783"
71
+ @transparency[:palette] = data.read(chunk_size).unpack('C*')
74
72
  when 0
75
73
  # Greyscale. Corresponding to entries in the PLTE chunk.
76
74
  # Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
@@ -109,11 +107,20 @@ module Prawn
109
107
  # where it's required.
110
108
  #
111
109
  def split_alpha_channel!
112
- split_image_data if alpha_channel?
110
+ if alpha_channel?
111
+ if color_type == 3
112
+ generate_alpha_channel
113
+ else
114
+ split_image_data
115
+ end
116
+ end
113
117
  end
114
118
 
115
119
  def alpha_channel?
116
- @color_type == 4 || @color_type == 6
120
+ return true if color_type == 4 || color_type == 6
121
+ return @transparency.any? if color_type == 3
122
+
123
+ false
117
124
  end
118
125
 
119
126
  # Build a PDF object representing this image in +document+, and return
@@ -285,6 +292,37 @@ module Prawn
285
292
 
286
293
  @img_data = color_data
287
294
  end
295
+
296
+ def generate_alpha_channel
297
+ alpha_palette = Hash.new(0xff)
298
+ 0.upto(palette.bytesize / 3) do |n|
299
+ alpha_palette[n] = @transparency[:palette][n] || 0xff
300
+ end
301
+
302
+ scanline_length = width + 1
303
+ scanlines = @img_data.bytesize / scanline_length
304
+ pixels = width * height
305
+
306
+ data = StringIO.new(@img_data)
307
+ data.binmode
308
+
309
+ @alpha_channel = [0x00].pack('C') * (pixels + scanlines)
310
+ alpha = StringIO.new(@alpha_channel)
311
+ alpha.binmode
312
+
313
+ scanlines.times do |line|
314
+ data.seek(line * scanline_length)
315
+
316
+ filter = data.getbyte
317
+
318
+ alpha.putc filter
319
+
320
+ width.times do
321
+ color = data.read(1).unpack('C').first
322
+ alpha.putc alpha_palette[color]
323
+ end
324
+ end
325
+ end
288
326
  end
289
327
  end
290
328
  end
@@ -16,6 +16,7 @@ module Prawn
16
16
  attr_reader :max_line_height
17
17
  attr_reader :max_descender
18
18
  attr_reader :max_ascender
19
+ attr_reader :finalized
19
20
  attr_accessor :consumed
20
21
 
21
22
  # The following present only for testing purposes
@@ -31,34 +32,38 @@ module Prawn
31
32
  end
32
33
 
33
34
  def space_count
34
- if @unfinalized_line
35
+ unless finalized
35
36
  fail "Lines must be finalized before calling #space_count"
36
37
  end
38
+
37
39
  @fragments.inject(0) do |sum, fragment|
38
40
  sum + fragment.space_count
39
41
  end
40
42
  end
41
43
 
42
44
  def line_width
43
- if @unfinalized_line
45
+ unless finalized
44
46
  fail "Lines must be finalized before calling #line_width"
45
47
  end
48
+
46
49
  @fragments.inject(0) do |sum, fragment|
47
50
  sum + fragment.width
48
51
  end
49
52
  end
50
53
 
51
54
  def line
52
- if @unfinalized_line
55
+ unless finalized
53
56
  fail "Lines must be finalized before calling #line"
54
57
  end
58
+
55
59
  @fragments.collect do |fragment|
56
60
  fragment.text.dup.force_encoding(::Encoding::UTF_8)
57
61
  end.join
58
62
  end
59
63
 
60
64
  def finalize_line
61
- @unfinalized_line = false
65
+ @finalized = true
66
+
62
67
  omit_trailing_whitespace_from_line_width
63
68
  @fragments = []
64
69
  @consumed.each do |hash|
@@ -85,7 +90,7 @@ module Prawn
85
90
  end
86
91
 
87
92
  def initialize_line
88
- @unfinalized_line = true
93
+ @finalized = false
89
94
  @max_line_height = 0
90
95
  @max_descender = 0
91
96
  @max_ascender = 0
@@ -99,24 +104,26 @@ module Prawn
99
104
  end
100
105
 
101
106
  def next_string
102
- unless @unfinalized_line
107
+ if finalized
103
108
  fail "Lines must not be finalized when calling #next_string"
104
109
  end
105
- hash = @unconsumed.shift
106
- if hash.nil?
107
- nil
108
- else
109
- @consumed << hash.dup
110
- @current_format_state = hash.dup
110
+
111
+ next_unconsumed_hash = @unconsumed.shift
112
+
113
+ if next_unconsumed_hash
114
+ @consumed << next_unconsumed_hash.dup
115
+ @current_format_state = next_unconsumed_hash.dup
111
116
  @current_format_state.delete(:text)
112
- hash[:text]
117
+
118
+ next_unconsumed_hash[:text]
113
119
  end
114
120
  end
115
121
 
116
122
  def preview_next_string
117
- hash = @unconsumed.first
118
- if hash.nil? then nil
119
- else hash[:text]
123
+ next_unconsumed_hash = @unconsumed.first
124
+
125
+ if next_unconsumed_hash
126
+ next_unconsumed_hash[:text]
120
127
  end
121
128
  end
122
129
 
@@ -181,9 +188,10 @@ module Prawn
181
188
  end
182
189
 
183
190
  def retrieve_fragment
184
- if @unfinalized_line
191
+ unless finalized
185
192
  fail "Lines must be finalized before fragments can be retrieved"
186
193
  end
194
+
187
195
  @fragments.shift
188
196
  end
189
197