phantom_svg 1.0.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/.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
|