zoomifier 1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +43 -0
- data/bin/zoomify +4 -0
- data/lib/zoomifier.rb +184 -0
- data/spec/data/1024x768.jpg +0 -0
- data/spec/data/2973x2159.jpg +0 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/zoomifier_spec.rb +115 -0
- metadata +68 -0
data/README.markdown
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
Zoomifier
|
2
|
+
=========
|
3
|
+
__Version:__ 1.3 (November 24, 2008)
|
4
|
+
|
5
|
+
__Authors:__ [Donald Ball](mailto:donald.ball@gmail.com)
|
6
|
+
|
7
|
+
__Copyright:__ Copyright (c) 2008, Donald Ball
|
8
|
+
|
9
|
+
__License:__ Apache Public License, v2.0
|
10
|
+
|
11
|
+
Zoomifier is a ruby library for creating directories of tiled images suitable for viewing with the free Zoomify flash player:
|
12
|
+
|
13
|
+
http://www.zoomify.com/
|
14
|
+
|
15
|
+
as well as a rails plugin that provides a helper method to make adding zoomified images to your rails app very easy.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
To install the plugin:
|
20
|
+
|
21
|
+
script/plugin install git://github.com/dball/zoomifier.git
|
22
|
+
|
23
|
+
If you want the standalone library for whatever reason:
|
24
|
+
|
25
|
+
sudo gem install dball-zoomifier
|
26
|
+
|
27
|
+
all this gets you is the zoomify script installed in your PATH, though, and the free convert released by Zoomify is quite a bit faster.
|
28
|
+
|
29
|
+
I'm working on a GemPlugin version, but I can't seem to figure out how you're supposed to have assets installed; there doesn't seem to be any command which runs the GemPlugin's install.rb script.
|
30
|
+
|
31
|
+
## Testing
|
32
|
+
|
33
|
+
Install the rspec gem, if you don't already have it, then run spec spec from the vendor/plugins/zoomifier directory.
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
In your views, wherever you want a zoomified image:
|
38
|
+
|
39
|
+
<%= zoomify_image_tag ('image.jpg', { :id => 'foo', :width => 400, :height => 300 }) %>
|
40
|
+
|
41
|
+
This will render a zoomified image with the specified dimensions using the swfobject Javascript library. An image tag with given attributes, except for the id (which is used to tag the div wrapper), is generated as a fallback for users without Javascript, so feel free to feed it alt and title and all that other good stuff as you see fit.
|
42
|
+
|
43
|
+
The directory of zoomified tiles is create in the same directory as the image, using its name without its extension, e.g. image/ in this example. The images will be automatically created if they do not exist already, or if the image file is newer than its tiles. Bear in mind this process can be fairly slow for large images, and of course, there's no point in zoomifying small images, so be patient on your first request. If there's sufficient, or, hell, any interest, I could write up some rake tasks to do this ahead of time.
|
data/bin/zoomify
ADDED
data/lib/zoomifier.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rmagick'
|
5
|
+
|
6
|
+
# Breaks up images into tiles suitable for viewing with Zoomify.
|
7
|
+
# See http://zoomify.com/ for more details.
|
8
|
+
#
|
9
|
+
# @author Donald A. Ball Jr. <donald.ball@gmail.com>
|
10
|
+
# @copyright (C) 2008 Donald A. Ball Jr.
|
11
|
+
#
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
13
|
+
# you may not use this file except in compliance with the License.
|
14
|
+
# You may obtain a copy of the License at
|
15
|
+
#
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
17
|
+
#
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
21
|
+
# See the License for the specific language governing permissions and
|
22
|
+
# limitations under the License.
|
23
|
+
|
24
|
+
module Zoomifier
|
25
|
+
|
26
|
+
TILESIZE = 256
|
27
|
+
|
28
|
+
# Zoomifies the image file specified by filename. The zoomified directory
|
29
|
+
# name will be the filename without its extension, e.g. 5.jpg will be
|
30
|
+
# zoomified into a directory named 5. If there is already a directory with
|
31
|
+
# this name, it will be destroyed without mercy.
|
32
|
+
def self.zoomify(filename)
|
33
|
+
raise ArgumentError unless filename && File.file?(filename)
|
34
|
+
#filename = File.expand_path(filename)
|
35
|
+
outputdir = File.dirname(filename) + '/' + File.basename(filename, '.*')
|
36
|
+
raise ArgumentError unless filename != outputdir
|
37
|
+
if File.directory?(outputdir) && File.file?(outputdir + '/ImageProperties.xml') && File.mtime(filename) <= File.mtime(outputdir + '/ImageProperties.xml')
|
38
|
+
return
|
39
|
+
end
|
40
|
+
FileUtils.rm_rf(outputdir) if File.exists?(outputdir)
|
41
|
+
Dir.mkdir(outputdir)
|
42
|
+
tmpdir = "#{outputdir}/tmp"
|
43
|
+
Dir.mkdir(tmpdir)
|
44
|
+
tilesdir = nil
|
45
|
+
image = Magick::Image.read(filename).first.strip!
|
46
|
+
# Each level of zoom is a factor of 2. Here we obtain the number of zooms
|
47
|
+
# allowed by the original file dimensions and the constant tile size.
|
48
|
+
levels = (Math.log([image.rows, image.columns].max.to_f / TILESIZE) / Math.log(2)).ceil
|
49
|
+
tiles = 0
|
50
|
+
(0..levels).each do |level|
|
51
|
+
n = levels - level
|
52
|
+
# Obtain the image to tile for this level. The 0th level should consist
|
53
|
+
# of one tile, while the highest level should be the original image.
|
54
|
+
level_image = image.resize(image.columns >> n, image.rows >> n)
|
55
|
+
tiles(tmpdir, level, level_image) do |filename|
|
56
|
+
# The tile images are chunked into directories named TileGroupN, N
|
57
|
+
# starting at 0 and increasing monotonically. Each directory contains
|
58
|
+
# at most 256 images. The images are sorted by level, tile row, and
|
59
|
+
# tile column.
|
60
|
+
div, mod = tiles.divmod(256)
|
61
|
+
if mod == 0
|
62
|
+
tilesdir = "#{outputdir}/TileGroup#{div}"
|
63
|
+
Dir.mkdir(tilesdir)
|
64
|
+
end
|
65
|
+
FileUtils.mv("#{tmpdir}/#{filename}", "#{tilesdir}/#{filename}")
|
66
|
+
tiles += 1
|
67
|
+
end
|
68
|
+
# Rmagick needs a bit of help freeing image memory.
|
69
|
+
level_image = nil
|
70
|
+
GC.start
|
71
|
+
end
|
72
|
+
File.open("#{outputdir}/ImageProperties.xml", 'w') do |f|
|
73
|
+
f.write("<IMAGE_PROPERTIES WIDTH=\"#{image.columns}\" HEIGHT=\"#{image.rows}\" NUMTILES=\"#{tiles}\" NUMIMAGES=\"1\" VERSION=\"1.8\" TILESIZE=\"#{TILESIZE}\" />")
|
74
|
+
end
|
75
|
+
Dir.rmdir(tmpdir)
|
76
|
+
outputdir
|
77
|
+
end
|
78
|
+
|
79
|
+
# Splits the given image up into images of TILESIZE, writes them to the
|
80
|
+
# given directory, and yields their names
|
81
|
+
def self.tiles(dir, level, image)
|
82
|
+
slice(image.rows).each_with_index do |y_slice, j|
|
83
|
+
slice(image.columns).each_with_index do |x_slice, i|
|
84
|
+
# The images are named "level-column-row.jpg"
|
85
|
+
filename = "#{level}-#{i}-#{j}.jpg"
|
86
|
+
tile_image = image.crop(x_slice[0], y_slice[0], x_slice[1], y_slice[1])
|
87
|
+
tile_image.write("#{dir}/#{filename}") do
|
88
|
+
# FIXME - the images end up being 4-5x larger than those produced
|
89
|
+
# by Zoomifier EZ and friends... no idea why just yet, except to note
|
90
|
+
# that the density of these tiles ends up being 400x400, while
|
91
|
+
# everybody else produces tiles at 72x72. Can't see why that would
|
92
|
+
# matter though...
|
93
|
+
self.quality = 80
|
94
|
+
end
|
95
|
+
# Rmagick needs a bit of help freeing image memory.
|
96
|
+
tile_image = nil
|
97
|
+
GC.start
|
98
|
+
yield filename
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns an array of slices ([offset, length]) obtained by slicing the
|
104
|
+
# given number by TILESIZE.
|
105
|
+
# E.g. 256 -> [[0, 256]], 257 -> [[0, 256], [256, 1]],
|
106
|
+
# 513 -> [[0, 256], [256, 256], [512, 1]]
|
107
|
+
def self.slice(n)
|
108
|
+
results = []
|
109
|
+
i = 0
|
110
|
+
while true
|
111
|
+
if i + TILESIZE >= n
|
112
|
+
results << [i, n-i]
|
113
|
+
break
|
114
|
+
else
|
115
|
+
results << [i, TILESIZE]
|
116
|
+
i += TILESIZE
|
117
|
+
end
|
118
|
+
end
|
119
|
+
results
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.unzoomify(url)
|
123
|
+
tmpdir = 'tmp'
|
124
|
+
FileUtils.rm_rf(tmpdir) if File.exists?(tmpdir)
|
125
|
+
Dir.mkdir(tmpdir)
|
126
|
+
doc = nil
|
127
|
+
begin
|
128
|
+
open("#{url}/ImageProperties.xml") do |f|
|
129
|
+
doc = REXML::Document.new(f)
|
130
|
+
end
|
131
|
+
rescue OpenURI::HTTPError
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
attrs = doc.root.attributes
|
135
|
+
return nil unless attrs['TILESIZE'] == '256' && attrs['VERSION'] == '1.8'
|
136
|
+
width = attrs['WIDTH'].to_i
|
137
|
+
height = attrs['HEIGHT'].to_i
|
138
|
+
tiles = attrs['NUMTILES'].to_i
|
139
|
+
image_paths = (0 .. tiles/256).map {|n| "TileGroup#{n}"}
|
140
|
+
max_level = 0
|
141
|
+
while (get_tile(url, image_paths, tmpdir, "#{max_level}-0-0.jpg"))
|
142
|
+
max_level += 1
|
143
|
+
end
|
144
|
+
max_level -= 1
|
145
|
+
image = Magick::Image.new(width, height)
|
146
|
+
(0 .. width / TILESIZE).each do |column|
|
147
|
+
(0 .. height / TILESIZE).each do |row|
|
148
|
+
filename = "#{max_level}-#{column}-#{row}.jpg"
|
149
|
+
get_tile(url, image_paths, tmpdir, filename)
|
150
|
+
tile_image = Magick::Image.read("#{tmpdir}/#{filename}").first
|
151
|
+
image.composite!(tile_image, column*TILESIZE, row*TILESIZE, Magick::OverCompositeOp)
|
152
|
+
time_image = nil
|
153
|
+
GC.start
|
154
|
+
end
|
155
|
+
end
|
156
|
+
# FIXME - get filename from the url
|
157
|
+
image.write('file.jpg') { self.quality = 90 }
|
158
|
+
image = nil
|
159
|
+
GC.start
|
160
|
+
FileUtils.rm_rf(tmpdir)
|
161
|
+
end
|
162
|
+
|
163
|
+
# TODO - could reduce the miss rate by using heuristics to guess the
|
164
|
+
# proper path from which to download the file
|
165
|
+
def self.get_tile(url, image_paths, tmpdir, filename)
|
166
|
+
image_paths.each do |path|
|
167
|
+
begin
|
168
|
+
open("#{tmpdir}/#{filename}", 'wb') {|f| f.write(open("#{url}/#{path}/#{filename}").read)}
|
169
|
+
return filename
|
170
|
+
rescue OpenURI::HTTPError
|
171
|
+
end
|
172
|
+
end
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
if __FILE__ == $0
|
179
|
+
if ARGV.length == 1
|
180
|
+
Zoomifier::zoomify(ARGV[0])
|
181
|
+
else
|
182
|
+
puts "Usage: zoomify filename"
|
183
|
+
end
|
184
|
+
end
|
Binary file
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'zoomifier'
|
3
|
+
|
4
|
+
describe Zoomifier do
|
5
|
+
it "should respond to its main method" do
|
6
|
+
Zoomifier.should respond_to(:zoomify)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "all images", :shared => true do
|
10
|
+
before(:all) do
|
11
|
+
FileUtils.rm_rf(@output)
|
12
|
+
Zoomifier::zoomify(@input)
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:all) do
|
16
|
+
FileUtils.rm_rf(@output)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should create the output directory" do
|
20
|
+
File.directory?(@output).should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should create the image properties file" do
|
24
|
+
File.file?(@output + 'ImageProperties.xml').should be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should create the tiles" do
|
28
|
+
tiles = Dir.glob(@output + 'TileGroup*/*.jpg')
|
29
|
+
tiles.sort.should == @tiles.map {|file| @output + file }.sort
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should create tiles of 256x256 or less" do
|
33
|
+
@tiles.each do |file|
|
34
|
+
image = Magick::Image.read(@output + file).first
|
35
|
+
image.rows.should <= 256
|
36
|
+
image.columns.should <= 256
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not recreate the tiles if the image date precedes them" do
|
41
|
+
old_timestamps = timestamps
|
42
|
+
sleep(1)
|
43
|
+
Zoomifier::zoomify(@input)
|
44
|
+
old_timestamps.should == timestamps
|
45
|
+
end
|
46
|
+
|
47
|
+
def timestamps
|
48
|
+
timestamps = {}
|
49
|
+
['ImageProperties.xml', 'TileGroup*/*.jpg'].each do |pattern|
|
50
|
+
Dir.glob(@output + pattern) do |filename|
|
51
|
+
timestamps[filename] = File.mtime(filename)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
timestamps
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "On a 1024x768 JPEG file" do
|
59
|
+
before(:all) do
|
60
|
+
@input = File.dirname(__FILE__) + '/data/1024x768.jpg'
|
61
|
+
@output = File.dirname(__FILE__) + '/data/1024x768/'
|
62
|
+
@tiles = %w[0-0-0.jpg 1-1-1.jpg 2-1-0.jpg 2-2-1.jpg 2-3-2.jpg
|
63
|
+
1-0-0.jpg 2-0-0.jpg 2-1-1.jpg 2-2-2.jpg
|
64
|
+
1-0-1.jpg 2-0-1.jpg 2-1-2.jpg 2-3-0.jpg
|
65
|
+
1-1-0.jpg 2-0-2.jpg 2-2-0.jpg 2-3-1.jpg].map do |file|
|
66
|
+
'TileGroup0/' + file
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it_should_behave_like "all images"
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "On a 2973x2159 JPEG file" do
|
74
|
+
before(:all) do
|
75
|
+
@input = File.dirname(__FILE__) + '/data/2973x2159.jpg'
|
76
|
+
@output = File.dirname(__FILE__) + '/data/2973x2159/'
|
77
|
+
@tiles = %w[
|
78
|
+
0-0-0.jpg 3-3-2.jpg 4-10-0.jpg 4-3-4.jpg 4-6-8.jpg
|
79
|
+
1-0-0.jpg 3-3-3.jpg 4-10-1.jpg 4-3-5.jpg 4-7-0.jpg
|
80
|
+
1-0-1.jpg 3-3-4.jpg 4-10-2.jpg 4-3-6.jpg 4-7-1.jpg
|
81
|
+
1-1-0.jpg 3-4-0.jpg 4-10-3.jpg 4-3-7.jpg 4-7-2.jpg
|
82
|
+
1-1-1.jpg 3-4-1.jpg 4-10-4.jpg 4-3-8.jpg 4-7-3.jpg
|
83
|
+
2-0-0.jpg 3-4-2.jpg 4-10-5.jpg 4-4-0.jpg 4-7-4.jpg
|
84
|
+
2-0-1.jpg 3-4-3.jpg 4-10-6.jpg 4-4-1.jpg 4-7-5.jpg
|
85
|
+
2-0-2.jpg 3-4-4.jpg 4-10-7.jpg 4-4-2.jpg 4-7-6.jpg
|
86
|
+
2-1-0.jpg 3-5-0.jpg 4-10-8.jpg 4-4-3.jpg 4-7-7.jpg
|
87
|
+
2-1-1.jpg 3-5-1.jpg 4-11-0.jpg 4-4-4.jpg 4-7-8.jpg
|
88
|
+
2-1-2.jpg 3-5-2.jpg 4-11-1.jpg 4-4-5.jpg 4-8-0.jpg
|
89
|
+
2-2-0.jpg 3-5-3.jpg 4-11-2.jpg 4-4-6.jpg 4-8-1.jpg
|
90
|
+
2-2-1.jpg 3-5-4.jpg 4-11-3.jpg 4-4-7.jpg 4-8-2.jpg
|
91
|
+
2-2-2.jpg 4-0-0.jpg 4-11-4.jpg 4-4-8.jpg 4-8-3.jpg
|
92
|
+
3-0-0.jpg 4-0-1.jpg 4-11-5.jpg 4-5-0.jpg 4-8-4.jpg
|
93
|
+
3-0-1.jpg 4-0-2.jpg 4-11-6.jpg 4-5-1.jpg 4-8-5.jpg
|
94
|
+
3-0-2.jpg 4-0-3.jpg 4-11-7.jpg 4-5-2.jpg 4-8-6.jpg
|
95
|
+
3-0-3.jpg 4-0-4.jpg 4-11-8.jpg 4-5-3.jpg 4-8-7.jpg
|
96
|
+
3-0-4.jpg 4-0-5.jpg 4-2-0.jpg 4-5-4.jpg 4-8-8.jpg
|
97
|
+
3-1-0.jpg 4-0-6.jpg 4-2-1.jpg 4-5-5.jpg 4-9-0.jpg
|
98
|
+
3-1-1.jpg 4-0-7.jpg 4-2-2.jpg 4-5-6.jpg 4-9-1.jpg
|
99
|
+
3-1-2.jpg 4-0-8.jpg 4-2-3.jpg 4-5-7.jpg 4-9-2.jpg
|
100
|
+
3-1-3.jpg 4-1-0.jpg 4-2-4.jpg 4-5-8.jpg 4-9-3.jpg
|
101
|
+
3-1-4.jpg 4-1-1.jpg 4-2-5.jpg 4-6-0.jpg 4-9-4.jpg
|
102
|
+
3-2-0.jpg 4-1-2.jpg 4-2-6.jpg 4-6-1.jpg 4-9-5.jpg
|
103
|
+
3-2-1.jpg 4-1-3.jpg 4-2-7.jpg 4-6-2.jpg 4-9-6.jpg
|
104
|
+
3-2-2.jpg 4-1-4.jpg 4-2-8.jpg 4-6-3.jpg 4-9-7.jpg
|
105
|
+
3-2-3.jpg 4-1-5.jpg 4-3-0.jpg 4-6-4.jpg 4-9-8.jpg
|
106
|
+
3-2-4.jpg 4-1-6.jpg 4-3-1.jpg 4-6-5.jpg
|
107
|
+
3-3-0.jpg 4-1-7.jpg 4-3-2.jpg 4-6-6.jpg
|
108
|
+
3-3-1.jpg 4-1-8.jpg 4-3-3.jpg 4-6-7.jpg].map do |file|
|
109
|
+
'TileGroup0/' + file
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it_should_behave_like "all images"
|
114
|
+
end
|
115
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zoomifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.3"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Donald Ball
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-21 00:00:00 -06:00
|
13
|
+
default_executable: zoomify
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rmagick
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: donald.ball@gmail.com
|
27
|
+
executables:
|
28
|
+
- zoomify
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.markdown
|
33
|
+
files:
|
34
|
+
- lib/zoomifier.rb
|
35
|
+
- bin/zoomify
|
36
|
+
- spec/zoomifier_spec.rb
|
37
|
+
- spec/spec_helper.rb
|
38
|
+
- spec/data/1024x768.jpg
|
39
|
+
- spec/data/2973x2159.jpg
|
40
|
+
- README.markdown
|
41
|
+
has_rdoc: true
|
42
|
+
homepage:
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.1
|
64
|
+
signing_key:
|
65
|
+
specification_version: 2
|
66
|
+
summary: A library for zoomifying images
|
67
|
+
test_files:
|
68
|
+
- spec/zoomifier_spec.rb
|