savage-transform 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +22 -0
  4. data/.rspec +1 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +108 -0
  7. data/Rakefile +1 -0
  8. data/VERSION +1 -0
  9. data/lib/savage.rb +3 -0
  10. data/lib/savage/direction.rb +56 -0
  11. data/lib/savage/direction_proxy.rb +19 -0
  12. data/lib/savage/directions/arc_to.rb +30 -0
  13. data/lib/savage/directions/close_path.rb +22 -0
  14. data/lib/savage/directions/coordinate_target.rb +21 -0
  15. data/lib/savage/directions/cubic_curve_to.rb +47 -0
  16. data/lib/savage/directions/horizontal_to.rb +31 -0
  17. data/lib/savage/directions/line_to.rb +15 -0
  18. data/lib/savage/directions/move_to.rb +15 -0
  19. data/lib/savage/directions/point_target.rb +22 -0
  20. data/lib/savage/directions/quadratic_curve_to.rb +44 -0
  21. data/lib/savage/directions/vertical_to.rb +31 -0
  22. data/lib/savage/parser.rb +108 -0
  23. data/lib/savage/path.rb +66 -0
  24. data/lib/savage/sub_path.rb +78 -0
  25. data/lib/savage/transformable.rb +59 -0
  26. data/lib/savage/utils.rb +7 -0
  27. data/savage-transform.gemspec +26 -0
  28. data/spec/savage/directions/arc_to_spec.rb +97 -0
  29. data/spec/savage/directions/close_path_spec.rb +30 -0
  30. data/spec/savage/directions/cubic_curve_to_spec.rb +146 -0
  31. data/spec/savage/directions/horizontal_to_spec.rb +10 -0
  32. data/spec/savage/directions/line_to_spec.rb +14 -0
  33. data/spec/savage/directions/move_to_spec.rb +10 -0
  34. data/spec/savage/directions/point_spec.rb +12 -0
  35. data/spec/savage/directions/quadratic_curve_spec.rb +123 -0
  36. data/spec/savage/directions/vertical_to_spec.rb +10 -0
  37. data/spec/savage/parser_spec.rb +250 -0
  38. data/spec/savage/path_spec.rb +105 -0
  39. data/spec/savage/sub_path_spec.rb +195 -0
  40. data/spec/savage/transformable_spec.rb +99 -0
  41. data/spec/savage_spec.rb +5 -0
  42. data/spec/shared/command.rb +13 -0
  43. data/spec/shared/coordinate_target.rb +36 -0
  44. data/spec/shared/direction.rb +29 -0
  45. data/spec/shared/point_target.rb +45 -0
  46. data/spec/spec_helper.rb +36 -0
  47. metadata +153 -0
@@ -0,0 +1,22 @@
1
+ module Savage
2
+ module Directions
3
+ class PointTarget < Direction
4
+
5
+ attr_accessor :target
6
+
7
+ def initialize(x, y, absolute=true)
8
+ @target = Point.new(x,y)
9
+ super(absolute)
10
+ end
11
+
12
+ def to_a
13
+ [command_code, @target.x, @target.y]
14
+ end
15
+
16
+ def movement
17
+ [target.x, target.y]
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ module Savage
2
+ module Directions
3
+ class QuadraticCurveTo < PointTarget
4
+ attr_accessor :control
5
+
6
+ def initialize(*args)
7
+ raise ArgumentError if args.length < 2
8
+ case args.length
9
+ when 2
10
+ super(args[0],args[1],true)
11
+ when 3
12
+ raise ArgumentError if args[2].kind_of?(Numeric)
13
+ super(args[0],args[1],args[2])
14
+ when 4
15
+ @control = Point.new(args[0],args[1])
16
+ super(args[2],args[3],true)
17
+ when 5
18
+ @control = Point.new(args[0],args[1])
19
+ super(args[2],args[3],args[4])
20
+ end
21
+ end
22
+
23
+ def to_a
24
+ if @control
25
+ [command_code, @control.x, @control.y, @target.x, @target.y]
26
+ else
27
+ [command_code, @target.x, @target.y]
28
+ end
29
+ end
30
+
31
+ def command_code
32
+ return (absolute?) ? 'Q' : 'q' if @control
33
+ (absolute?) ? 'T' : 't'
34
+ end
35
+
36
+ def transform(scale_x, skew_x, skew_y, scale_y, tx, ty)
37
+ # relative line_to dont't need to be tranlated
38
+ tx = ty = 0 if relative?
39
+ transform_dot( target, scale_x, skew_x, skew_y, scale_y, tx, ty)
40
+ transform_dot( control, scale_x, skew_x, skew_y, scale_y, tx, ty) if control
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ module Savage
2
+ module Directions
3
+ class VerticalTo < CoordinateTarget
4
+ def command_code
5
+ (absolute?) ? 'V' : 'v'
6
+ end
7
+
8
+ def transform(scale_x, skew_x, skew_y, scale_y, tx, ty)
9
+
10
+ unless skew_x.zero?
11
+ raise 'rotating or skewing (in X axis) an "vertical_to" direction is not supported yet.'
12
+ end
13
+
14
+ self.target *= scale_y
15
+ self.target += ty if absolute?
16
+ end
17
+
18
+ def to_fully_transformable_dir( pen_x, pen_y )
19
+ if absolute?
20
+ LineTo.new( pen_x, target, true )
21
+ else
22
+ LineTo.new( 0, target, false )
23
+ end
24
+ end
25
+
26
+ def movement
27
+ [nil, target]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,108 @@
1
+ module Savage
2
+ class Parser
3
+ DIRECTIONS = {
4
+ :m => {:class => Directions::MoveTo,
5
+ :args => 2},
6
+ :l => {:class => Directions::LineTo,
7
+ :args => 2},
8
+ :h => {:class => Directions::HorizontalTo,
9
+ :args => 1},
10
+ :v => {:class => Directions::VerticalTo,
11
+ :args => 1},
12
+ :c => {:class => Directions::CubicCurveTo,
13
+ :args => 6},
14
+ :s => {:class => Directions::CubicCurveTo,
15
+ :args => 4},
16
+ :q => {:class => Directions::QuadraticCurveTo,
17
+ :args => 4},
18
+ :t => {:class => Directions::QuadraticCurveTo,
19
+ :args => 2},
20
+ :a => {:class => Directions::ArcTo,
21
+ :args => 7}
22
+ }
23
+
24
+ class << self
25
+ def parse(parsable)
26
+ raise TypeError if parsable.class != String
27
+ subpaths = extract_subpaths parsable
28
+ raise TypeError if (subpaths.empty?)
29
+ path = Path.new
30
+ path.subpaths = []
31
+ subpaths.each_with_index do |subpath, i|
32
+ path.subpaths << parse_subpath(subpath, i == 0)
33
+ end
34
+ path
35
+ end
36
+
37
+ private
38
+ def extract_subpaths(parsable)
39
+ subpaths = []
40
+ if move_index = parsable.index(/[Mm]/)
41
+ subpaths << parsable[0...move_index] if move_index > 0
42
+ parsable.scan(/[Mm](?:\d|[eE.,+-]|[LlHhVvQqCcTtSsAaZz]|\W)+/m) do |match_group|
43
+ subpaths << $&
44
+ end
45
+ else
46
+ subpaths << parsable
47
+ end
48
+ subpaths
49
+ end
50
+
51
+ def parse_subpath(parsable, force_absolute=false)
52
+ subpath = SubPath.new
53
+ subpath.directions = extract_directions parsable, force_absolute
54
+ subpath
55
+ end
56
+
57
+ def extract_directions(parsable, force_absolute=false)
58
+ directions = []
59
+ i = 0
60
+ parsable.scan(/[MmLlHhVvQqCcTtSsAaZz](?:\d|[eE.,+-]|\W)*/m) do |match_group|
61
+ direction = build_direction $&, force_absolute && i == 0
62
+ if direction.kind_of?(Array)
63
+ directions.concat direction
64
+ else
65
+ directions << direction
66
+ end
67
+ i += 1
68
+ end
69
+ directions
70
+ end
71
+
72
+ def build_direction(parsable, force_absolute=false)
73
+ directions = []
74
+ @coordinates = extract_coordinates parsable
75
+ recurse_code = parsable[0,1]
76
+ first_absolute = force_absolute
77
+
78
+ # we need to handle this separately, since ClosePath doesn't take any coordinates
79
+ if @coordinates.empty? && recurse_code =~ /[Zz]/
80
+ directions << Directions::ClosePath.new(parsable[0,1] == parsable[0,1].upcase)
81
+ end
82
+
83
+ until @coordinates.empty?
84
+ absolute = (first_absolute || parsable[0,1] == parsable[0,1].upcase)
85
+ directions << construct_direction(recurse_code.strip[0].downcase.intern, absolute)
86
+ recurse_code = 'L' if recurse_code.downcase =~ /m/
87
+ first_absolute = false
88
+ end
89
+
90
+ directions
91
+ end
92
+
93
+ def construct_direction(recurse_code, absolute)
94
+ args = @coordinates.shift DIRECTIONS[recurse_code][:args]
95
+ raise TypeError if args.any?(&:nil?)
96
+ DIRECTIONS[recurse_code][:class].new(*args, absolute)
97
+ end
98
+
99
+ def extract_coordinates(command_string)
100
+ coordinates = []
101
+ command_string.scan(/-?\d+(\.\d+)?([eE][+-]?\d+)?/) do |match_group|
102
+ coordinates << $&.to_f
103
+ end
104
+ coordinates
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,66 @@
1
+ module Savage
2
+ class Path
3
+ require File.dirname(__FILE__) + "/direction_proxy"
4
+ require File.dirname(__FILE__) + "/sub_path"
5
+
6
+ include Utils
7
+ include DirectionProxy
8
+ include Transformable
9
+
10
+ attr_accessor :subpaths
11
+
12
+ define_proxies do |sym,const|
13
+ define_method(sym) do |*args|
14
+ @subpaths.last.send(sym,*args)
15
+ end
16
+ end
17
+
18
+ def initialize(*args)
19
+ @subpaths = [SubPath.new]
20
+ @subpaths.last.move_to(*args) if (2..3).include?(*args.length)
21
+ yield self if block_given?
22
+ end
23
+
24
+ def directions
25
+ directions = []
26
+ @subpaths.each { |subpath| directions.concat(subpath.directions) }
27
+ directions
28
+ end
29
+
30
+ def move_to(*args)
31
+ unless (@subpaths.last.directions.empty?)
32
+ (@subpaths << SubPath.new(*args)).last
33
+ else
34
+ @subpaths.last.move_to(*args)
35
+ end
36
+ end
37
+
38
+ def closed?
39
+ @subpaths.last.closed?
40
+ end
41
+
42
+ def to_command
43
+ @subpaths.collect { |subpath| subpath.to_command }.join
44
+ end
45
+
46
+ def transform(*args)
47
+ dup.tap do |path|
48
+ path.to_transformable_commands!
49
+ path.subpaths.each {|subpath| subpath.transform *args }
50
+ end
51
+ end
52
+
53
+ # Public: make commands within transformable commands
54
+ # H/h/V/v is considered not 'transformable'
55
+ # because when they are rotated, they will
56
+ # turn into other commands
57
+ def to_transformable_commands!
58
+ subpaths.each &:to_transformable_commands!
59
+ end
60
+
61
+ def fully_transformable?
62
+ subpaths.all? &:fully_transformable?
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,78 @@
1
+ module Savage
2
+ class SubPath
3
+ include Utils
4
+ include DirectionProxy
5
+ include Transformable
6
+
7
+ define_proxies do |sym,const|
8
+ define_method(sym) do |*args|
9
+ raise TypeError if const == "QuadraticCurveTo" && @directions.last.class != Directions::QuadraticCurveTo && [2,3].include?(args.length)
10
+ raise TypeError if const == "CubicCurveTo" && @directions.last.class != Directions::CubicCurveTo && [4,5].include?(args.length)
11
+ (@directions << Savage::Directions.const_get(const).new(*args)).last
12
+ end
13
+ end
14
+
15
+ attr_accessor :directions
16
+
17
+ def move_to(*args)
18
+ return nil unless @directions.empty?
19
+ (@directions << Directions::MoveTo.new(*args)).last
20
+ end
21
+
22
+ def initialize(*args)
23
+ @directions = []
24
+ move_to(*args) if (2..3).include?(args.length)
25
+ yield self if block_given?
26
+ end
27
+
28
+ def to_command
29
+ @directions.to_enum(:each_with_index).collect { |dir, i|
30
+ command_string = dir.to_command
31
+ if i > 0
32
+ prev_command_code = @directions[i-1].command_code
33
+ if dir.command_code == prev_command_code || (prev_command_code.match(/^[Mm]$/) && dir.command_code == 'L')
34
+ command_string.gsub!(/^[A-Za-z]/,'')
35
+ command_string.insert(0,' ') unless command_string.match(/^-/)
36
+ end
37
+ end
38
+ command_string
39
+ }.join
40
+ end
41
+
42
+ def commands
43
+ @directions
44
+ end
45
+
46
+ def closed?
47
+ @directions.last.kind_of? Directions::ClosePath
48
+ end
49
+
50
+ def transform(*args)
51
+ directions.each { |dir| dir.transform *args }
52
+ end
53
+
54
+ def to_transformable_commands!
55
+ if !fully_transformable?
56
+ pen_x, pen_y = 0, 0
57
+ directions.each_with_index do |dir, index|
58
+ unless dir.fully_transformable?
59
+ directions[index] = dir.to_fully_transformable_dir( pen_x, pen_y )
60
+ end
61
+
62
+ dx, dy = dir.movement
63
+ if dir.absolute?
64
+ pen_x = dx if dx
65
+ pen_y = dy if dy
66
+ else
67
+ pen_x += dx if dx
68
+ pen_y += dy if dy
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def fully_transformable?
75
+ directions.all? &:fully_transformable?
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,59 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+
4
+ module Savage
5
+
6
+ module Transformable
7
+
8
+ # Matrix 2D:
9
+ # | a, c, e |
10
+ # | b, d, f |
11
+ # | 0, 0, 1 |
12
+ def transform(a, b, c, d, e, f)
13
+ end
14
+
15
+ def translate(tx, ty=0)
16
+ transform( 1, 0, 0, 1, tx, ty )
17
+ end
18
+
19
+ def scale(sx, sy=sx)
20
+ transform( sx, 0, 0, sy, 0, 0 )
21
+ end
22
+
23
+ # Public: rotate by angle degrees
24
+ #
25
+ # - angle : rotation in degrees
26
+ # - cx : center x
27
+ # - cy : center y
28
+ #
29
+ # Returns nil
30
+ #
31
+ # TODO:
32
+ # make cx, cy be origin center
33
+ def rotate( angle, cx=0, cy=0 )
34
+ a = (angle.to_f/180).to_d * Math::PI
35
+ translate( cx, cy )
36
+ transform( Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0)
37
+ translate( -cx, -cy )
38
+ end
39
+
40
+ def skew_x( angle )
41
+ a = angle.to_f/180 * Math::PI
42
+ transform( 1, 0, Math.tan(a), 1, 0, 0 )
43
+ end
44
+
45
+ def skew_y( angle )
46
+ a = angle.to_f/180 * Math::PI
47
+ transform( 1, Math.tan(a), 0, 1, 0, 0 )
48
+ end
49
+
50
+ protected
51
+
52
+ def transform_dot( dot, a, b, c, d, e, f )
53
+ x, y = dot.x, dot.y
54
+ dot.x = x*a + y*c + e
55
+ dot.y = x*b + y*d + f
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ module Savage
2
+ module Utils
3
+ def bool_to_int(value)
4
+ (value) ? 1 : 0
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.homepage = "http://github.com/qhwa/savage"
7
+ spec.rubygems_version = "2.2.2"
8
+ spec.summary = "A little library to manipulate SVG path data. Path commands can be transformed."
9
+ spec.name = "savage-transform"
10
+ spec.version = "1.3.0"
11
+ spec.authors = ["qhwa", "Jeremy Holland"]
12
+ spec.description = "A little gem for extracting and manipulating SVG vector path data. Path commands can be transfromed."
13
+ spec.email = ["qhwa@163.com", "jeremy@jeremypholland.com"]
14
+ spec.summary = %q{extract color information from images}
15
+ spec.description = %q{extract color information from images}
16
+ spec.homepage = "https://github.com/qhwa/color_extract"
17
+ spec.license = "MIT"
18
+ spec.files = `git ls-files`.split($/)
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", ">= 2.3.0"
26
+ end