prawn-svg 0.35.1 → 0.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -5
  3. data/README.md +4 -7
  4. data/lib/prawn/svg/attributes/opacity.rb +23 -8
  5. data/lib/prawn/svg/attributes/stroke.rb +7 -9
  6. data/lib/prawn/svg/attributes/transform.rb +1 -1
  7. data/lib/prawn/svg/calculators/pixels.rb +9 -22
  8. data/lib/prawn/svg/color.rb +58 -59
  9. data/lib/prawn/svg/css/font_parser.rb +46 -0
  10. data/lib/prawn/svg/document.rb +5 -1
  11. data/lib/prawn/svg/elements/base.rb +22 -30
  12. data/lib/prawn/svg/elements/gradient.rb +99 -74
  13. data/lib/prawn/svg/elements/line.rb +1 -1
  14. data/lib/prawn/svg/elements/marker.rb +2 -0
  15. data/lib/prawn/svg/elements/root.rb +1 -1
  16. data/lib/prawn/svg/elements/text_component.rb +3 -3
  17. data/lib/prawn/svg/funciri.rb +14 -0
  18. data/lib/prawn/svg/gradient_renderer.rb +313 -0
  19. data/lib/prawn/svg/length.rb +43 -0
  20. data/lib/prawn/svg/paint.rb +67 -0
  21. data/lib/prawn/svg/percentage.rb +24 -0
  22. data/lib/prawn/svg/properties.rb +208 -104
  23. data/lib/prawn/svg/renderer.rb +5 -0
  24. data/lib/prawn/svg/state.rb +5 -3
  25. data/lib/prawn/svg/transform_parser.rb +19 -13
  26. data/lib/prawn/svg/transform_utils.rb +37 -0
  27. data/lib/prawn/svg/version.rb +1 -1
  28. data/lib/prawn-svg.rb +7 -3
  29. data/prawn-svg.gemspec +4 -4
  30. data/spec/prawn/svg/attributes/opacity_spec.rb +27 -20
  31. data/spec/prawn/svg/attributes/transform_spec.rb +5 -2
  32. data/spec/prawn/svg/calculators/pixels_spec.rb +1 -2
  33. data/spec/prawn/svg/color_spec.rb +22 -52
  34. data/spec/prawn/svg/elements/base_spec.rb +9 -10
  35. data/spec/prawn/svg/elements/gradient_spec.rb +92 -36
  36. data/spec/prawn/svg/elements/marker_spec.rb +13 -15
  37. data/spec/prawn/svg/funciri_spec.rb +59 -0
  38. data/spec/prawn/svg/length_spec.rb +89 -0
  39. data/spec/prawn/svg/paint_spec.rb +96 -0
  40. data/spec/prawn/svg/pathable_spec.rb +3 -3
  41. data/spec/prawn/svg/pdfmatrix_spec.rb +60 -0
  42. data/spec/prawn/svg/percentage_spec.rb +60 -0
  43. data/spec/prawn/svg/properties_spec.rb +124 -107
  44. data/spec/prawn/svg/transform_parser_spec.rb +13 -13
  45. data/spec/sample_svg/gradient_stress_test.svg +115 -0
  46. metadata +17 -5
  47. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +0 -23
@@ -0,0 +1,67 @@
1
+ module Prawn::SVG
2
+ Paint = Struct.new(:color, :url) do
3
+ class << self
4
+ def none
5
+ new(:none, nil)
6
+ end
7
+
8
+ def black
9
+ new(Color.black, nil)
10
+ end
11
+
12
+ def parse(value)
13
+ case CSS::ValuesParser.parse(value)
14
+ in [['url', [url]]]
15
+ # If there is no fallback color, and the URL is unresolvable, the spec says that the document is in error.
16
+ # Chrome appears to treat this the same as an explicit 'none', so we do the same.
17
+ new(:none, url)
18
+ in [['url', [url]], keyword_or_color]
19
+ parse_keyword_or_color(keyword_or_color, url)
20
+ in [['url', [url]], keyword_or_color, ['icc-color', _args]] # rubocop:disable Lint/DuplicateBranch
21
+ parse_keyword_or_color(keyword_or_color, url)
22
+ in [keyword_or_color, ['icc-color', _args]]
23
+ parse_keyword_or_color(keyword_or_color, nil)
24
+ in [keyword_or_color] # rubocop:disable Lint/DuplicateBranch
25
+ parse_keyword_or_color(keyword_or_color, nil)
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def parse_keyword_or_color(value, url)
34
+ if value.is_a?(String)
35
+ keyword = value.downcase
36
+ return new(keyword.to_sym, url) if ['none', 'currentcolor'].include?(keyword)
37
+ end
38
+
39
+ color = Color.parse(value)
40
+ new(color, url) if color
41
+ end
42
+ end
43
+
44
+ def none?
45
+ color == :none && (url.nil? || !!@unresolved_url)
46
+ end
47
+
48
+ def resolve(gradients, current_color, color_mode)
49
+ if url
50
+ if url[0] == '#' && gradients && (gradient = gradients[url[1..]])
51
+ return gradient
52
+ else
53
+ @unresolved_url = true
54
+ end
55
+ end
56
+
57
+ case color
58
+ when :currentcolor
59
+ current_color
60
+ when :none
61
+ nil
62
+ else
63
+ color_mode == :cmyk ? color.to_cmyk : color
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ Prawn::SVG::Percentage = Struct.new(:value)
2
+
3
+ class Prawn::SVG::Percentage
4
+ REGEXP = /\A([+-]?\d*(?:\.\d+)?)%\z/i.freeze
5
+
6
+ def self.parse(value, positive_only: false)
7
+ if (matches = value.match(REGEXP))
8
+ number = Float(matches[1], exception: false)
9
+ new(number) if number && (!positive_only || number >= 0)
10
+ end
11
+ end
12
+
13
+ def to_f
14
+ value
15
+ end
16
+
17
+ def to_factor
18
+ value / 100.0
19
+ end
20
+
21
+ def to_pixels(axis_length, _font_size)
22
+ value * axis_length / 100.0
23
+ end
24
+ end
@@ -1,127 +1,231 @@
1
- class Prawn::SVG::Properties
2
- Config = Struct.new(:default, :inheritable?, :keywords, :keyword_restricted?, :attr, :ivar)
3
-
4
- EM = 16
5
- FONT_SIZES = {
6
- 'xx-small' => EM / 4,
7
- 'x-small' => EM / 4 * 2,
8
- 'small' => EM / 4 * 3,
9
- 'medium' => EM / 4 * 4,
10
- 'large' => EM / 4 * 5,
11
- 'x-large' => EM / 4 * 6,
12
- 'xx-large' => EM / 4 * 7
13
- }.freeze
14
-
15
- PROPERTIES = {
16
- 'clip-path' => Config.new('none', false, %w[inherit none]),
17
- 'color' => Config.new('', true),
18
- 'display' => Config.new('inline', false, %w[inherit inline none], true),
19
- 'fill' => Config.new('black', true, %w[inherit none currentColor]),
20
- 'fill-opacity' => Config.new('1', true),
21
- 'fill-rule' => Config.new('nonzero', true, %w[inherit nonzero evenodd]),
22
- 'font-family' => Config.new('sans-serif', true),
23
- 'font-size' => Config.new('medium', true,
24
- %w[inherit xx-small x-small small medium large x-large xx-large larger smaller]),
25
- 'font-style' => Config.new('normal', true, %w[inherit normal italic oblique], true),
26
- 'font-variant' => Config.new('normal', true, %w[inherit normal small-caps], true),
27
- 'font-weight' => Config.new('normal', true, %w[inherit normal bold 100 200 300 400 500 600 700 800 900], true), # bolder/lighter not supported
28
- 'letter-spacing' => Config.new('normal', true, %w[inherit normal]),
29
- 'marker-end' => Config.new('none', true, %w[inherit none]),
30
- 'marker-mid' => Config.new('none', true, %w[inherit none]),
31
- 'marker-start' => Config.new('none', true, %w[inherit none]),
32
- 'opacity' => Config.new('1', false),
33
- 'overflow' => Config.new('visible', false, %w[inherit visible hidden scroll auto], true),
34
- 'stop-color' => Config.new('black', false, %w[inherit none currentColor]),
35
- 'stroke' => Config.new('none', true, %w[inherit none currentColor]),
36
- 'stroke-dasharray' => Config.new('none', true, %w[inherit none]),
37
- 'stroke-linecap' => Config.new('butt', true, %w[inherit butt round square], true),
38
- 'stroke-linejoin' => Config.new('miter', true, %w[inherit miter round bevel], true),
39
- 'stroke-opacity' => Config.new('1', true),
40
- 'stroke-width' => Config.new('1', true),
41
- 'text-anchor' => Config.new('start', true, %w[inherit start middle end], true),
42
- 'text-decoration' => Config.new('none', true, %w[inherit none underline], true),
43
- 'dominant-baseline' => Config.new('auto', true, %w[inherit auto middle], true)
44
- }.freeze
45
-
46
- PROPERTIES.each do |name, value|
47
- value.attr = name.gsub('-', '_')
48
- value.ivar = "@#{value.attr}"
49
- end
1
+ module Prawn::SVG
2
+ class Properties
3
+ Config = Struct.new(:default, :inheritable?, :valid_values, :attr, :ivar)
4
+
5
+ EM = 16
6
+ FONT_SIZES = {
7
+ 'xx-small' => EM / 4,
8
+ 'x-small' => EM / 4 * 2,
9
+ 'small' => EM / 4 * 3,
10
+ 'medium' => EM / 4 * 4,
11
+ 'large' => EM / 4 * 5,
12
+ 'x-large' => EM / 4 * 6,
13
+ 'xx-large' => EM / 4 * 7
14
+ }.freeze
15
+
16
+ PROPERTIES = {
17
+ 'clip-path' => Config.new('none', false, ['none', :funciri]),
18
+ 'color' => Config.new(Color.black, true, [:color]),
19
+ 'display' => Config.new('inline', false, %w[inline none]),
20
+ 'dominant-baseline' => Config.new('auto', true, %w[auto middle]),
21
+ 'fill' => Config.new(Paint.black, true, [:paint]),
22
+ 'fill-opacity' => Config.new(1.0, true, [:number]),
23
+ 'fill-rule' => Config.new('nonzero', true, %w[nonzero evenodd]),
24
+ 'font-family' => Config.new('sans-serif', true, [:any]),
25
+ # Only the computed (numeric) value of font-size is inherited, not the value itself
26
+ 'font-size' => Config.new(nil, false,
27
+ [:positive_length, :positive_percentage, 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller']),
28
+ 'font-style' => Config.new('normal', true, %w[normal italic oblique]),
29
+ 'font-variant' => Config.new('normal', true, %w[normal small-caps]),
30
+ 'font-weight' => Config.new('normal', true, %w[normal bold 100 200 300 400 500 600 700 800 900]), # bolder/lighter not supported
31
+ 'letter-spacing' => Config.new('normal', true, [:length, 'normal']),
32
+ 'marker-end' => Config.new('none', true, [:funciri, 'none']),
33
+ 'marker-mid' => Config.new('none', true, [:funciri, 'none']),
34
+ 'marker-start' => Config.new('none', true, [:funciri, 'none']),
35
+ 'opacity' => Config.new(1.0, false, [:number]),
36
+ 'overflow' => Config.new('visible', false, %w[visible hidden scroll auto]),
37
+ 'stop-color' => Config.new(Color.black, false, [:color_with_icc, 'currentcolor']),
38
+ 'stop-opacity' => Config.new(1.0, false, [:number]),
39
+ 'stroke' => Config.new(Paint.none, true, [:paint]),
40
+ 'stroke-dasharray' => Config.new('none', true, [:dasharray, 'none']),
41
+ 'stroke-linecap' => Config.new('butt', true, %w[butt round square]),
42
+ 'stroke-linejoin' => Config.new('miter', true, %w[miter round bevel]),
43
+ 'stroke-opacity' => Config.new(1.0, true, [:number]),
44
+ 'stroke-width' => Config.new(1.0, true, [:positive_length, :positive_percentage]),
45
+ 'text-anchor' => Config.new('start', true, %w[start middle end]),
46
+ 'text-decoration' => Config.new('none', true, %w[none underline]),
47
+ 'visibility' => Config.new('visible', true, %w[visible hidden collapse])
48
+ }.freeze
49
+
50
+ PROPERTIES.each do |name, value|
51
+ value.attr = name.gsub('-', '_')
52
+ value.ivar = "@#{value.attr}"
53
+ end
50
54
 
51
- PROPERTY_CONFIGS = PROPERTIES.values
52
- NAMES = PROPERTIES.keys
53
- ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
55
+ PROPERTY_CONFIGS = PROPERTIES.values
56
+ NAMES = PROPERTIES.keys
57
+ ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
54
58
 
55
- attr_accessor(*ATTR_NAMES)
59
+ attr_accessor(*ATTR_NAMES)
56
60
 
57
- def load_default_stylesheet
58
- PROPERTY_CONFIGS.each do |config|
59
- instance_variable_set(config.ivar, config.default)
61
+ def initialize
62
+ @numeric_font_size = EM
60
63
  end
61
64
 
62
- self
63
- end
65
+ def load_default_stylesheet
66
+ PROPERTY_CONFIGS.each do |config|
67
+ instance_variable_set(config.ivar, config.default)
68
+ end
64
69
 
65
- def set(name, value)
66
- if (config = PROPERTIES[name.to_s.downcase])
67
- value = value.strip
68
- keyword = value.downcase
69
- keywords = config.keywords || ['inherit']
70
+ self
71
+ end
70
72
 
71
- if keywords.include?(keyword)
72
- value = keyword
73
- elsif config.keyword_restricted?
74
- value = config.default
73
+ def set(name, value)
74
+ name = name.to_s.downcase
75
+ if (config = PROPERTIES[name])
76
+ if (value = parse_value(config, value.strip))
77
+ instance_variable_set(config.ivar, value)
78
+ end
79
+ elsif name == 'font'
80
+ apply_font_shorthand(value)
75
81
  end
82
+ end
76
83
 
77
- instance_variable_set(config.ivar, value)
84
+ def numeric_font_size
85
+ @numeric_font_size or raise 'numeric_font_size not set; this is only present in computed properties'
86
+ end
87
+
88
+ def to_h
89
+ PROPERTIES.each.with_object({}) do |(name, config), result|
90
+ result[name] = instance_variable_get(config.ivar)
91
+ end
78
92
  end
79
- end
80
93
 
81
- def to_h
82
- PROPERTIES.each.with_object({}) do |(name, config), result|
83
- result[name] = instance_variable_get(config.ivar)
94
+ def load_hash(hash)
95
+ hash.each { |name, value| set(name, value) if value }
84
96
  end
85
- end
86
97
 
87
- def load_hash(hash)
88
- hash.each { |name, value| set(name, value) if value }
89
- end
98
+ def compute_properties(other)
99
+ PROPERTY_CONFIGS.each do |config|
100
+ value = other.send(config.attr)
101
+
102
+ if value && value != 'inherit'
103
+ instance_variable_set(config.ivar, value)
90
104
 
91
- def compute_properties(other)
92
- PROPERTY_CONFIGS.each do |config|
93
- value = other.send(config.attr)
105
+ elsif value.nil? && !config.inheritable?
106
+ instance_variable_set(config.ivar, config.default)
107
+ end
108
+ end
94
109
 
95
- if value && value != 'inherit'
96
- value = compute_font_size_property(value).to_s if config.attr == 'font_size'
97
- instance_variable_set(config.ivar, value)
110
+ @numeric_font_size = calculate_numeric_font_size
111
+ nil
112
+ end
98
113
 
99
- elsif value.nil? && !config.inheritable?
100
- instance_variable_set(config.ivar, config.default)
114
+ private
115
+
116
+ def calculate_numeric_font_size
117
+ case font_size
118
+ when Length
119
+ font_size.to_pixels(nil, numeric_font_size)
120
+ when Percentage
121
+ font_size.to_factor * numeric_font_size
122
+ when Numeric
123
+ font_size
124
+ when 'larger'
125
+ numeric_font_size + 4
126
+ when 'smaller'
127
+ numeric_font_size - 4
128
+ when nil, 'inherit'
129
+ numeric_font_size
130
+ when String
131
+ FONT_SIZES[font_size] or raise "Unknown font size keyword: #{font_size}"
132
+ else
133
+ raise "Unknown font size property value: #{font_size.inspect}"
101
134
  end
102
135
  end
103
- end
104
136
 
105
- def numerical_font_size
106
- # px = pt for PDFs
107
- FONT_SIZES[font_size] || font_size.to_f
108
- end
137
+ def parse_value(config, value)
138
+ keyword = value.downcase
139
+
140
+ return 'inherit' if keyword == 'inherit'
141
+
142
+ config.valid_values.detect do |type|
143
+ result = parse_value_with_type(type, value, keyword)
144
+ break result if result
145
+ end
146
+ end
147
+
148
+ def parse_value_with_type(type, value, keyword)
149
+ case type
150
+ when String
151
+ keyword if type == keyword
152
+ when :color
153
+ values = Prawn::SVG::CSS::ValuesParser.parse(value)
154
+ Prawn::SVG::Color.parse(values[0]) if values.length == 1
155
+ when :color_with_icc
156
+ case Prawn::SVG::CSS::ValuesParser.parse(value)
157
+ in [other, ['icc-color', _args]]
158
+ Prawn::SVG::Color.parse(other)
159
+ in [other] # rubocop:disable Lint/DuplicateBranch
160
+ Prawn::SVG::Color.parse(other)
161
+ else
162
+ nil
163
+ end
164
+ when :paint
165
+ Paint.parse(value)
166
+ when :funciri
167
+ FuncIRI.parse(value)
168
+ when :dasharray
169
+ value.split(Prawn::SVG::Elements::COMMA_WSP_REGEXP).map do |value|
170
+ Length.parse(value) || Percentage.parse(value) || break
171
+ end
172
+ when :number
173
+ Float(value, exception: false)
174
+ when :length
175
+ Length.parse(value)
176
+ when :percentage
177
+ Percentage.parse(value)
178
+ when :positive_length
179
+ Length.parse(value, positive_only: true)
180
+ when :positive_percentage
181
+ Percentage.parse(value, positive_only: true)
182
+ when :any
183
+ value
184
+ else
185
+ raise "Unknown valid value type: #{type}"
186
+ end
187
+ end
188
+
189
+ FONT_KEYWORD_MAPPING = ['font-style', 'font-variant', 'font-weight'].each.with_object({}) do |property, result|
190
+ PROPERTIES[property].valid_values.each do |value|
191
+ result[value] = property
192
+ end
193
+ end
194
+
195
+ def apply_font_shorthand(value)
196
+ if value.strip.match(/\s/).nil?
197
+ case value.strip.downcase
198
+ when 'inherit'
199
+ load_hash('font-style' => 'inherit', 'font-variant' => 'inherit', 'font-weight' => 'inherit', 'font-size' => 'inherit', 'font-family' => 'inherit')
200
+ when 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'
201
+ load_hash('font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal', 'font-size' => 'medium', 'font-family' => 'sans-serif')
202
+ end
203
+
204
+ return
205
+ end
206
+
207
+ properties = ['font-style', 'font-variant', 'font-weight'].each.with_object({}) do |property, result|
208
+ result[property] = PROPERTIES[property].default
209
+ end
210
+
211
+ values = CSS::FontParser.parse(value)
212
+ return if values.length < 2
213
+
214
+ properties['font-family'] = values.pop
215
+ font_size = values.pop.sub(%r{/.*}, '')
216
+
217
+ values.each do |keyword|
218
+ keyword = keyword.downcase
219
+ if (property = FONT_KEYWORD_MAPPING[keyword])
220
+ properties[property] = keyword
221
+ else
222
+ break
223
+ end
224
+ end or return
109
225
 
110
- private
111
-
112
- def compute_font_size_property(value)
113
- if value[-1] == '%'
114
- numerical_font_size * (value.to_f / 100.0)
115
- elsif value == 'larger'
116
- numerical_font_size + 4
117
- elsif value == 'smaller'
118
- numerical_font_size - 4
119
- elsif value.match(/(\d|\.)em\z/i)
120
- numerical_font_size * value.to_f
121
- elsif value.match(/(\d|\.)rem\z/i)
122
- value.to_f * EM
123
- else
124
- FONT_SIZES[value] || value.to_f
226
+ set('font-size', font_size) or return
227
+ load_hash(properties)
228
+ value
125
229
  end
126
230
  end
127
231
  end
@@ -216,6 +216,11 @@ module Prawn
216
216
  Renderer.new(prawn, sub_document, sub_options).draw
217
217
  document.warnings.concat(sub_document.warnings)
218
218
  yield
219
+
220
+ when 'svg:render_gradient'
221
+ type = arguments.first
222
+ GradientRenderer.new(prawn, type, **kwarguments).draw
223
+ yield
219
224
  end
220
225
  end
221
226
 
@@ -1,14 +1,16 @@
1
1
  class Prawn::SVG::State
2
2
  attr_accessor :text, :preserve_space,
3
- :fill_opacity, :stroke_opacity, :stroke_width,
3
+ :opacity, :last_fill_opacity, :last_stroke_opacity,
4
+ :stroke_width,
4
5
  :computed_properties,
5
6
  :viewport_sizing,
6
7
  :inside_use, :inside_clip_path
7
8
 
8
9
  def initialize
9
10
  @stroke_width = 1
10
- @fill_opacity = 1
11
- @stroke_opacity = 1
11
+ @opacity = 1
12
+ @last_fill_opacity = 1
13
+ @last_stroke_opacity = 1
12
14
  @computed_properties = Prawn::SVG::Properties.new.load_default_stylesheet
13
15
  end
14
16
 
@@ -1,32 +1,38 @@
1
1
  module Prawn::SVG::TransformParser
2
- def parse_transform_attribute(transform)
2
+ include Prawn::SVG::PDFMatrix
3
+
4
+ def parse_transform_attribute(transform, space: :pdf)
3
5
  matrix = Matrix.identity(3)
4
6
 
7
+ flip = space == :svg ? 1.0 : -1.0
8
+
5
9
  parse_css_method_calls(transform).each do |name, arguments|
6
10
  case name
7
11
  when 'translate'
8
12
  x, y = arguments
9
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
13
+ matrix *= translation_matrix(x_pixels(x.to_f), flip * y_pixels(y.to_f))
10
14
 
11
15
  when 'translateX'
12
16
  x = arguments.first
13
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, 0], [0, 0, 1]]
17
+ matrix *= translation_matrix(x_pixels(x.to_f), 0.0)
14
18
 
15
19
  when 'translateY'
16
20
  y = arguments.first
17
- matrix *= Matrix[[1, 0, 0], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
21
+ matrix *= translation_matrix(0.0, flip * y_pixels(y.to_f))
18
22
 
19
23
  when 'rotate'
20
24
  angle, x, y = arguments.collect(&:to_f)
21
25
  angle = angle * Math::PI / 180.0
22
26
 
27
+ rotation = rotation_matrix(angle, space: space)
28
+
23
29
  case arguments.length
24
30
  when 1
25
- matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
31
+ matrix *= rotation
26
32
  when 3
27
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
28
- matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
29
- matrix *= Matrix[[1, 0, -x_pixels(x.to_f)], [0, 1, y_pixels(y.to_f)], [0, 0, 1]]
33
+ matrix *= translation_matrix(x_pixels(x.to_f), flip * y_pixels(y.to_f))
34
+ matrix *= rotation
35
+ matrix *= translation_matrix(-x_pixels(x.to_f), -flip * y_pixels(y.to_f))
30
36
  else
31
37
  warnings << "transform 'rotate' must have either one or three arguments"
32
38
  end
@@ -34,20 +40,20 @@ module Prawn::SVG::TransformParser
34
40
  when 'scale'
35
41
  x_scale = arguments[0].to_f
36
42
  y_scale = (arguments[1] || x_scale).to_f
37
- matrix *= Matrix[[x_scale, 0, 0], [0, y_scale, 0], [0, 0, 1]]
43
+ matrix *= scale_matrix(x_scale, y_scale)
38
44
 
39
45
  when 'skewX'
40
46
  angle = arguments[0].to_f * Math::PI / 180.0
41
- matrix *= Matrix[[1, -Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
47
+ matrix *= Matrix[[1, flip * Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
42
48
 
43
49
  when 'skewY'
44
50
  angle = arguments[0].to_f * Math::PI / 180.0
45
- matrix *= Matrix[[1, 0, 0], [-Math.tan(angle), 1, 0], [0, 0, 1]]
51
+ matrix *= Matrix[[1, 0, 0], [flip * Math.tan(angle), 1, 0], [0, 0, 1]]
46
52
 
47
53
  when 'matrix'
48
54
  if arguments.length == 6
49
55
  a, b, c, d, e, f = arguments.collect(&:to_f)
50
- matrix *= Matrix[[a, -c, e], [-b, d, -f], [0, 0, 1]]
56
+ matrix *= Matrix[[a, flip * c, e], [flip * b, d, flip * f], [0, 0, 1]]
51
57
  else
52
58
  warnings << "transform 'matrix' must have six arguments"
53
59
  end
@@ -57,7 +63,7 @@ module Prawn::SVG::TransformParser
57
63
  end
58
64
  end
59
65
 
60
- matrix.to_a[0..1].transpose.flatten
66
+ matrix
61
67
  end
62
68
 
63
69
  private
@@ -0,0 +1,37 @@
1
+ module Prawn::SVG::PDFMatrix
2
+ def load_matrix(matrix)
3
+ if matrix.is_a?(Matrix) && matrix.row_count == 3 && matrix.column_count == 3
4
+ matrix
5
+ elsif matrix.is_a?(Array) && matrix.length == 6
6
+ Matrix[
7
+ [matrix[0], matrix[2], matrix[4]],
8
+ [matrix[1], matrix[3], matrix[5]],
9
+ [0.0, 0.0, 1.0]
10
+ ]
11
+ else
12
+ raise ArgumentError, 'unexpected matrix format'
13
+ end
14
+ end
15
+
16
+ def matrix_for_pdf(matrix)
17
+ matrix.to_a[0..1].transpose.flatten
18
+ end
19
+
20
+ def translation_matrix(x = 0, y = 0)
21
+ Matrix[[1.0, 0.0, x.to_f], [0.0, 1.0, y.to_f], [0.0, 0.0, 1.0]]
22
+ end
23
+
24
+ def scale_matrix(x = 1, y = x)
25
+ Matrix[[x.to_f, 0.0, 0.0], [0.0, y.to_f, 0.0], [0.0, 0.0, 1.0]]
26
+ end
27
+
28
+ def rotation_matrix(angle, space: :pdf)
29
+ dir = space == :svg ? 1.0 : -1.0
30
+
31
+ Matrix[
32
+ [Math.cos(angle), -dir * Math.sin(angle), 0],
33
+ [dir * Math.sin(angle), Math.cos(angle), 0],
34
+ [0, 0, 1]
35
+ ]
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.35.1'.freeze
3
+ VERSION = '0.36.0'.freeze
4
4
  end
5
5
  end
data/lib/prawn-svg.rb CHANGED
@@ -10,12 +10,17 @@ require 'prawn/svg/calculators/arc_to_bezier_curve'
10
10
  require 'prawn/svg/calculators/aspect_ratio'
11
11
  require 'prawn/svg/calculators/document_sizing'
12
12
  require 'prawn/svg/calculators/pixels'
13
+ require 'prawn/svg/transform_utils'
13
14
  require 'prawn/svg/transform_parser'
14
15
  require 'prawn/svg/url_loader'
15
16
  require 'prawn/svg/loaders/data'
16
17
  require 'prawn/svg/loaders/file'
17
18
  require 'prawn/svg/loaders/web'
18
19
  require 'prawn/svg/color'
20
+ require 'prawn/svg/funciri'
21
+ require 'prawn/svg/length'
22
+ require 'prawn/svg/paint'
23
+ require 'prawn/svg/percentage'
19
24
  require 'prawn/svg/attributes'
20
25
  require 'prawn/svg/properties'
21
26
  require 'prawn/svg/pathable'
@@ -23,6 +28,7 @@ require 'prawn/svg/elements'
23
28
  require 'prawn/svg/extension'
24
29
  require 'prawn/svg/renderer'
25
30
  require 'prawn/svg/interface'
31
+ require 'prawn/svg/css/font_parser'
26
32
  require 'prawn/svg/css/font_family_parser'
27
33
  require 'prawn/svg/css/selector_parser'
28
34
  require 'prawn/svg/css/stylesheets'
@@ -30,12 +36,10 @@ require 'prawn/svg/css/values_parser'
30
36
  require 'prawn/svg/ttf'
31
37
  require 'prawn/svg/font'
32
38
  require 'prawn/svg/gradients'
39
+ require 'prawn/svg/gradient_renderer'
33
40
  require 'prawn/svg/document'
34
41
  require 'prawn/svg/state'
35
42
 
36
- require 'prawn/svg/extensions/additional_gradient_transforms'
37
- Prawn::Document.prepend Prawn::SVG::Extensions::AdditionalGradientTransforms
38
-
39
43
  module Prawn
40
44
  Svg = SVG # backwards compatibility
41
45
  end
data/prawn-svg.gemspec CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.required_ruby_version = '>= 2.7.0'
21
21
 
22
- gem.add_runtime_dependency 'css_parser', '~> 1.6'
23
- gem.add_runtime_dependency 'matrix', '~> 0.4.2'
24
- gem.add_runtime_dependency 'prawn', '>= 0.11.1', '< 3'
25
- gem.add_runtime_dependency 'rexml', '>=3.2.0', '< 4'
22
+ gem.add_dependency 'css_parser', '~> 1.6'
23
+ gem.add_dependency 'matrix', '~> 0.4.2'
24
+ gem.add_dependency 'prawn', '>= 0.11.1', '< 3'
25
+ gem.add_dependency 'rexml', '>= 3.3.9', '< 4'
26
26
  end