#!/usr/bin/python ## Copyright (c) 2008, Kapil Thangavelu ## All rights reserved. ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions are ## met: ## Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the ## distribution. The names of its authors/contributors may be used to ## endorse or promote products derived from this software without ## specific prior written permission. ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS ## FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE ## COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR ## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ## HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, ## STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED ## OF THE POSSIBILITY OF SUCH DAMAGE. """ Implements a Deep Zoom / Seadragon Composer in Python For use with the seajax viewer reversed from the excellent blog description at http://gashi.ch/blog/inside-deep-zoom-2 from Daniel Gasienica incidentally he's got an updated version of this script that supports collections thats included in the openzoom project http://code.google.com/p/open-zoom/source/browse/trunk/src/main/python/deepzoom/deepzoom.py Author: Kapil Thangavelu Date: 11/29/2008 License: BSD """ import math, os, optparse, sys from PIL import Image xml_template = '''\ ''' filter_map = { 'cubic' : Image.CUBIC, 'bilinear' : Image.BILINEAR, 'bicubic' : Image.BICUBIC, 'nearest' : Image.NEAREST, 'antialias' : Image.ANTIALIAS, } class PyramidComposer( object ): def __init__( self, image, tile_size=256.0, overlap=1, format="png", filter=None): self.image = image self.tile_size = tile_size self.overlap = overlap self.format = format self.width, self.height = self.image.size self._levels = None self.filter = filter @property def levels( self ): """ number of levels in an image pyramid """ if self._levels is not None: return self._levels self._levels = int( math.ceil( math.log( max( (self.width, self.height) ), 2) ) ) return self._levels def getLevelDimensions( self, level ): assert level <= self.levels and level >= 0, "Invalid Pyramid Level" scale = self.getLevelScale( level ) return math.ceil( self.width * scale) , math.ceil( self.height * scale ) def getLevelScale( self, level ): #print math.pow( 0.5, self.levels - level ) return 1.0 / (1 << ( self.levels - level ) ) def getLevelRowCol( self, level ): w, h = self.getLevelDimensions( level ) return ( math.ceil( w / self.tile_size ), math.ceil( h / self.tile_size ) ) def getTileBox( self, level, column, row ): """ return a bounding box (x1,y1,x2,y2)""" # find start position for current tile # python's ternary operator doesn't like zero as true condition result # ie. True and 0 or 1 -> returns 1 if not column: px = 0 else: px = self.tile_size * column - self.overlap if not row: py = 0 else: py = self.tile_size * row - self.overlap # scaled dimensions for this level dsw, dsh = self.getLevelDimensions( level ) # find the dimension of the tile, adjust for no overlap data on top and left edges sx = self.tile_size + ( column == 0 and 1 or 2 ) * self.overlap sy = self.tile_size + ( row == 0 and 1 or 2 ) * self.overlap # adjust size for single-tile levels where the image size is smaller # than the regular tile size, and for tiles on the bottom and right # edges that would exceed the image bounds sx = min( sx, dsw-px ) sy = min( sy, dsh-py ) return px, py, px+sx, py+sy def getLevelImage( self, level ): w, h = self.getLevelDimensions( level ) w, h = int(w), int(h) # don't transform to what we already have if self.width == w and self.height == h: return self.image if not self.filter: return self.image.resize( (w,h) ) return self.image.resize( (w,h), self.filter) def iterTiles( self, level ): col, row = self.getLevelRowCol( level ) for w in range( 0, int( col ) ): for h in range( 0, int( row ) ): yield (w,h), ( self.getTileBox( level, w, h ) ) def __len__( self ): return self.levels def save( self, parent_directory, name ): dir_path = ensure( os.path.join( ensure( expand( parent_directory ) ), "%s_files"%name ) ) # store images for n in range( self.levels + 1 ): level_dir = ensure( os.path.join( dir_path, str( n ) ) ) level_image = self.getLevelImage( n ) for ( col, row), box in self.iterTiles( n ): tile = level_image.crop( map(int, box) ) tile_path = os.path.join( level_dir, "%s_%s.%s"%( col, row, self.format ) ) tile_file = open( tile_path, 'wb+') tile.save( tile_file ) # store dzi file fh = open( os.path.join( parent_directory, "%s.dzi"%(name)), 'w+' ) fh.write( xml_template%( self.__dict__ ) ) fh.close() def info( self ): for n in range( self.levels +1 ): print "Level", n, self.getLevelDimensions( n ), self.getLevelScale( n ), self.getLevelRowCol( n ) for (col, row ), box in self.iterTiles( n ): if n > self.levels*.75 and n < self.levels*.95: print " ", "%s/%s_%s"%(n, col, row ), box def expand( d): return os.path.abspath( os.path.expanduser( os.path.expandvars( d ) ) ) def ensure( d ): if not os.path.exists( d ): os.mkdir( d ) return d def main( ): parser = optparse.OptionParser(usage = "usage: %prog [options] filename") parser.add_option('-s', '--tile-size', dest = "size", type="int", default=256, help = 'The tile height/width') parser.add_option('-q', '--quality', dest="quality", type="int", help = 'Set the quality level of the image') parser.add_option('-f', '--format', dest="format", default="jpg", help = 'Set the Image Format (jpg or png)') parser.add_option('-n', '--name', dest="name", help = 'Set the name of the output directory/dzi') parser.add_option('-p', '--path', dest="path", help = 'Set the path of the output directory/dzi') parser.add_option('-t', '--transform', dest="transform", default="antialias", help = 'Type of Transform (bicubic, nearest, antialias, bilinear') parser.add_option('-d', '--debug', dest="debug", action="store_true", default=False, help = 'Output debug information relating to box makeup') (options, args ) = parser.parse_args() if not args: parser.print_help() sys.exit(1) image_path = expand( args[0] ) if not os.path.exists( image_path ): print "Invalid File", image_path sys.exit(1) if not options.name: options.name = os.path.splitext( os.path.basename( image_path ) )[0] if not options.path: options.path = os.path.dirname( image_path ) if options.transform and options.transform in filter_map: options.transform = filter_map[ options.transform ] img = Image.open( image_path ) composer = PyramidComposer( img, tile_size=options.size, format=options.format, filter=options.transform ) if options.debug: composer.info() sys.exit() composer.save( options.path, options.name ) if __name__ == '__main__': main()