dball-zoomifier 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +4 -0
- data/bin/zoomify +4 -0
- data/lib/zoomifier.rb +182 -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 +114 -0
- metadata +67 -0
data/README.txt
ADDED
data/bin/zoomify
ADDED
data/lib/zoomifier.rb
ADDED
@@ -0,0 +1,182 @@
|
|
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
|
+
# @version 1.2
|
11
|
+
# @copyright (C) 2008 Donald A. Ball Jr.
|
12
|
+
#
|
13
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
14
|
+
# you may not use this file except in compliance with the License.
|
15
|
+
# You may obtain a copy of the License at
|
16
|
+
#
|
17
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
18
|
+
#
|
19
|
+
# Unless required by applicable law or agreed to in writing, software
|
20
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
21
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
22
|
+
# See the License for the specific language governing permissions and
|
23
|
+
# limitations under the License.
|
24
|
+
|
25
|
+
module Zoomifier
|
26
|
+
|
27
|
+
TILESIZE = 256
|
28
|
+
|
29
|
+
# Zoomifies the image file specified by filename. The zoomified directory
|
30
|
+
# name will be the filename without its extension, e.g. 5.jpg will be
|
31
|
+
# zoomified into a directory named 5. If there is already a directory with
|
32
|
+
# this name, it will be destroyed without mercy.
|
33
|
+
def self.zoomify(filename)
|
34
|
+
raise ArgumentError unless filename && File.file?(filename)
|
35
|
+
#filename = File.expand_path(filename)
|
36
|
+
outputdir = File.dirname(filename) + '/' + File.basename(filename, '.*')
|
37
|
+
raise ArgumentError unless filename != outputdir
|
38
|
+
FileUtils.rm_rf(outputdir) if File.exists?(outputdir)
|
39
|
+
Dir.mkdir(outputdir)
|
40
|
+
tmpdir = "#{outputdir}/tmp"
|
41
|
+
Dir.mkdir(tmpdir)
|
42
|
+
tilesdir = nil
|
43
|
+
image = Magick::Image.read(filename).first.strip!
|
44
|
+
# Each level of zoom is a factor of 2. Here we obtain the number of zooms
|
45
|
+
# allowed by the original file dimensions and the constant tile size.
|
46
|
+
levels = (Math.log([image.rows, image.columns].max.to_f / TILESIZE) / Math.log(2)).ceil
|
47
|
+
tiles = 0
|
48
|
+
(0..levels).each do |level|
|
49
|
+
n = levels - level
|
50
|
+
# Obtain the image to tile for this level. The 0th level should consist
|
51
|
+
# of one tile, while the highest level should be the original image.
|
52
|
+
level_image = image.resize(image.columns >> n, image.rows >> n)
|
53
|
+
tiles(tmpdir, level, level_image) do |filename|
|
54
|
+
# The tile images are chunked into directories named TileGroupN, N
|
55
|
+
# starting at 0 and increasing monotonically. Each directory contains
|
56
|
+
# at most 256 images. The images are sorted by level, tile row, and
|
57
|
+
# tile column.
|
58
|
+
div, mod = tiles.divmod(256)
|
59
|
+
if mod == 0
|
60
|
+
tilesdir = "#{outputdir}/TileGroup#{div}"
|
61
|
+
Dir.mkdir(tilesdir)
|
62
|
+
end
|
63
|
+
FileUtils.mv("#{tmpdir}/#{filename}", "#{tilesdir}/#{filename}")
|
64
|
+
tiles += 1
|
65
|
+
end
|
66
|
+
# Rmagick needs a bit of help freeing image memory.
|
67
|
+
level_image = nil
|
68
|
+
GC.start
|
69
|
+
end
|
70
|
+
File.open("#{outputdir}/ImageProperties.xml", 'w') do |f|
|
71
|
+
f.write("<IMAGE_PROPERTIES WIDTH=\"#{image.columns}\" HEIGHT=\"#{image.rows}\" NUMTILES=\"#{tiles}\" NUMIMAGES=\"1\" VERSION=\"1.8\" TILESIZE=\"#{TILESIZE}\" />")
|
72
|
+
end
|
73
|
+
Dir.rmdir(tmpdir)
|
74
|
+
outputdir
|
75
|
+
end
|
76
|
+
|
77
|
+
# Splits the given image up into images of TILESIZE, writes them to the
|
78
|
+
# given directory, and yields their names
|
79
|
+
def self.tiles(dir, level, image)
|
80
|
+
slice(image.rows).each_with_index do |y_slice, j|
|
81
|
+
slice(image.columns).each_with_index do |x_slice, i|
|
82
|
+
# The images are named "level-column-row.jpg"
|
83
|
+
filename = "#{level}-#{i}-#{j}.jpg"
|
84
|
+
tile_image = image.crop(x_slice[0], y_slice[0], x_slice[1], y_slice[1])
|
85
|
+
tile_image.write("#{dir}/#{filename}") do
|
86
|
+
# FIXME - the images end up being 4-5x larger than those produced
|
87
|
+
# by Zoomifier EZ and friends... no idea why just yet, except to note
|
88
|
+
# that the density of these tiles ends up being 400x400, while
|
89
|
+
# everybody else produces tiles at 72x72. Can't see why that would
|
90
|
+
# matter though...
|
91
|
+
self.quality = 80
|
92
|
+
end
|
93
|
+
# Rmagick needs a bit of help freeing image memory.
|
94
|
+
tile_image = nil
|
95
|
+
GC.start
|
96
|
+
yield filename
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns an array of slices ([offset, length]) obtained by slicing the
|
102
|
+
# given number by TILESIZE.
|
103
|
+
# E.g. 256 -> [[0, 256]], 257 -> [[0, 256], [256, 1]],
|
104
|
+
# 513 -> [[0, 256], [256, 256], [512, 1]]
|
105
|
+
def self.slice(n)
|
106
|
+
results = []
|
107
|
+
i = 0
|
108
|
+
while true
|
109
|
+
if i + TILESIZE >= n
|
110
|
+
results << [i, n-i]
|
111
|
+
break
|
112
|
+
else
|
113
|
+
results << [i, TILESIZE]
|
114
|
+
i += TILESIZE
|
115
|
+
end
|
116
|
+
end
|
117
|
+
results
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.unzoomify(url)
|
121
|
+
tmpdir = 'tmp'
|
122
|
+
FileUtils.rm_rf(tmpdir) if File.exists?(tmpdir)
|
123
|
+
Dir.mkdir(tmpdir)
|
124
|
+
doc = nil
|
125
|
+
begin
|
126
|
+
open("#{url}/ImageProperties.xml") do |f|
|
127
|
+
doc = REXML::Document.new(f)
|
128
|
+
end
|
129
|
+
rescue OpenURI::HTTPError
|
130
|
+
return nil
|
131
|
+
end
|
132
|
+
attrs = doc.root.attributes
|
133
|
+
return nil unless attrs['TILESIZE'] == '256' && attrs['VERSION'] == '1.8'
|
134
|
+
width = attrs['WIDTH'].to_i
|
135
|
+
height = attrs['HEIGHT'].to_i
|
136
|
+
tiles = attrs['NUMTILES'].to_i
|
137
|
+
image_paths = (0 .. tiles/256).map {|n| "TileGroup#{n}"}
|
138
|
+
max_level = 0
|
139
|
+
while (get_tile(url, image_paths, tmpdir, "#{max_level}-0-0.jpg"))
|
140
|
+
max_level += 1
|
141
|
+
end
|
142
|
+
max_level -= 1
|
143
|
+
image = Magick::Image.new(width, height)
|
144
|
+
(0 .. width / TILESIZE).each do |column|
|
145
|
+
(0 .. height / TILESIZE).each do |row|
|
146
|
+
filename = "#{max_level}-#{column}-#{row}.jpg"
|
147
|
+
get_tile(url, image_paths, tmpdir, filename)
|
148
|
+
tile_image = Magick::Image.read("#{tmpdir}/#{filename}").first
|
149
|
+
image.composite!(tile_image, column*TILESIZE, row*TILESIZE, Magick::OverCompositeOp)
|
150
|
+
time_image = nil
|
151
|
+
GC.start
|
152
|
+
end
|
153
|
+
end
|
154
|
+
# FIXME - get filename from the url
|
155
|
+
image.write('file.jpg') { self.quality = 90 }
|
156
|
+
image = nil
|
157
|
+
GC.start
|
158
|
+
FileUtils.rm_rf(tmpdir)
|
159
|
+
end
|
160
|
+
|
161
|
+
# TODO - could reduce the miss rate by using heuristics to guess the
|
162
|
+
# proper path from which to download the file
|
163
|
+
def self.get_tile(url, image_paths, tmpdir, filename)
|
164
|
+
image_paths.each do |path|
|
165
|
+
begin
|
166
|
+
open("#{tmpdir}/#{filename}", 'wb') {|f| f.write(open("#{url}/#{path}/#{filename}").read)}
|
167
|
+
return filename
|
168
|
+
rescue OpenURI::HTTPError
|
169
|
+
end
|
170
|
+
end
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
if __FILE__ == $0
|
177
|
+
if ARGV.length == 1
|
178
|
+
Zoomifier::zoomify(ARGV[0])
|
179
|
+
else
|
180
|
+
puts "Usage: zoomify filename"
|
181
|
+
end
|
182
|
+
end
|
Binary file
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
@@ -0,0 +1,114 @@
|
|
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 "On a 1024x768 JPEG file" do
|
10
|
+
before(:all) do
|
11
|
+
@input = File.dirname(__FILE__) + '/data/1024x768.jpg'
|
12
|
+
@output = File.dirname(__FILE__) + '/data/1024x768/'
|
13
|
+
@tiles = %w[0-0-0.jpg 1-1-1.jpg 2-1-0.jpg 2-2-1.jpg 2-3-2.jpg
|
14
|
+
1-0-0.jpg 2-0-0.jpg 2-1-1.jpg 2-2-2.jpg
|
15
|
+
1-0-1.jpg 2-0-1.jpg 2-1-2.jpg 2-3-0.jpg
|
16
|
+
1-1-0.jpg 2-0-2.jpg 2-2-0.jpg 2-3-1.jpg]
|
17
|
+
FileUtils.rm_rf(@output)
|
18
|
+
Zoomifier::zoomify(@input)
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
FileUtils.rm_rf(@output)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should create the output directory" do
|
26
|
+
File.directory?(@output).should be_true
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should create the image properties file" do
|
30
|
+
File.file?(@output + '/ImageProperties.xml').should be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should create a tile group directory" do
|
34
|
+
File.directory?(@output + '/TileGroup0/').should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should create the tiled images" do
|
38
|
+
tile_images = Dir.entries(@output + '/TileGroup0/').reject {|f| f.match(/^\./)}
|
39
|
+
tile_images.sort.should == @tiles.sort
|
40
|
+
tile_images.each do |file|
|
41
|
+
image = Magick::Image.read(@output + '/TileGroup0/' + file).first
|
42
|
+
image.rows.should <= 256
|
43
|
+
image.columns.should <= 256
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "On a 2973x2159 JPEG file" do
|
49
|
+
before(:all) do
|
50
|
+
@input = File.dirname(__FILE__) + '/data/2973x2159.jpg'
|
51
|
+
@output = File.dirname(__FILE__) + '/data/2973x2159/'
|
52
|
+
@tiles = %w[
|
53
|
+
0-0-0.jpg 3-3-2.jpg 4-10-0.jpg 4-3-4.jpg 4-6-8.jpg
|
54
|
+
1-0-0.jpg 3-3-3.jpg 4-10-1.jpg 4-3-5.jpg 4-7-0.jpg
|
55
|
+
1-0-1.jpg 3-3-4.jpg 4-10-2.jpg 4-3-6.jpg 4-7-1.jpg
|
56
|
+
1-1-0.jpg 3-4-0.jpg 4-10-3.jpg 4-3-7.jpg 4-7-2.jpg
|
57
|
+
1-1-1.jpg 3-4-1.jpg 4-10-4.jpg 4-3-8.jpg 4-7-3.jpg
|
58
|
+
2-0-0.jpg 3-4-2.jpg 4-10-5.jpg 4-4-0.jpg 4-7-4.jpg
|
59
|
+
2-0-1.jpg 3-4-3.jpg 4-10-6.jpg 4-4-1.jpg 4-7-5.jpg
|
60
|
+
2-0-2.jpg 3-4-4.jpg 4-10-7.jpg 4-4-2.jpg 4-7-6.jpg
|
61
|
+
2-1-0.jpg 3-5-0.jpg 4-10-8.jpg 4-4-3.jpg 4-7-7.jpg
|
62
|
+
2-1-1.jpg 3-5-1.jpg 4-11-0.jpg 4-4-4.jpg 4-7-8.jpg
|
63
|
+
2-1-2.jpg 3-5-2.jpg 4-11-1.jpg 4-4-5.jpg 4-8-0.jpg
|
64
|
+
2-2-0.jpg 3-5-3.jpg 4-11-2.jpg 4-4-6.jpg 4-8-1.jpg
|
65
|
+
2-2-1.jpg 3-5-4.jpg 4-11-3.jpg 4-4-7.jpg 4-8-2.jpg
|
66
|
+
2-2-2.jpg 4-0-0.jpg 4-11-4.jpg 4-4-8.jpg 4-8-3.jpg
|
67
|
+
3-0-0.jpg 4-0-1.jpg 4-11-5.jpg 4-5-0.jpg 4-8-4.jpg
|
68
|
+
3-0-1.jpg 4-0-2.jpg 4-11-6.jpg 4-5-1.jpg 4-8-5.jpg
|
69
|
+
3-0-2.jpg 4-0-3.jpg 4-11-7.jpg 4-5-2.jpg 4-8-6.jpg
|
70
|
+
3-0-3.jpg 4-0-4.jpg 4-11-8.jpg 4-5-3.jpg 4-8-7.jpg
|
71
|
+
3-0-4.jpg 4-0-5.jpg 4-2-0.jpg 4-5-4.jpg 4-8-8.jpg
|
72
|
+
3-1-0.jpg 4-0-6.jpg 4-2-1.jpg 4-5-5.jpg 4-9-0.jpg
|
73
|
+
3-1-1.jpg 4-0-7.jpg 4-2-2.jpg 4-5-6.jpg 4-9-1.jpg
|
74
|
+
3-1-2.jpg 4-0-8.jpg 4-2-3.jpg 4-5-7.jpg 4-9-2.jpg
|
75
|
+
3-1-3.jpg 4-1-0.jpg 4-2-4.jpg 4-5-8.jpg 4-9-3.jpg
|
76
|
+
3-1-4.jpg 4-1-1.jpg 4-2-5.jpg 4-6-0.jpg 4-9-4.jpg
|
77
|
+
3-2-0.jpg 4-1-2.jpg 4-2-6.jpg 4-6-1.jpg 4-9-5.jpg
|
78
|
+
3-2-1.jpg 4-1-3.jpg 4-2-7.jpg 4-6-2.jpg 4-9-6.jpg
|
79
|
+
3-2-2.jpg 4-1-4.jpg 4-2-8.jpg 4-6-3.jpg 4-9-7.jpg
|
80
|
+
3-2-3.jpg 4-1-5.jpg 4-3-0.jpg 4-6-4.jpg 4-9-8.jpg
|
81
|
+
3-2-4.jpg 4-1-6.jpg 4-3-1.jpg 4-6-5.jpg
|
82
|
+
3-3-0.jpg 4-1-7.jpg 4-3-2.jpg 4-6-6.jpg
|
83
|
+
3-3-1.jpg 4-1-8.jpg 4-3-3.jpg 4-6-7.jpg]
|
84
|
+
FileUtils.rm_rf(@output)
|
85
|
+
Zoomifier::zoomify(@input)
|
86
|
+
end
|
87
|
+
|
88
|
+
after(:all) do
|
89
|
+
FileUtils.rm_rf(@output)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should create the output directory" do
|
93
|
+
File.directory?(@output).should be_true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should create the image properties file" do
|
97
|
+
File.file?(@output + '/ImageProperties.xml').should be_true
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should create a tile group directory" do
|
101
|
+
File.directory?(@output + '/TileGroup0/').should be_true
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should create the tiled images" do
|
105
|
+
tile_images = Dir.entries(@output + '/TileGroup0/').reject {|f| f.match(/^\./)}
|
106
|
+
tile_images.sort.should == @tiles.sort
|
107
|
+
tile_images.each do |file|
|
108
|
+
image = Magick::Image.read(@output + '/TileGroup0/' + file).first
|
109
|
+
image.rows.should <= 256
|
110
|
+
image.columns.should <= 256
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dball-zoomifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.2"
|
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 -08:00
|
13
|
+
default_executable: zoomify
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rmagick
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
description:
|
25
|
+
email: donald.ball@gmail.com
|
26
|
+
executables:
|
27
|
+
- zoomify
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- README.txt
|
32
|
+
files:
|
33
|
+
- lib/zoomifier.rb
|
34
|
+
- bin/zoomify
|
35
|
+
- spec/zoomifier_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
- spec/data/1024x768.jpg
|
38
|
+
- spec/data/2973x2159.jpg
|
39
|
+
- README.txt
|
40
|
+
has_rdoc: true
|
41
|
+
homepage:
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.2.0
|
63
|
+
signing_key:
|
64
|
+
specification_version: 2
|
65
|
+
summary: A library for zoomifying images
|
66
|
+
test_files:
|
67
|
+
- spec/zoomifier_spec.rb
|