first commit
This commit is contained in:
commit
9716b85a19
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2008 Donald A. Ball Jr. <donald.ball@gmail.com>
|
||||||
|
|
||||||
|
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.
|
|
@ -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/
|
|
@ -0,0 +1,2 @@
|
||||||
|
require 'zoomifier'
|
||||||
|
Zoomifier::zoomify(ARGV[0])
|
|
@ -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. <donald.ball@gmail.com>
|
||||||
|
# @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("<IMAGE_PROPERTIES WIDTH=\"#{image.columns}\" HEIGHT=\"#{image.rows}\" NUMTILES=\"#{tiles}\" NUMIMAGES=\"1\" VERSION=\"1.8\" TILESIZE=\"#{TILESIZE}\" />")
|
||||||
|
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
|
|
@ -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
|
Loading…
Reference in New Issue