Template:Gif animation to svg.py

From Wikimania 2016 • Esino Lario, Italy
Jump to navigation Jump to search
#!/usr/bin/env python
import re, json

## http://stackoverflow.com/questions/3503879
import subprocess, sys
def system(command, is_verbose=False):
 if (is_verbose): sys.stdout.write(command) ## write omits newline
 stdout = subprocess.check_output(command, shell=True)
 if (is_verbose): print(": " + stdout)
 return stdout

import os.path ## to check if file exists
def mkdir_cache(is_refresh_cache=False, is_verbose=False, suffix='.cache/'):
 basename = __file__[:__file__.rfind('.')]
 dir_cache = basename + suffix
 if (is_refresh_cache):
  for (dir, dirs, filenames) in os.walk(dir_cache, topdown=False): os.rmdir(dir)
  if (is_verbose): print("delete {dir_cache}".format(**locals()))
 if (not os.path.exists(dir_cache)):
  if (is_verbose): print("make {dir_cache}".format(**locals()))
  os.makedirs(dir_cache)
 elif (is_verbose): print("{dir_cache} already exists".format(**locals()))
 return dir_cache

## http://www.techrepublic.com/article/parsing-data-from-the-web-in-python/
import urllib2, time ## urllib2 for web access, time for sleep
def read_webpage(url, path_cache='', is_refresh_cache=False, is_verbose=False):
 dir_cache = mkdir_cache(is_refresh_cache=is_refresh_cache, is_verbose=is_verbose)
 if (not path_cache): path_cache = dir_cache + urllib2.quote(url, safe='')
 if (is_refresh_cache or (not os.path.isfile(path_cache))):
  html = urllib2.urlopen(url).read()
  file_html = open(path_cache, 'wb')
  file_html.write(html)
  if (is_verbose): print("fetch {url} into {path_cache}".format(**locals()))
  time.sleep(1) ## avoid rate-limit-exceeded error
 else:
  file_html = open(path_cache)
  html = file_html.read()
  if (is_verbose): print("read from {path_cache}".format(**locals()))
 file_html.close()
 return html

## http://stackoverflow.com/questions/3715493
import base64
def base64_encode(path):
 with open(path, 'rb') as file: return base64.b64encode(file.read())

def make_svg(url, increment, message_action):
 if (message_action == '3D'  ): message_action = 'to rotate the 3D model'
 if (message_action == 'time'): message_action = 'to move through time'
 ## Get image URL if description page URL given
 dir_cache   = mkdir_cache()
 filename    = url[url.rfind('/')+1:]
 if (filename.lower().find('file:') == 0):
  filename  = filename[filename.rfind(':') + 1:]
  path_html = '{dir_cache}{filename}.htm'.format(**locals())
  html      = read_webpage(url, path_html, is_verbose=True)
  url       = re.search(r'http.*?//upload\.[^"]+', html).group(0)
 ## Fetch image if needed
 basename      = filename[:filename.rfind('.')]
 path_gif      = dir_cache + filename
 path_basename = path_gif[:path_gif.rfind('.')]
 read_webpage(url, path_gif, is_verbose=True)
 ## Extract GIF animation frames if needed
 if (os.path.isfile('{dir_cache}{basename}-0.png'.format(**locals()))):
  print("skip extracting GIF animation frames")
 else:
  print("extract GIF animation frames")
  system('magick "{path_gif}" -coalesce "{path_basename}.png"'.format(**locals()), is_verbose=True)
 ## Base64-encode frames if needed
 path_json    = dir_cache + basename + '.json'
 jsons        = {}
 n_image      = 0
 n_frame      = 0
 if (0):
 # if (os.path.isfile(path_json)):
  file_json    = open(path_json, 'r')
  jsons        = json.loads(file_json.read())
  n_frame      = jsons['n_frame']
  out_image    = jsons['out_image']
  width_image  = jsons['width_image']
  height_image = jsons['height_image']
 else:
  ## Count frames
  n_image = 0
  while (os.path.isfile('{dir_cache}{basename}-{n_image}.png'.format(**locals()))): n_image += 1
  ## Base64-encode relevant frames
  n_frame    = int(n_image / abs(increment))
  out_images = []
  for i_frame in range(n_frame):
   i_image    = i_frame * increment + (0 if (increment > 0) else n_image + increment)
   path_frame = '{dir_cache}{basename}-{i_image}.png'.format(**locals())
   stdout     = system('magick "{path_frame}" info:'.format(**locals()), is_verbose=True)
   (width_image, height_image) = [int(dim) for dim in re.search(r'\d+x\d+', stdout).group(0).split('x')]
   base64_encoded = base64_encode(path_frame)
   out_images.append('''\
  <image id="image_{i_frame}" x="0" y="0" width="{width_image}" height="{height_image}" xlink:href="data:image/png;base64,{base64_encoded}"/>\
'''.format(**locals()))
  out_image = '\n'.join(out_images)
  jsons     = {'out_image':out_image, 'width_image' :width_image,
               'n_frame'  :n_frame  , 'height_image':height_image}
  file_json = open(path_json, 'w')
  try: ## use try/finally so that file is closed even if write fails
   file_json.write(json.dumps(jsons, indent=1, separators=(',',':')))
  finally:
   file_json.close()
 ## Create SVG
 out_mains = []
 scale_thumbnail = round(1.0 / n_frame, 5)
 height_trigger  = int(height_image * (scale_thumbnail + 1) + 0.5)
 width_trigger   = round(width_image * scale_thumbnail, 2)
 width_thumbnail = int(width_trigger + 0.9999)
 for i_frame in range(n_frame):
  x_trigger = round(i_frame * width_trigger, 2)
  out_mains.append('''\
  <g class="frame">
   <g class="content">
    <use xlink:href="#image_{i_frame}"/>
   </g>
   <g class="trigger" transform="translate({x_trigger},{height_image})">
    <use xlink:href="#image_{i_frame}" transform="scale({scale_thumbnail})"/>
    <use xlink:href="#triggers"/>
   </g>
   <!-- <title>frame {i_frame}</title> -->
  </g>'''.format(**locals()))
 out_main         = '\n'.join(out_mains)
 title            = basename.replace('_', ' ')
 stroke_width     = max(width_image, height_image) / 200
 font_size        = width_image  / 20
 x_help           = width_image  / 2
 y_help           = height_image / 2
 height_thumbnail = height_image * scale_thumbnail - stroke_width / 2
 width_images     = [width_image * multiple for multiple in range(99)]
 ## Compile everything into an .svg file
 file_out = open(basename + '.svg', 'w')
 try: ## use try/finally so that file is closed even if write fails
  print("write SVG")
  file_out.write('''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 {width_image} {height_trigger}">
 <title>{title}</title>
 <desc>Interactive SVG by CMG Lee of the GIF animation at {url} . Move left and right over the SVG image {message_action}.</desc>
 <style type="text/css">
  #main {{ font-family:Helvetica,Arial,sans-serif; font-size:{font_size}px; text-anchor:middle;
           stroke-width:{stroke_width}; fill:#000000; }}
  #trigger              {{ stroke:none; fill-opacity:0; }}
  .frame       .content {{ visibility:hidden; pointer-events:none; fill:#000000; }}
  .frame       .trigger {{ opacity:0.5; cursor:ew-resize; }}
  .frame:hover .content {{ visibility:visible; }}
  .frame:hover .trigger {{ opacity:1; pointer-events:auto; font-weight:bold; stroke:#ff0000; }}
 </style>
 <defs>
  <g id="help">
   <text x="{x_help}" y="{y_help}" dy="-1ex">Move left and right</text>
   <text x="{x_help}" y="{y_help}" dy="1ex">{message_action}</text>
  </g>
  <rect id="trigger" x="0" y="-4999" width="{width_thumbnail}" height="9999"/>
  <g id="triggers">
   <rect x="0" y="0" width="{width_thumbnail}" height="{height_thumbnail}" fill="none"/>
   <use xlink:href="#trigger"/>
   <use xlink:href="#trigger" transform="translate(-{width_images[1]},0)"/>
   <use xlink:href="#trigger" transform="translate( {width_images[1]},0)"/>
   <use xlink:href="#trigger" transform="translate(-{width_images[2]},0)"/>
   <use xlink:href="#trigger" transform="translate( {width_images[2]},0)"/>
   <use xlink:href="#trigger" transform="translate(-{width_images[3]},0)"/>
   <use xlink:href="#trigger" transform="translate( {width_images[3]},0)"/>
   <use xlink:href="#trigger" transform="translate(-{width_images[4]},0)"/>
   <use xlink:href="#trigger" transform="translate( {width_images[4]},0)"/>
  </g>
{out_image}
 </defs>
 <g id="main">
  <circle cx="0" cy="0" r="9999" fill="#ffffff"/>
  <use xlink:href="#image_0" opacity="0.5"/>
  <use xlink:href="#help" stroke-opacity="0.5" stroke="#ffffff"/>
  <use xlink:href="#help"/>
  <g id="frames">
{out_main}
  </g>
 </g>
</svg>
'''.format(**locals()))
 finally:
  file_out.close()

n_argv = len(sys.argv)
if (n_argv < 2):
 print(("usage: {sys.argv[0]} <URL of GIF animation file> [<use every nth GIF frame;" +
        " negative reverses order>] [<action message or '3D' or 'time'>]")
       .format(**locals()))
else:
 make_svg(    sys.argv[1],
          int(sys.argv[2]) if (n_argv > 2) else 1,
              sys.argv[3]  if (n_argv > 3) else '3D')