Module: Zoomifier

Defined in:
lib/zoomifier.rb

Overview

Breaks up images into tiles suitable for viewing with Zoomify. See zoomify.com/ for more details.

Constant Summary collapse

TILESIZE =
256

Class Method Summary collapse

Class Method Details

.get_tile(url, image_paths, tmpdir, filename) ⇒ Object

TODO - could reduce the miss rate by using heuristics to guess the proper path from which to download the file



165
166
167
168
169
170
171
172
173
174
# File 'lib/zoomifier.rb', line 165

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

.slice(n) ⇒ Object

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]]


107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/zoomifier.rb', line 107

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

.tiles(dir, level, image) ⇒ Object

Splits the given image up into images of TILESIZE, writes them to the given directory, and yields their names



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/zoomifier.rb', line 81

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

.unzoomify(url) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/zoomifier.rb', line 122

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

.zoomify(filename) ⇒ Object

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.

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/zoomifier.rb', line 32

def self.zoomify(filename)
  raise ArgumentError unless filename && File.file?(filename)
  #filename = File.expand_path(filename)

  outputdir = File.dirname(filename) + '/' + File.basename(filename, '.*')
  raise ArgumentError unless filename != outputdir
  if File.directory?(outputdir) && File.file?(outputdir + '/ImageProperties.xml') && File.mtime(filename) <= File.mtime(outputdir + '/ImageProperties.xml')
    return
  end
  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