dball-zoomifier 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ Zoomifier is a ruby library for creating directories of tiled images
2
+ suitable for viewing with the free Zoomify flash player:
3
+
4
+ http://www.zoomify.com/
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
3
+ require 'zoomifier'
4
+ Zoomifier::zoomify(ARGV[0])
@@ -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
@@ -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