/scripts/tilemaker.py

http://panojs.googlecode.com/ · Python · 222 lines · 133 code · 46 blank · 43 comment · 21 complexity · ff2a2c9d892b43d76fda25720dbe2585 MD5 · raw file

  1. #!/usr/bin/env python
  2. """
  3. This program assists with cutting down large images into square tiles. It can
  4. take an image of arbitrary size and create tiles of any size.
  5. python tilemaker.py -s256 -Q9 -t"tile-%d-%d-%d.png" -bFFFFFF -v canvas.png
  6. Copyright, 2005-2006: Michal Migurski, Serge Wroclawski
  7. License: Apache 2.0
  8. """
  9. import math
  10. from os.path import split, splitext
  11. from PIL import Image
  12. chatty_default = False
  13. background_default = "FFFFFF"
  14. efficient_default = True
  15. scaling_filter = Image.BICUBIC
  16. from sys import exit
  17. def main():
  18. """Main method"""
  19. from optparse import OptionParser
  20. parser = OptionParser(usage = "usage: %prog [options] filename")
  21. # Now, Dan wants tile height and width.
  22. parser.add_option('-s', '--tile-size', dest = "size", type="int",
  23. default=512, help = 'The tile height/width')
  24. parser.add_option('-t', '--template', dest = "template",
  25. default = None,
  26. help = "Template filename pattern")
  27. parser.add_option('-v', '--verbose', dest = "verbosity",
  28. action = "store_true", default = False,
  29. help = "Increase verbosity")
  30. parser.add_option('-Q', '--quality', dest="quality", type="int",
  31. help = 'Set the quality level of the image')
  32. parser.add_option('-b', '--background', dest="background",
  33. help = 'Set the background color')
  34. # Location based arguments are always a pain
  35. (options, args) = parser.parse_args()
  36. if len(args) != 1:
  37. parser.error("incorrect number of arguments")
  38. filename = args[0]
  39. if not options.template:
  40. fname, extension = splitext(split(filename)[1])
  41. options.template = fname + '-%d-%d-%d' + extension
  42. if not options.background:
  43. options.background = background_default
  44. verbosity = options.verbosity
  45. size = options.size
  46. quality = options.quality
  47. template = options.template
  48. background = options.background
  49. # Split the image up into "squares"
  50. img = prepare(filename, bgcolor = background, chatty = verbosity)
  51. subdivide(img, size = (size, size),
  52. quality = quality, filename = template, chatty = verbosity)
  53. def prepare(filename, bgcolor = background_default, chatty = chatty_default):
  54. """
  55. Prepare a large image for tiling.
  56. Load an image from a file. Resize the image so that it is square,
  57. with dimensions that are an even power of two in length (e.g. 512,
  58. 1024, 2048, ...). Then, return it.
  59. """
  60. src = Image.open(filename)
  61. if chatty:
  62. print "original size: %s" % str(src.size)
  63. full_size = (1, 1)
  64. while full_size[0] < src.size[0] or full_size[1] < src.size[1]:
  65. full_size = (full_size[0] * 2, full_size[1] * 2)
  66. img = Image.new('RGBA', full_size)
  67. img.paste("#" + bgcolor)
  68. src.thumbnail(full_size, scaling_filter)
  69. img.paste(src, (int((full_size[0] - src.size[0]) / 2),
  70. int((full_size[1] - src.size[1]) / 2)))
  71. if chatty:
  72. print "full size: %s" % str(full_size)
  73. return img
  74. def tile(im, level, quadrant=(0, 0), size=(512, 512),
  75. efficient=efficient_default, chatty=chatty_default):
  76. """
  77. Extract a single tile from a larger image.
  78. Given an image, a zoom level (int), a quadrant (column, row tuple;
  79. ints), and an output size, crop and size a portion of the larger
  80. image. If the given zoom level would result in scaling the image up,
  81. throw an error - no need to create information where none exists.
  82. """
  83. scale = int(math.pow(2, level))
  84. if efficient:
  85. #efficient: crop out the area of interest first, then scale and copy it
  86. inverse_size = (float(im.size[0]) / float(size[0] * scale),
  87. float(im.size[1]) / float(size[1] * scale))
  88. top_left = (int(quadrant[0] * size[0] * inverse_size[0]),
  89. int(quadrant[1] * size[1] * inverse_size[1]))
  90. bottom_right = (int(top_left[0] + (size[0] * inverse_size[0])),
  91. int(top_left[1] + (size[1] * inverse_size[1])))
  92. if inverse_size[0] < 1.0 or inverse_size[1] < 1.0:
  93. raise Exception('Requested zoom level (%d) is too high' % level)
  94. if chatty:
  95. print "crop(%s).resize(%s)" % (str(top_left + bottom_right),
  96. str(size))
  97. zoomed = im.crop(top_left + bottom_right).resize(size, scaling_filter).copy()
  98. return zoomed
  99. else:
  100. # inefficient: copy the whole image, scale it and then crop
  101. # out the area of interest
  102. new_size = (size[0] * scale, size[1] * scale)
  103. top_left = (quadrant[0] * size[0], quadrant[1] * size[1])
  104. bottom_right = (top_left[0] + size[0], top_left[1] + size[1])
  105. if new_size[0] > im.size[0] or new_size[1] > im.size[1]:
  106. raise Exception('Requested zoom level (%d) is too high' % level)
  107. if chatty:
  108. print "resize(%s).crop(%s)" % (str(new_size),
  109. str(top_left + bottom_right))
  110. zoomed = im.copy().resize(new_size, scaling_filter).crop(top_left + bottom_right).copy()
  111. return zoomed
  112. def subdivide(img, level=0, quadrant=(0, 0), size=(512, 512),
  113. filename='tile-%d-%d-%d.jpg',
  114. quality = None, chatty = chatty_default):
  115. """
  116. Recursively subdivide a large image into small tiles.
  117. Given an image, a zoom level (int), a quadrant (column, row tuple;
  118. ints), and an output size, cut the image into even quarters and
  119. recursively subdivide each, then generate a combined tile from the
  120. resulting subdivisions. If further subdivision would result in
  121. scaling the image up, use tile() to turn the image itself into a
  122. tile.
  123. """
  124. if img.size[0] <= size[0] * math.pow(2, level):
  125. # looks like we've reached the bottom - the image can't be
  126. # subdivided further. # extract a tile from the passed image.
  127. out_img = tile(img, level, quadrant=quadrant, size=size)
  128. out_img.save(filename % (level, quadrant[0], quadrant[1]))
  129. if chatty:
  130. print '.', ' ' * level, filename % (level, quadrant[0], quadrant[1])
  131. return out_img
  132. # haven't reach the bottom.
  133. # subdivide deeper, construct the current image out of deeper images.
  134. out_img = Image.new('RGBA', (size[0] * 2, size[1] * 2))
  135. out_img.paste(subdivide(img = img,
  136. level = (level + 1),
  137. quadrant=((quadrant[0] * 2) + 0,
  138. (quadrant[1] * 2) + 0),
  139. size = size,
  140. filename=filename, chatty=chatty), (0,0))
  141. out_img.paste(subdivide(img = img,
  142. level=(level + 1),
  143. quadrant=((quadrant[0] * 2) + 0,
  144. (quadrant[1] * 2) + 1),
  145. size = size,
  146. filename=filename, chatty=chatty), (0,size[1]))
  147. out_img.paste(subdivide(img = img,
  148. level=(level + 1),
  149. quadrant=((quadrant[0] * 2) + 1,
  150. (quadrant[1] * 2) + 0),
  151. size = size,
  152. filename=filename, chatty=chatty), (size[0], 0))
  153. out_img.paste(subdivide(img,
  154. level=(level + 1),
  155. quadrant=((quadrant[0] * 2) + 1,
  156. (quadrant[1] * 2) + 1),
  157. size = size,
  158. filename=filename, chatty=chatty), (size[0], size[1]))
  159. out_img = out_img.resize(size, scaling_filter)
  160. # In the future, we may want to verify the quality. Right now we let
  161. # the underlying code handle bad values (other than a non-int)
  162. if not quality:
  163. out_img.save(filename % (level, quadrant[0], quadrant[1]))
  164. else:
  165. out_img.save(filename % (level, quadrant[0], quadrant[1]),
  166. quality=quality)
  167. if chatty:
  168. print '-', ' ' * level, filename % (level, quadrant[0], quadrant[1])
  169. return out_img
  170. if __name__ == '__main__':
  171. exit(main())