Genshin/phantom_svg

View on GitHub
lib/phantom/parser/svg_writer.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'rexml/document'

require_relative 'abstract_image_writer.rb'
require_relative 'phantom_xmldecl.rb'

module Phantom
  module SVG
    module Parser
      # SVG writer.
      class SVGWriter < AbstractImageWriter
        # Write svg file from object to path.
        # Return write size.
        def write(path, object)
          return 0 if path.nil? || path.empty? || object.nil?

          reset

          # Parse object.
          return 0 unless write_proc(object)

          # Add svg version.
          @root.elements['svg'].add_attribute('version', '1.1')

          # Write to file.
          File.open(path, 'w') { |file| @root.write(file, 2) }
        end

        private

        # Reset SVGWriter object.
        def reset
          @root = REXML::Document.new
          @root.context[:attribute_quote] = :quote
          @root << Phantom::SVG::Parser::PhantomXMLDecl.new('1.0', 'UTF-8')
          @root << REXML::Comment.new(' Generated by phantom_svg. ')
        end

        # Write procedure.
        def write_proc(object)
          if object.is_a?(Base)
            if object.frames.size == 1    then  write_svg(object.frames[0])
            elsif object.frames.size > 1  then  write_animation_svg(object)
            else                                return false
            end
          elsif object.is_a?(Frame)       then  write_svg(object)
          else                                  return false
          end

          true
        end

        # Write no animation svg.
        def write_svg(frame)
          write_image(frame, @root)
        end

        # Write animation svg.
        def write_animation_svg(base)
          svg = @root.add_element('svg', 'id' => 'phantom_svg')
          defs = svg.add_element('defs')

          # Header.
          write_size(base, svg)
          svg.add_namespace('http://www.w3.org/2000/svg')
          svg.add_namespace('xlink', 'http://www.w3.org/1999/xlink')

          # Images.
          write_images(base.frames, defs)

          # Animation.
          write_animation(base, defs)

          # Show control.
          write_show_control(base, svg)
        end

        # Write image size.
        def write_size(s, d)
          d.add_attribute('width', s.width.is_a?(String) ? s.width : "#{s.width.to_i}px")
          d.add_attribute('height', s.height.is_a?(String) ? s.height : "#{s.height.to_i}px")
        end

        # Write viewbox.
        def write_viewbox(s, d)
          d.add_attribute('viewBox', s.viewbox.to_s) if s.instance_variable_defined?(:@viewbox)
        end

        # Write namespaces from src to dest.
        def write_namespaces(src, dest)
          src.namespaces.each do |key, val|
            if key == 'xmlns' then  dest.add_namespace(val)
            else                    dest.add_namespace(key, val)
            end
          end
        end

        # Write surfaces to dest.
        def write_surfaces(surfaces, dest)
          surfaces.each { |surface| dest.add_element(Marshal.load(Marshal.dump(surface))) }
        end

        # Write image.
        def write_image(frame, parent_node, id = nil)
          svg = parent_node.add_element('svg')
          svg.add_attribute('id', id) unless id.nil?
          write_size(frame, svg) if parent_node == @root
          write_viewbox(frame, svg)
          svg.add_attribute('preserveAspectRatio', 'none')
          write_namespaces(frame, svg)
          write_surfaces(frame.surfaces, svg)
          convert_id_to_unique(svg, "#{id}_") unless id.nil?
        end

        # Write images.
        def write_images(frames, parent_node)
          REXML::Comment.new(' Images. ', parent_node)
          frames.each_with_index { |frame, i| write_image(frame, parent_node, "frame#{i}") }
        end

        # Write animation.
        def write_animation(base, parent_node)
          return if skip_frame_and_no_animation?(base)

          REXML::Comment.new(' Animation. ', parent_node)
          symbol = parent_node.add_element('symbol', 'id' => 'animation')

          begin_text = "0s;frame#{base.frames.length - 1}_anim.end"
          base.frames.each_with_index do |frame, i|
            next if i == 0 && base.skip_first

            write_animation_frame(frame, "frame#{i}", begin_text, symbol)

            begin_text = "frame#{i}_anim.end"
          end
        end

        def write_animation_frame(frame, id, begin_text, parent)
          use = parent.add_element('use', 'xlink:href' => "##{id}",
                                          'visibility' => 'hidden')

          use.add_element('set',  'id' => "#{id}_anim",
                                  'attributeName' => 'visibility',
                                  'to' => 'visible',
                                  'begin' => begin_text,
                                  'dur' => "#{frame.duration}s")
        end

        # Write show control.
        def write_show_control(base, parent_node)
          REXML::Comment.new(' Main control. ', parent_node)

          if skip_frame_and_no_animation?(base)
            write_show_control_main_2(parent_node)
          else
            write_show_control_header(base, parent_node)
            write_show_control_main(base, parent_node)
          end
        end

        # Write show control header.
        def write_show_control_header(base, parent_node)
          repeat_count = base.loops.to_i == 0 ? 'indefinite' : base.loops.to_i.to_s

          parent_node.add_element('animate',  'id' => 'controller',
                                              'begin' => '0s',
                                              'dur' => "#{base.total_duration}s",
                                              'repeatCount' => repeat_count)
        end

        # Write show control main.
        def write_show_control_main(base, parent_node)
          use = parent_node.add_element('use', 'xlink:href' => '#frame0')

          use.add_element('set',  'attributeName' => 'xlink:href',
                                  'to' => '#animation',
                                  'begin' => 'controller.begin')

          use.add_element('set',  'attributeName' => 'xlink:href',
                                  'to' => "#frame#{base.frames.length - 1}",
                                  'begin' => 'controller.end')
        end

        # Write show control main.
        def write_show_control_main_2(parent_node)
          use = parent_node.add_element('use', 'xlink:href' => '#frame0')

          use.add_element('set',  'attributeName' => 'xlink:href',
                                  'to' => '#frame1',
                                  'begin' => '0s')
        end

        # Convert id.
        def convert_id_to_unique(root_node, prefix)
          id_array = []
          overwrite_id(root_node, prefix, id_array)
          overwrite_relative_id(root_node, prefix, id_array)
        end

        # Overwrite id in surfaces.
        def overwrite_id(parent_node, prefix, out_id_array)
          parent_node.elements.each do |child|
            old_id = child.attributes['id']
            unless old_id.nil?
              out_id_array << old_id
              child.add_attribute('id', "#{prefix}#{old_id}")
            end
            overwrite_id(child, prefix, out_id_array)
          end
        end

        # Overwrite relative id in surfaces.
        def overwrite_relative_id(parent_node, prefix, id_array)
          parent_node.elements.each do |child|
            child.attributes.each do |_, val|
              id_array.each do |id|
                val.gsub!("##{id}", "##{prefix}#{id}")
              end
            end
            overwrite_relative_id(child, prefix, id_array)
          end
        end

        # If base has skip frame and no animation, return true.
        def skip_frame_and_no_animation?(base)
          base.skip_first == true && base.frames.size == 2
        end
      end # class SVGWriter
    end # module Parser
  end # module SVG
end # module Phantom