dball-zoomifier 1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|