From 9716b85a19bcae3d3280720475c3893d157a207c Mon Sep 17 00:00:00 2001 From: Donald Ball Date: Thu, 20 Nov 2008 23:40:59 -0600 Subject: [PATCH] first commit --- LICENSE.txt | 13 ++++ README.txt | 4 ++ bin/zoomify | 2 + lib/zoomifier.rb | 173 ++++++++++++++++++++++++++++++++++++++++++++++ zoomifier.gemspec | 23 ++++++ 5 files changed, 215 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.txt create mode 100644 bin/zoomify create mode 100644 lib/zoomifier.rb create mode 100644 zoomifier.gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1a8faf6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + Copyright 2008 Donald A. Ball Jr. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..74dadd6 --- /dev/null +++ b/README.txt @@ -0,0 +1,4 @@ +Zoomifier is a ruby library for creating directories of tiled images +suitable for viewing with the free Zoomify flash player: + +http://www.zoomify.com/ diff --git a/bin/zoomify b/bin/zoomify new file mode 100644 index 0000000..61dd9df --- /dev/null +++ b/bin/zoomify @@ -0,0 +1,2 @@ +require 'zoomifier' +Zoomifier::zoomify(ARGV[0]) diff --git a/lib/zoomifier.rb b/lib/zoomifier.rb new file mode 100644 index 0000000..0856dbb --- /dev/null +++ b/lib/zoomifier.rb @@ -0,0 +1,173 @@ +require 'fileutils' +require 'open-uri' +require 'rexml/document' +require_gem 'rmagick' + +# Breaks up images into tiles suitable for viewing with Zoomify. +# See http://zoomify.com/ for more details. +# +# @author Donald A. Ball Jr. +# @version 1.1 +# @copyright (C) 2008 Donald A. Ball Jr. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Zoomifier + + TILESIZE = 256 + + # Zoomifies the image file specified by filename. The zoomified directory + # name will be the filename without its extension, e.g. 5.jpg will be + # zoomified into a directory named 5. If there is already a directory with + # this name, it will be destroyed without mercy. + def self.zoomify(filename) + raise ArgumentError unless File.exists?(filename) && File.file?(filename) + outputdir = File.basename(filename, '.*') + raise ArgumentError unless filename != outputdir + FileUtils.rm_rf(outputdir) if File.exists?(outputdir) + Dir.mkdir(outputdir) + tmpdir = "#{outputdir}/tmp" + Dir.mkdir(tmpdir) + tilesdir = nil + image = Magick::Image.read(filename).first.strip! + # Each level of zoom is a factor of 2. Here we obtain the number of zooms + # allowed by the original file dimensions and the constant tile size. + levels = (Math.log([image.rows, image.columns].max.to_f / TILESIZE) / Math.log(2)).ceil + tiles = 0 + (0..levels).each do |level| + n = levels - level + # Obtain the image to tile for this level. The 0th level should consist + # of one tile, while the highest level should be the original image. + level_image = image.resize(image.columns >> n, image.rows >> n) + tiles(tmpdir, level, level_image) do |filename| + # The tile images are chunked into directories named TileGroupN, N + # starting at 0 and increasing monotonically. Each directory contains + # at most 256 images. The images are sorted by level, tile row, and + # tile column. + div, mod = tiles.divmod(256) + if mod == 0 + tilesdir = "#{outputdir}/TileGroup#{div}" + Dir.mkdir(tilesdir) + end + FileUtils.mv("#{tmpdir}/#{filename}", "#{tilesdir}/#{filename}") + tiles += 1 + end + # Rmagick needs a bit of help freeing image memory. + level_image = nil + GC.start + end + File.open("#{outputdir}/ImageProperties.xml", 'w') do |f| + f.write("") + end + Dir.rmdir(tmpdir) + outputdir + end + + # Splits the given image up into images of TILESIZE, writes them to the + # given directory, and yields their names + def self.tiles(dir, level, image) + slice(image.rows).each_with_index do |y_slice, j| + slice(image.columns).each_with_index do |x_slice, i| + # The images are named "level-column-row.jpg" + filename = "#{level}-#{i}-#{j}.jpg" + tile_image = image.crop(x_slice[0], y_slice[0], x_slice[1], y_slice[1]) + tile_image.write("#{dir}/#{filename}") do + # FIXME - the images end up being 4-5x larger than those produced + # by Zoomifier EZ and friends... no idea why just yet, except to note + # that the density of these tiles ends up being 400x400, while + # everybody else produces tiles at 72x72. Can't see why that would + # matter though... + self.quality = 80 + end + # Rmagick needs a bit of help freeing image memory. + tile_image = nil + GC.start + yield filename + end + end + end + + # Returns an array of slices ([offset, length]) obtained by slicing the + # given number by TILESIZE. + # E.g. 256 -> [[0, 256]], 257 -> [[0, 256], [256, 1]], + # 513 -> [[0, 256], [256, 256], [512, 1]] + def self.slice(n) + results = [] + i = 0 + while true + if i + TILESIZE >= n + results << [i, n-i] + break + else + results << [i, TILESIZE] + i += TILESIZE + end + end + results + end + + def self.unzoomify(url) + tmpdir = 'tmp' + FileUtils.rm_rf(tmpdir) if File.exists?(tmpdir) + Dir.mkdir(tmpdir) + doc = nil + begin + open("#{url}/ImageProperties.xml") do |f| + doc = REXML::Document.new(f) + end + rescue OpenURI::HTTPError + return nil + end + attrs = doc.root.attributes + return nil unless attrs['TILESIZE'] == '256' && attrs['VERSION'] == '1.8' + width = attrs['WIDTH'].to_i + height = attrs['HEIGHT'].to_i + tiles = attrs['NUMTILES'].to_i + image_paths = (0 .. tiles/256).map {|n| "TileGroup#{n}"} + max_level = 0 + while (get_tile(url, image_paths, tmpdir, "#{max_level}-0-0.jpg")) + max_level += 1 + end + max_level -= 1 + image = Magick::Image.new(width, height) + (0 .. width / TILESIZE).each do |column| + (0 .. height / TILESIZE).each do |row| + filename = "#{max_level}-#{column}-#{row}.jpg" + get_tile(url, image_paths, tmpdir, filename) + tile_image = Magick::Image.read("#{tmpdir}/#{filename}").first + image.composite!(tile_image, column*TILESIZE, row*TILESIZE, Magick::OverCompositeOp) + time_image = nil + GC.start + end + end + # FIXME - get filename from the url + image.write('file.jpg') { self.quality = 90 } + image = nil + GC.start + FileUtils.rm_rf(tmpdir) + end + + # TODO - could reduce the miss rate by using heuristics to guess the + # proper path from which to download the file + def self.get_tile(url, image_paths, tmpdir, filename) + image_paths.each do |path| + begin + open("#{tmpdir}/#{filename}", 'wb') {|f| f.write(open("#{url}/#{path}/#{filename}").read)} + return filename + rescue OpenURI::HTTPError + end + end + nil + end + +end diff --git a/zoomifier.gemspec b/zoomifier.gemspec new file mode 100644 index 0000000..13a66c6 --- /dev/null +++ b/zoomifier.gemspec @@ -0,0 +1,23 @@ +require 'rubygems' +spec = Gem::Specification.new do |s| + s.name = 'Zoomifier' + s.version = '1.1' + s.author = 'Donald Ball' + s.email = 'donald.ball@gmail.com' + s.platform = Gem::Platform::RUBY + s.summary = 'A library for zoomifying images' + s.files = ['lib/zoomifier.rb', 'bin/zoomify'] + s.require_path = 'lib' + s.autorequire = 'zoomifier' + s.bindir = 'bin' + s.executables = ['zoomify'] + s.default_executable = 'zoomify' + s.has_rdoc = true + s.extra_rdoc_files = ['README.txt'] + s.add_dependency('rmagick') +end + +if $0 == __FILE__ + Gem::manage_gems + Gem::Builder.new(spec).build +end