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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +22 -0
- data/.rspec +1 -0
- data/LICENSE +20 -0
- data/README.rdoc +108 -0
- data/Rakefile +1 -0
- data/VERSION +1 -0
- data/lib/savage.rb +3 -0
- data/lib/savage/direction.rb +56 -0
- data/lib/savage/direction_proxy.rb +19 -0
- data/lib/savage/directions/arc_to.rb +30 -0
- data/lib/savage/directions/close_path.rb +22 -0
- data/lib/savage/directions/coordinate_target.rb +21 -0
- data/lib/savage/directions/cubic_curve_to.rb +47 -0
- data/lib/savage/directions/horizontal_to.rb +31 -0
- data/lib/savage/directions/line_to.rb +15 -0
- data/lib/savage/directions/move_to.rb +15 -0
- data/lib/savage/directions/point_target.rb +22 -0
- data/lib/savage/directions/quadratic_curve_to.rb +44 -0
- data/lib/savage/directions/vertical_to.rb +31 -0
- data/lib/savage/parser.rb +108 -0
- data/lib/savage/path.rb +66 -0
- data/lib/savage/sub_path.rb +78 -0
- data/lib/savage/transformable.rb +59 -0
- data/lib/savage/utils.rb +7 -0
- data/savage-transform.gemspec +26 -0
- data/spec/savage/directions/arc_to_spec.rb +97 -0
- data/spec/savage/directions/close_path_spec.rb +30 -0
- data/spec/savage/directions/cubic_curve_to_spec.rb +146 -0
- data/spec/savage/directions/horizontal_to_spec.rb +10 -0
- data/spec/savage/directions/line_to_spec.rb +14 -0
- data/spec/savage/directions/move_to_spec.rb +10 -0
- data/spec/savage/directions/point_spec.rb +12 -0
- data/spec/savage/directions/quadratic_curve_spec.rb +123 -0
- data/spec/savage/directions/vertical_to_spec.rb +10 -0
- data/spec/savage/parser_spec.rb +250 -0
- data/spec/savage/path_spec.rb +105 -0
- data/spec/savage/sub_path_spec.rb +195 -0
- data/spec/savage/transformable_spec.rb +99 -0
- data/spec/savage_spec.rb +5 -0
- data/spec/shared/command.rb +13 -0
- data/spec/shared/coordinate_target.rb +36 -0
- data/spec/shared/direction.rb +29 -0
- data/spec/shared/point_target.rb +45 -0
- data/spec/spec_helper.rb +36 -0
- 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
|
data/lib/savage/path.rb
ADDED
@@ -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
|
data/lib/savage/utils.rb
ADDED
@@ -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
|