phantom_svg 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +42 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +9 -0
- data/Gemfile +16 -0
- data/Guardfile +12 -0
- data/LICENSE +165 -0
- data/README.md +6 -0
- data/lib/phantom/frame.rb +71 -0
- data/lib/phantom/parser/abstract_animation_reader.rb +117 -0
- data/lib/phantom/parser/json_animation_reader.rb +42 -0
- data/lib/phantom/parser/raster.rb +115 -0
- data/lib/phantom/parser/svg_reader.rb +131 -0
- data/lib/phantom/parser/svg_writer.rb +163 -0
- data/lib/phantom/parser/xml_animation_reader.rb +32 -0
- data/lib/phantom/svg.rb +139 -0
- data/lib/phantom_svg.rb +1 -0
- data/phantom_svg.gemspec +25 -0
- data/spec/images/apngasm.png +0 -0
- data/spec/images/ninja.svg +63 -0
- data/spec/images/stuck_out_tongue/0.svg +103 -0
- data/spec/images/stuck_out_tongue/1.svg +103 -0
- data/spec/images/stuck_out_tongue/10.svg +103 -0
- data/spec/images/stuck_out_tongue/11.svg +103 -0
- data/spec/images/stuck_out_tongue/2.svg +103 -0
- data/spec/images/stuck_out_tongue/3.svg +103 -0
- data/spec/images/stuck_out_tongue/4.svg +103 -0
- data/spec/images/stuck_out_tongue/5.svg +103 -0
- data/spec/images/stuck_out_tongue/6.svg +103 -0
- data/spec/images/stuck_out_tongue/7.svg +103 -0
- data/spec/images/stuck_out_tongue/8.svg +103 -0
- data/spec/images/stuck_out_tongue/9.svg +103 -0
- data/spec/images/stuck_out_tongue/loops_test.json +9 -0
- data/spec/images/stuck_out_tongue/loops_test.xml +4 -0
- data/spec/images/stuck_out_tongue/skip_first_test.json +10 -0
- data/spec/images/stuck_out_tongue/skip_first_test.xml +5 -0
- data/spec/images/stuck_out_tongue/test1.json +20 -0
- data/spec/images/stuck_out_tongue/test1.xml +15 -0
- data/spec/images/stuck_out_tongue/test2.json +13 -0
- data/spec/images/stuck_out_tongue/test2.xml +4 -0
- data/spec/phantom/svg_spec.rb +421 -0
- data/spec/spec_helper.rb +81 -0
- metadata +170 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require_relative '../frame.rb'
|
5
|
+
require_relative 'abstract_animation_reader.rb'
|
6
|
+
|
7
|
+
module Phantom
|
8
|
+
module SVG
|
9
|
+
module Parser
|
10
|
+
# AnimationReader for JSON.
|
11
|
+
class JSONAnimationReader < AbstractAnimationReader
|
12
|
+
private
|
13
|
+
|
14
|
+
# Read parameter from animation information file.
|
15
|
+
def read_parameter(path)
|
16
|
+
open(path) do |file|
|
17
|
+
JSON.load(file).each do |key, val|
|
18
|
+
case key
|
19
|
+
when 'frames' then read_frame_infos(val)
|
20
|
+
when 'delays' then val.each { |delay| add_delay(delay) }
|
21
|
+
else set_parameter(key, val)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Read frame informations
|
28
|
+
def read_frame_infos(value)
|
29
|
+
value.each do |frame_info|
|
30
|
+
if frame_info.instance_of?(Hash)
|
31
|
+
frame_info.each do |name, delay|
|
32
|
+
add_frame_info(name, delay)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
add_frame_info(frame_info)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end # class JSONAnimationReader < AbstractAnimationReader
|
40
|
+
end # module Parser
|
41
|
+
end # module SVG
|
42
|
+
end # module Phantom
|
@@ -0,0 +1,115 @@
|
|
1
|
+
|
2
|
+
require 'rapngasm'
|
3
|
+
require 'gdk3'
|
4
|
+
require 'rsvg2'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'cairo'
|
7
|
+
|
8
|
+
require_relative '../frame.rb'
|
9
|
+
|
10
|
+
module Phantom
|
11
|
+
module SVG
|
12
|
+
module Parser
|
13
|
+
module Raster
|
14
|
+
def load_raster(path, id)
|
15
|
+
apngasm = APNGAsm.new
|
16
|
+
apngasm.disassemble(path)
|
17
|
+
|
18
|
+
if apngasm.frame_count == 1
|
19
|
+
@frames << create_frame_from_png(path, id)
|
20
|
+
else
|
21
|
+
create_frame_from_apng(apngasm, id)
|
22
|
+
end
|
23
|
+
|
24
|
+
apngasm.reset
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_frame_from_png(path, id, duration = nil)
|
28
|
+
pixbuf = Gdk::Pixbuf.new(path)
|
29
|
+
|
30
|
+
frame = Phantom::SVG::Frame.new
|
31
|
+
frame.width = "#{pixbuf.width}px"
|
32
|
+
frame.height = "#{pixbuf.height}px"
|
33
|
+
frame.surfaces = create_surfaces(path, pixbuf.width, pixbuf.height)
|
34
|
+
frame.duration = duration unless duration.nil?
|
35
|
+
frame.namespaces = {
|
36
|
+
'xmlns' => 'http://www.w3.org/2000/svg',
|
37
|
+
'xlink' => 'http://www.w3.org/1999/xlink'
|
38
|
+
}
|
39
|
+
|
40
|
+
frame
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_frame_from_apng(apngasm, id)
|
44
|
+
png_frames = apngasm.get_frames
|
45
|
+
width = 0
|
46
|
+
height = 0
|
47
|
+
Dir::mktmpdir(nil, File.dirname(__FILE__)) do |dir|
|
48
|
+
apngasm.save_pngs(dir)
|
49
|
+
png_frames.each_with_index do |png_frame, i|
|
50
|
+
width = png_frame.width if width < png_frame.width
|
51
|
+
height = png_frame.height if height < png_frame.height
|
52
|
+
duration = png_frame.delay_numerator.to_f / png_frame.delay_denominator.to_f
|
53
|
+
@frames << create_frame_from_png("#{dir}/#{i}.png", id, duration)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@width = "#{width}px"
|
57
|
+
@height = "#{height}px"
|
58
|
+
@loops = apngasm.get_loops
|
59
|
+
@skip_first = apngasm.is_skip_first
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_surfaces(path, width, height)
|
63
|
+
bin = File.binread(path)
|
64
|
+
base64 = [bin].pack('m')
|
65
|
+
|
66
|
+
image = REXML::Element.new('image')
|
67
|
+
image.add_attributes(
|
68
|
+
'width' => width,
|
69
|
+
'height' => height,
|
70
|
+
'xlink:href' => "data:image/png;base64,#{base64}"
|
71
|
+
)
|
72
|
+
|
73
|
+
[image]
|
74
|
+
end
|
75
|
+
|
76
|
+
def save_rasterized(path)
|
77
|
+
set_size if @width.to_i == 0 || @height.to_i == 0
|
78
|
+
|
79
|
+
apngasm = APNGAsm.new
|
80
|
+
apngasm.set_loops(@loops)
|
81
|
+
apngasm.set_skip_first(@skip_first)
|
82
|
+
|
83
|
+
Dir::mktmpdir(nil, File.dirname(__FILE__)) do |dir|
|
84
|
+
@frames.each_with_index do |frame, i|
|
85
|
+
create_tmp_file("#{dir}/tmp#{i}", frame)
|
86
|
+
apngasm.add_frame_file("#{dir}/tmp#{i}.png", frame.duration.to_f * 1000, 1000)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
result = apngasm.assemble(path)
|
91
|
+
apngasm.reset
|
92
|
+
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_tmp_file(path, frame)
|
97
|
+
save_svg_frame("#{path}.svg", frame, @width, @height)
|
98
|
+
convert_to_png(path, frame)
|
99
|
+
end
|
100
|
+
|
101
|
+
def convert_to_png(path, frame)
|
102
|
+
handle = RSVG::Handle.new_from_file("#{path}.svg")
|
103
|
+
|
104
|
+
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, @width, @height)
|
105
|
+
context = Cairo::Context.new(surface)
|
106
|
+
context.scale(@width / handle.dimensions.width, @height / handle.dimensions.height)
|
107
|
+
context.render_rsvg_handle(handle)
|
108
|
+
|
109
|
+
surface.write_to_png("#{path}.png")
|
110
|
+
surface.finish
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
require_relative '../frame.rb'
|
5
|
+
|
6
|
+
module Phantom
|
7
|
+
module SVG
|
8
|
+
module Parser
|
9
|
+
# SVG reader.
|
10
|
+
class SVGReader
|
11
|
+
attr_reader :frames, :width, :height, :loops, :skip_first, :has_animation
|
12
|
+
alias_method :has_animation?, :has_animation
|
13
|
+
|
14
|
+
# Construct SVGReader object.
|
15
|
+
def initialize(path = nil, options = {})
|
16
|
+
read(path, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Read svg file from path.
|
20
|
+
def read(path, options = {})
|
21
|
+
reset
|
22
|
+
|
23
|
+
return if path.nil? || path.empty?
|
24
|
+
|
25
|
+
@root = REXML::Document.new(open(path))
|
26
|
+
|
27
|
+
if @root.elements['svg'].attributes['id'] == 'phantom_svg'
|
28
|
+
read_animation_svg(options)
|
29
|
+
@has_animation = true
|
30
|
+
else
|
31
|
+
read_svg(options)
|
32
|
+
@has_animation = false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Reset SVGReader object.
|
39
|
+
def reset
|
40
|
+
@frames = []
|
41
|
+
@width = nil
|
42
|
+
@height = nil
|
43
|
+
@loops = nil
|
44
|
+
@skip_first = nil
|
45
|
+
@has_animation = false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Read no animation svg.
|
49
|
+
def read_svg(options)
|
50
|
+
read_images(@root, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Read animation svg.
|
54
|
+
def read_animation_svg(options)
|
55
|
+
svg = @root.elements['svg']
|
56
|
+
defs = svg.elements['defs']
|
57
|
+
|
58
|
+
read_size(svg, self, options)
|
59
|
+
read_images(defs, options)
|
60
|
+
read_skip_first
|
61
|
+
read_durations(options)
|
62
|
+
read_loops
|
63
|
+
end
|
64
|
+
|
65
|
+
# Read size from node to dest.
|
66
|
+
def read_size(node, dest, options = {})
|
67
|
+
node.attributes.each do |key, val|
|
68
|
+
case key
|
69
|
+
when 'width'
|
70
|
+
dest.instance_variable_set(:@width, choice_value(val, options[:width]))
|
71
|
+
when 'height'
|
72
|
+
dest.instance_variable_set(:@height, choice_value(val, options[:height]))
|
73
|
+
when 'viewBox'
|
74
|
+
dest.viewbox.set_from_text(choice_value(val, options[:viewbox]).to_s)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Read images from svg.
|
80
|
+
def read_images(parent_node, options)
|
81
|
+
parent_node.elements.each('svg') do |svg|
|
82
|
+
new_frame = Phantom::SVG::Frame.new
|
83
|
+
|
84
|
+
# Read namespaces.
|
85
|
+
new_frame.namespaces = svg.namespaces.clone
|
86
|
+
new_frame.namespaces.merge!(options[:namespaces]) unless options[:namespaces].nil?
|
87
|
+
|
88
|
+
# Read image size.
|
89
|
+
read_size(svg, new_frame, options)
|
90
|
+
|
91
|
+
# Read image surfaces.
|
92
|
+
new_frame.surfaces = choice_value(svg.elements.to_a, options[:surfaces])
|
93
|
+
|
94
|
+
# Read frame duration.
|
95
|
+
new_frame.duration = choice_value(new_frame.duration, options[:duration])
|
96
|
+
|
97
|
+
# Add frame to array.
|
98
|
+
@frames << new_frame
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Read skip_first.
|
103
|
+
def read_skip_first
|
104
|
+
@skip_first = @root.elements['svg/defs/symbol/use'].attributes['xlink:href'] != '#frame0'
|
105
|
+
end
|
106
|
+
|
107
|
+
# Read frame durations.
|
108
|
+
def read_durations(options)
|
109
|
+
i = @skip_first ? 1 : 0
|
110
|
+
@root.elements['svg/defs/symbol'].elements.each('use') do |use|
|
111
|
+
duration = use.elements['set'].attributes['dur'].to_f
|
112
|
+
@frames[i].duration = choice_value(duration, options[:duration])
|
113
|
+
i += 1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Read animation loop count.
|
118
|
+
def read_loops
|
119
|
+
@loops = @root.elements['svg/animate'].attributes['repeatCount'].to_i
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper method.
|
123
|
+
# Return val if override is nil.
|
124
|
+
# Return override if override is not nil.
|
125
|
+
def choice_value(val, override)
|
126
|
+
override.nil? ? val : override
|
127
|
+
end
|
128
|
+
end # class SVGReader
|
129
|
+
end # module Parser
|
130
|
+
end # module SVG
|
131
|
+
end # module Phantom
|
@@ -0,0 +1,163 @@
|
|
1
|
+
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module Phantom
|
5
|
+
module SVG
|
6
|
+
module Parser
|
7
|
+
# SVG writer.
|
8
|
+
class SVGWriter
|
9
|
+
# Construct SVGWriter object.
|
10
|
+
def initialize(path = nil, object = nil)
|
11
|
+
write(path, object)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Write svg file from object to path.
|
15
|
+
# Return write size.
|
16
|
+
def write(path, object)
|
17
|
+
return 0 if path.nil? || path.empty? || object.nil?
|
18
|
+
|
19
|
+
reset
|
20
|
+
|
21
|
+
# Parse object.
|
22
|
+
if object.is_a?(Base) then write_animation_svg(object)
|
23
|
+
elsif object.is_a?(Frame) then write_svg(object)
|
24
|
+
else return 0
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add svg version.
|
28
|
+
@root.elements['svg'].add_attribute('version', '1.1')
|
29
|
+
|
30
|
+
# Write to file.
|
31
|
+
File.open(path, 'w') { |file| @root.write(file, 2) }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Reset SVGWriter object.
|
37
|
+
def reset
|
38
|
+
@root = REXML::Document.new
|
39
|
+
@root << REXML::XMLDecl.new('1.0', 'UTF-8')
|
40
|
+
@root << REXML::Comment.new(' Generated by phantom_svg. ')
|
41
|
+
end
|
42
|
+
|
43
|
+
# Write no animation svg.
|
44
|
+
def write_svg(frame)
|
45
|
+
write_image(frame, @root)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write animation svg.
|
49
|
+
def write_animation_svg(base)
|
50
|
+
svg = @root.add_element('svg', 'id' => 'phantom_svg')
|
51
|
+
defs = svg.add_element('defs')
|
52
|
+
|
53
|
+
# Header.
|
54
|
+
write_size(base, svg)
|
55
|
+
svg.add_namespace('http://www.w3.org/2000/svg')
|
56
|
+
svg.add_namespace('xlink', 'http://www.w3.org/1999/xlink')
|
57
|
+
|
58
|
+
# Images.
|
59
|
+
write_images(base.frames, defs)
|
60
|
+
|
61
|
+
# Animation.
|
62
|
+
write_animation(base, defs)
|
63
|
+
|
64
|
+
# Show control.
|
65
|
+
write_show_control(base, svg)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Write image size.
|
69
|
+
def write_size(s, d)
|
70
|
+
d.add_attribute('width', s.width.is_a?(String) ? s.width : "#{s.width.to_i}px")
|
71
|
+
d.add_attribute('height', s.height.is_a?(String) ? s.height : "#{s.height.to_i}px")
|
72
|
+
d.add_attribute('viewBox', s.viewbox.to_s) if s.instance_variable_defined?(:@viewbox)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Write namespaces from src to dest.
|
76
|
+
def write_namespaces(src, dest)
|
77
|
+
src.namespaces.each do |key, val|
|
78
|
+
if key == 'xmlns' then dest.add_namespace(val)
|
79
|
+
else dest.add_namespace(key, val)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Write surfaces to dest.
|
85
|
+
def write_surfaces(surfaces, dest)
|
86
|
+
surfaces.each { |surface| dest.add_element(surface) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Write image.
|
90
|
+
def write_image(frame, parent_node, id = nil)
|
91
|
+
svg = parent_node.add_element('svg')
|
92
|
+
svg.add_attribute('id', id) unless id.nil?
|
93
|
+
write_size(frame, svg)
|
94
|
+
write_namespaces(frame, svg)
|
95
|
+
write_surfaces(frame.surfaces, svg)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Write images.
|
99
|
+
def write_images(frames, parent_node)
|
100
|
+
REXML::Comment.new(' Images. ', parent_node)
|
101
|
+
frames.each_with_index { |frame, i| write_image(frame, parent_node, "frame#{i}") }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Write animation.
|
105
|
+
def write_animation(base, parent_node)
|
106
|
+
REXML::Comment.new(' Animation. ', parent_node)
|
107
|
+
symbol = parent_node.add_element('symbol', 'id' => 'animation')
|
108
|
+
|
109
|
+
begin_text = "0s;frame#{base.frames.length - 1}_anim.end"
|
110
|
+
base.frames.each_with_index do |frame, i|
|
111
|
+
next if i == 0 && base.skip_first
|
112
|
+
|
113
|
+
write_animation_frame(frame, "frame#{i}", begin_text, symbol)
|
114
|
+
|
115
|
+
begin_text = "frame#{i}_anim.end"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def write_animation_frame(frame, id, begin_text, parent)
|
120
|
+
use = parent.add_element('use', 'xlink:href' => "##{id}",
|
121
|
+
'visibility' => 'hidden')
|
122
|
+
|
123
|
+
use.add_element('set', 'id' => "#{id}_anim",
|
124
|
+
'attributeName' => 'visibility',
|
125
|
+
'to' => 'visible',
|
126
|
+
'begin' => begin_text,
|
127
|
+
'dur' => "#{frame.duration}s")
|
128
|
+
end
|
129
|
+
|
130
|
+
# Write show control.
|
131
|
+
def write_show_control(base, parent_node)
|
132
|
+
REXML::Comment.new(' Main control. ', parent_node)
|
133
|
+
|
134
|
+
write_show_control_header(base, parent_node)
|
135
|
+
write_show_control_main(base, parent_node)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Write show control header.
|
139
|
+
def write_show_control_header(base, parent_node)
|
140
|
+
repeat_count = base.loops.to_i == 0 ? 'indefinite' : base.loops.to_i.to_s
|
141
|
+
|
142
|
+
parent_node.add_element('animate', 'id' => 'controller',
|
143
|
+
'begin' => '0s',
|
144
|
+
'dur' => "#{base.total_duration}s",
|
145
|
+
'repeatCount' => repeat_count)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Write show control main.
|
149
|
+
def write_show_control_main(base, parent_node)
|
150
|
+
use = parent_node.add_element('use', 'xlink:href' => '#frame0')
|
151
|
+
|
152
|
+
use.add_element('set', 'attributeName' => 'xlink:href',
|
153
|
+
'to' => '#animation',
|
154
|
+
'begin' => 'controller.begin')
|
155
|
+
|
156
|
+
use.add_element('set', 'attributeName' => 'xlink:href',
|
157
|
+
'to' => "#frame#{base.frames.length - 1}",
|
158
|
+
'begin' => 'controller.end')
|
159
|
+
end
|
160
|
+
end # class SVGWriter
|
161
|
+
end # module Parser
|
162
|
+
end # module SVG
|
163
|
+
end # module Phantom
|