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.
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