phantom_svg 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +11 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +16 -0
  7. data/Guardfile +12 -0
  8. data/LICENSE +165 -0
  9. data/README.md +6 -0
  10. data/lib/phantom/frame.rb +71 -0
  11. data/lib/phantom/parser/abstract_animation_reader.rb +117 -0
  12. data/lib/phantom/parser/json_animation_reader.rb +42 -0
  13. data/lib/phantom/parser/raster.rb +115 -0
  14. data/lib/phantom/parser/svg_reader.rb +131 -0
  15. data/lib/phantom/parser/svg_writer.rb +163 -0
  16. data/lib/phantom/parser/xml_animation_reader.rb +32 -0
  17. data/lib/phantom/svg.rb +139 -0
  18. data/lib/phantom_svg.rb +1 -0
  19. data/phantom_svg.gemspec +25 -0
  20. data/spec/images/apngasm.png +0 -0
  21. data/spec/images/ninja.svg +63 -0
  22. data/spec/images/stuck_out_tongue/0.svg +103 -0
  23. data/spec/images/stuck_out_tongue/1.svg +103 -0
  24. data/spec/images/stuck_out_tongue/10.svg +103 -0
  25. data/spec/images/stuck_out_tongue/11.svg +103 -0
  26. data/spec/images/stuck_out_tongue/2.svg +103 -0
  27. data/spec/images/stuck_out_tongue/3.svg +103 -0
  28. data/spec/images/stuck_out_tongue/4.svg +103 -0
  29. data/spec/images/stuck_out_tongue/5.svg +103 -0
  30. data/spec/images/stuck_out_tongue/6.svg +103 -0
  31. data/spec/images/stuck_out_tongue/7.svg +103 -0
  32. data/spec/images/stuck_out_tongue/8.svg +103 -0
  33. data/spec/images/stuck_out_tongue/9.svg +103 -0
  34. data/spec/images/stuck_out_tongue/loops_test.json +9 -0
  35. data/spec/images/stuck_out_tongue/loops_test.xml +4 -0
  36. data/spec/images/stuck_out_tongue/skip_first_test.json +10 -0
  37. data/spec/images/stuck_out_tongue/skip_first_test.xml +5 -0
  38. data/spec/images/stuck_out_tongue/test1.json +20 -0
  39. data/spec/images/stuck_out_tongue/test1.xml +15 -0
  40. data/spec/images/stuck_out_tongue/test2.json +13 -0
  41. data/spec/images/stuck_out_tongue/test2.xml +4 -0
  42. data/spec/phantom/svg_spec.rb +421 -0
  43. data/spec/spec_helper.rb +81 -0
  44. 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