Skip to content

Instantly share code, notes, and snippets.

@henrik
Created February 6, 2009 21:49
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save henrik/59636 to your computer and use it in GitHub Desktop.
Stitch Zoomify tiles into a single image (Ruby + ImageMagick).

Dezoomify

Ruby + ImageMagick script to stitch Zoomify Viewer tiles into a single image at the maximum zoom level.

Usage

Give the URL of a page that contains the Zoomify Flash viewer, e.g.:

./dezoomify.rb 'http://www.christies.com/lotfinder/ZoomImage.aspx?image=/LotFinderImages/D52792/D5279274'

The file would end up in /tmp/zoomified-0.0.jpg on Mac/*UX and C:\WINDOWS\Temp\zoomified-0.0.jpg on Windows. (The script has yet to be confirmed to work on Windows.)

You can give multiple URLs as separate arguments:

./dezoomify.rb 'http://collections.frick.org/CUS.18.zoomobject._580$7286*1484740' 'http://collections.frick.org/CUS.18.zoomobject._1106$8983*1484996' 'http://collections.frick.org/CUS.18.zoomobject._1105$7286*1516394'

The files would end up in /tmp/zoomified-0.0.jpg, /tmp/zoomified-1.0.jpg and /tmp/zoomified-2.0.jpg or the equivalent Windows directory.

The first number in the output filename reflects the ordinal of the provided URL. The second number is used if a single URL contains multiple Zoomify viewers.

Note that filenames do not increment from run to run, so beware of overwriting previously dezoomified files.

If you're on OS X, each file will be revealed in the Finder as download and stitching is completed.

Credits and license

Copyright (c) 2009 Henrik Nyh

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#!/usr/bin/env ruby
# Dezoomify. See README.markdown.
# By Henrik Nyh <http://henrik.nyh.se> 2009-02-06 under the MIT License.
require 'cgi'
require 'open-uri'
require 'rubygems'
require 'nokogiri'
module Kernel
def windows?
RbConfig::CONFIG['host_os'].match(/mswin|windows|mingw/i)
end
end
if windows?
# Case-sensitive. Use forward slashes, or double-escape backslashes.
TEMP_DIRECTORY = "C:/WINDOWS/Temp"
else
TEMP_DIRECTORY = "/tmp"
end
ARGV.each_with_index do |page_url, page_url_index|
if page_url.include?("zoomifyImagePath")
puts "#{page_url_index}. Extracting path from URL"
source = page_url
else
puts "#{page_url_index}. Visiting #{page_url}"
source = open(page_url).read
end
paths = source.scan(/zoomifyImagePath=([^"'&]+)/).flatten.map {|path| path.gsub(' ', '%20') }.uniq
paths.each_with_index do |path, path_index|
path = CGI.unescape(path)
full_path = URI.join(page_url, path+'/')
puts " #{page_url_index}.#{path_index} Found image path #{full_path}"
# <IMAGE_PROPERTIES WIDTH="1737" HEIGHT="2404" NUMTILES="99" NUMIMAGES="1" VERSION="1.8" TILESIZE="256"/>
xml_url = URI.join(full_path.to_s, 'ImageProperties.xml')
doc = Nokogiri::XML(open(xml_url))
props = doc.at('IMAGE_PROPERTIES')
width = props[:WIDTH].to_i
height = props[:HEIGHT].to_i
tilesize = props[:TILESIZE].to_f
tiles_wide = (width/tilesize).ceil
tiles_high = (height/tilesize).ceil
# Determine max zoom level.
# Also determine tile_counts per zoom level, used to determine tile group.
# With thanks to http://trac.openlayers.org/attachment/ticket/1285/zoomify.patch.
zoom = 0
w = width
h = height
tile_counts = []
while w > tilesize || h > tilesize
zoom += 1
t_wide = (w / tilesize).ceil
t_high = (h / tilesize).ceil
tile_counts.unshift t_wide*t_high
w = (w / 2.0).floor
h = (h / 2.0).floor
end
tile_counts.unshift 1 # Zoom level 0 has a single tile.
tile_count_before_level = tile_counts[0..-2].inject(0) {|sum, num| sum + num }
files_by_row = []
tiles_high.times do |y|
row = []
tiles_wide.times do |x|
filename = '%s-%s-%s.jpg' % [zoom, x, y]
local_filepath = "#{TEMP_DIRECTORY}/zoomify-#{filename}"
row << local_filepath
tile_group = ((x + y * tiles_wide + tile_count_before_level) / tilesize).floor
tile_url = URI.join(full_path.to_s, "TileGroup#{tile_group}/#{filename}")
url = URI.join(tile_url.to_s, filename)
puts " Getting #{url}..."
File.open(local_filepath, 'wb') {|f| f.print url.read }
end
files_by_row << row
end
# `montage` is ImageMagick.
# We first stitch together the tiles of each row, then stitch all rows.
# Stitching the full image all at once can get extremely inefficient for large images.
puts " Stitching #{tiles_wide} x #{tiles_high} = #{tiles_wide*tiles_high} tiles..."
row_files = []
files_by_row.each_with_index do |row, index|
filename = "#{TEMP_DIRECTORY}/zoomify-row-#{index}.jpg"
`montage #{row.join(' ')} -geometry +0+0 -tile #{tiles_wide}x1 #{filename}`
row_files << filename
end
filename = "#{TEMP_DIRECTORY}/zoomified-#{page_url_index}.#{path_index}.jpg"
`montage #{row_files.join(' ')} -geometry +0+0 -tile 1x#{tiles_high} #{filename}`
unless windows? || `which xattr`.empty?
# Set "Downloaded from" Finder metadata, like Safari does.
system('xattr', '-w', 'com.apple.metadata:kMDItemWhereFroms', page_url, filename)
end
puts " Done: #{filename}"
# Reveal in Finder if on OS X.
unless windows?
`which osascript && osascript -e 'tell app "Finder"' -e 'reveal POSIX file "#{filename}"' -e 'activate' -e 'end'`
end
end
end
@henrik
Copy link
Author

henrik commented Nov 23, 2013

Andrea G mailed me to suggest this change to handle timeouts. I don't have the time right now to update the script, but I'll paste what was mailed to me:

retries = 10
begin
  Timeout::timeout(5) do
    File.open(local_filepath, 'wb') {|f| f.print url.read }
  end
rescue Timeout::Error
  if retries > 0
    print "Timeout - Retrying... "
    retries -= 1
    retry
  else
    puts "ERROR: Not responding after 10 retries!  Giving up!"
    exit
  end
end

Copy link

ghost commented Mar 20, 2014

This patch will allow this script to stitch together the tiles from a zoom level below the highest, in the event the highest zoom has a ridiculous amount of tiles.

Setting zoom_levels_to_skip to 1 will download and stitch the second highest zoom tiles, setting it to 2 will download and stitch 2 from the highest zoomed tiles, etc. Leaving it at 0 will obviously leave the default functionality - the highest zoom level.

49,50c49,52
<     tiles_wide = (width/tilesize).ceil
<     tiles_high = (height/tilesize).ceil

---
>     zoom_levels_to_skip = 0
> 
>     tiles_wide = (width/tilesize/(2**zoom_levels_to_skip)).ceil
>     tiles_high = (height/tilesize/(2**zoom_levels_to_skip)).ceil
69a72
>     tile_counts = tile_counts[0..-(zoom_levels_to_skip + 1)]
76c79
<         filename = '%s-%s-%s.jpg' % [zoom, x, y]

---
>         filename = '%s-%s-%s.jpg' % [zoom - zoom_levels_to_skip, x, y]

@putt1ck
Copy link

putt1ck commented Feb 21, 2015

Installing on openSUSE Factory: install ruby-devel, then gem install nokogiri can complete.

Works very well, thx :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment