Skip to content

Instantly share code, notes, and snippets.

@henrik
Created February 6, 2009 21:49
Show Gist options
  • Save henrik/59636 to your computer and use it in GitHub Desktop.
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 Jun 1, 2010

Raff13: That page creates the Zoomify Flash embed code with JavaScript. I just updated dezoomify to support that. So with the current version of dezoomify, this should work:

ruby dezoomify.rb "http://www.natgeomaps.com/ti_104_zoomify.html?zoomifyImagePath=assets/files/zoomify/ti00000104/ti00000104_3_img&amp;zoomifyNavigatorVisible=false"

@Raff13
Copy link

Raff13 commented Jun 7, 2010

Henrik, it works very well now. And it works on Windows. One needs only Ruby and Imagemagick plus installing nokogiri gem. Then it's OK.

@Filipvl
Copy link

Filipvl commented Apr 6, 2011

Raff13, how do you install nokogiri gem on your pc?

@mi3gto
Copy link

mi3gto commented Oct 11, 2011

Help I really really need to extract images from a zoomify ste but know squat about programing, I did see a web page that you entered a url containing the zoomify'ed image but all it shows is a brief red line. Is there a simple command line or a web site i can enter a url string into that will extract the image at max res, I can supply the url if anyone can help please....please....gs

@mai9
Copy link

mai9 commented Dec 10, 2011

hi, I installed nokogiri, but it's not downloading any images. I tried the command line showed in the Usage section aswell as the one henrik posted on June 1st 2010. in both cases gives error on line 66, and it also says "0.0 Found image" which I understand it means that it can't find any image.

@henrik
Copy link
Author

henrik commented Dec 10, 2011

@mai9 What is the error message exactly? The "0.0" just means it attempted to download from the first URL given (starts counting from 0), and the first image on that page if there are many.

@mai9
Copy link

mai9 commented Dec 11, 2011

thanks for your fast reply henrik.
here's the screenshot: http://tinypic.com/r/2zevthh/5
the url I am trying to use is: http://www.christies.com/lotfinder/ZoomImage.aspx?image=/LotFinderImages/D52029/D5202920

I hope this information helps.

@henrik
Copy link
Author

henrik commented Dec 11, 2011 via email

@mai9
Copy link

mai9 commented Dec 12, 2011

thank you, that worked wonders :)

@billflu
Copy link

billflu commented Dec 23, 2011

Hello, I'm having trouble with this one:
http://maps.bpl.org/zoomify.php?baseUrl=http://maps.bpl.org/&viewer=modern&id=06_01_002652

The ZoomifyImagePath is javascript

Also, here is the path of the tiles:
http://maps.bpl.org/pub/zoom/06/01/06_01_002652/

@kugland
Copy link

kugland commented Apr 6, 2012

Hello, your script did not work with this image, I believe it extracted the wrong URL:

http://link.library.utoronto.ca/hollar/digobject.cfm?Idno=Hollar_k_0201&size=zoom&query=Hollar_k_0201&type=browse

Here is the result:

% ./dezoomify.rb 'http://link.library.utoronto.ca/hollar/digobject.cfm?Idno=Hollar_k_0201&size=zoom&query=Hollar_k_0201&type=browse'
0. Visiting http://link.library.utoronto.ca/hollar/digobject.cfm?Idno=Hollar_k_0201&size=zoom&query=Hollar_k_0201&type=browse
  0.0 Found image path http://link.library.utoronto.ca/hollar/http%3A%2F%2Fwww%2Elibrary%2Eutoronto%2Eca%2Fhollar%2Fzoomify%2FHollar_k_0201%2FHollar_k_0201_0001%2F/
/usr/lib/ruby/1.9.1/open-uri.rb:346:in `open_http': 404 Not Found (OpenURI::HTTPError)
        from /usr/lib/ruby/1.9.1/open-uri.rb:775:in `buffer_open'
        from /usr/lib/ruby/1.9.1/open-uri.rb:203:in `block in open_loop'
        from /usr/lib/ruby/1.9.1/open-uri.rb:201:in `catch'
        from /usr/lib/ruby/1.9.1/open-uri.rb:201:in `open_loop'
        from /usr/lib/ruby/1.9.1/open-uri.rb:146:in `open_uri'
        from /usr/lib/ruby/1.9.1/open-uri.rb:677:in `open'
        from /usr/lib/ruby/1.9.1/open-uri.rb:29:in `open'
        from ./dezoomify.rb:35:in `block (2 levels) in <main>'
        from ./dezoomify.rb:28:in `each'
        from ./dezoomify.rb:28:in `each_with_index'
        from ./dezoomify.rb:28:in `block in <main>'
        from ./dezoomify.rb:22:in `each'
        from ./dezoomify.rb:22:in `each_with_index'
        from ./dezoomify.rb:22:in `<main>'

@henrik
Copy link
Author

henrik commented Apr 9, 2012

@kugland I think I fixed it – try again with the current version of the script!

@kugland
Copy link

kugland commented Apr 9, 2012

Now it worked perfectly! But it still gives this warning, “./dezoomify.rb:12: Use RbConfig instead of obsolete and deprecated Config.”, which I removed by simply replacing “Config” with “RbConfig” in the aforementioned line.

@henrik
Copy link
Author

henrik commented Apr 9, 2012 via email

@nicetry158
Copy link

I am having trouble with the following image. Can you help? Note, this zoomify image displays fine in IE/Chrome/Safari. I had trouble viewing with firefox (not sure why).

http://220.227.252.236/ehmr/Ibrahimpatanam%20and%20Manchal.aspx

$ ./dezoomify.rb 'http://220.227.252.236/ehmr/Ibrahimpatanam%20and%20Manchal.aspx'
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in gem_original_require': no such file to load -- nokogiri (LoadError) from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:inrequire'
from ./dezoomify.rb:8

@henrik
Copy link
Author

henrik commented May 18, 2013

@nicetry158 I think maybe OS X used to include the nokogiri Ruby gem but doesn't anymore.

Something like sudo gem install nokogiri (it will ask for your password) may work to install it. If it seems to successfully install, dezoomify will hopefully work.

@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