Skip to content

Instantly share code, notes, and snippets.

@mattolson
Created April 9, 2013 15:20

Revisions

  1. mattolson created this gist Apr 9, 2013.
    130 changes: 130 additions & 0 deletions unzoomify
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,130 @@
    #!/usr/bin/env ruby

    require 'rubygems'
    require 'fileutils'
    require 'open-uri'
    require 'rexml/document'
    require 'tempfile'
    require 'shellwords'

    module Shell
    # Exceptions
    class CommandFailed < RuntimeError; end

    def run(command, args="")
    full_command = "#{command} #{escape_args(args)}"
    puts "Running command: #{full_command}"
    begin
    result = `#{full_command}`
    rescue Errno::ENOENT
    raise_shell_command_failed(full_command)
    end
    if $?.exitstatus == 1
    throw :unable_to_handle
    elsif !$?.success?
    raise_shell_command_failed(full_command)
    end
    end

    def raise_shell_command_failed(command)
    raise CommandFailed, "Command failed (#{command}) with exit status #{$?.exitstatus}"
    end

    def escape_args(args)
    args.shellsplit.map do |arg|
    quote arg.gsub(/\\?'/, %q('\\\\''))
    end.join(' ')
    end

    def quote(s)
    "'#{s}'"
    end
    end

    class Zoomify
    TILESIZE = 256
    include Shell

    def unzoomify(url, level=5)
    @url = url
    @level = level
    @output_filename = "tmp/#{File.basename(@url)}.png"

    # Read properties file
    doc = nil
    begin
    open("#{@url}/ImageProperties.xml") do |f|
    doc = REXML::Document.new(f)
    end
    rescue OpenURI::HTTPError
    puts "ERROR: Failed to open ImageProperties.xml"
    return nil
    end

    # Sanity check
    attrs = doc.root.attributes
    unless attrs['TILESIZE'] == TILESIZE.to_s && attrs['VERSION'] == '1.8'
    puts "ERROR: unexpected tile size"
    return nil
    end

    # Get properties
    width = attrs['WIDTH'].to_i
    height = attrs['HEIGHT'].to_i
    num_tiles = attrs['NUMTILES'].to_i
    num_tiles_x = width / TILESIZE
    num_tiles_y = height / TILESIZE

    @tile_groups = (0..num_tiles/TILESIZE).to_a

    image = transparent(width, height)
    (0..num_tiles_x).each do |column|
    (0..num_tiles_y).each do |row|
    # Get tile
    filename = "#{@level}-#{column}-#{row}.jpg"
    puts "Getting #{filename}..."
    tile_image = get_tile(filename)

    # Merge it into master image
    composite!(tile_image, column*TILESIZE, row*TILESIZE) if tile_image
    end
    end

    return @output_filename
    end

    def get_tile(filename)
    @tile_groups.each do |group|
    begin
    tile_url = "#{@url}/TileGroup#{group}/#{filename}"
    file_ext = ".#{File.extname(filename).split('.').last}"
    tempfile = ::Tempfile.new ['unzoomify', file_ext]
    tempfile.binmode

    puts "Getting #{tile_url}..."
    open(tile_url) do |f|
    tempfile.write(f.read)
    end

    tempfile.close
    return tempfile
    rescue OpenURI::HTTPError, Errno::ENOENT
    puts "Not found."
    end
    end
    puts "WARNING: (#{@url}, #{filename}) appears to be missing or invalid"
    nil
    end

    def transparent(width, height)
    run :convert, %(-size #{width}x#{height} canvas:none #{@output_filename})
    end

    def composite!(tile, x, y)
    run :composite, %(-compose src-over -geometry '#{TILESIZE}x#{TILESIZE}+#{x}+#{y}' #{tile.path} #{@output_filename} #{@output_filename})
    end
    end

    z = Zoomify.new
    full_image_path = z.unzoomify 'directory_or_url'
    puts full_image_path ? "Full size image written to #{full_image_path}." : "ERROR"