summary refs log tree commit diff
path: root/doc/tilemaker.py
blob: a02c7d269b7be96ef60edd6fb527c60291ed30b7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#!/usr/bin/env python

"""
This program assists with cutting down large images into square tiles.  It can
take an image of arbitrary size and create tiles of any size.

python tilemaker.py -s256 -Q9 -t"tile-%d-%d-%d.png" -bFFFFFF -v canvas.png

Copyright, 2005-2006: Michal Migurski, Serge Wroclawski
License: Apache 2.0
"""

import math
from os.path import split, splitext
from PIL import Image

chatty_default = False
background_default = "FFFFFF"
efficient_default = True
scaling_filter = Image.BICUBIC

from sys import exit

def main():
    """Main method"""
    from optparse import OptionParser
    
    parser = OptionParser(usage = "usage: %prog [options] filename")
    # Now, Dan wants tile height and width.
    parser.add_option('-s', '--tile-size', dest = "size", type="int",
                      default=512, help = 'The tile height/width')
    parser.add_option('-t', '--template', dest = "template",
                      default = None,
                      help = "Template filename pattern")
    parser.add_option('-v', '--verbose', dest = "verbosity",
                      action = "store_true", default = False,
                      help = "Increase verbosity")
    parser.add_option('-Q', '--quality', dest="quality", type="int",
                      help = 'Set the quality level of the image')
    parser.add_option('-b', '--background', dest="background",
                      help = 'Set the background color')
    
    # Location based arguments are always a pain
    (options, args) = parser.parse_args()
    if len(args) != 1:
        parser.error("incorrect number of arguments")
    filename = args[0]
    if not options.template:
        fname, extension = splitext(split(filename)[1])
        options.template = fname + '-%d-%d-%d' + extension
    if not options.background:
        options.background = background_default

    verbosity = options.verbosity
    size = options.size
    quality = options.quality
    template = options.template
    background = options.background
    
    # Split the image up into "squares"
    img = prepare(filename, bgcolor = background, chatty = verbosity)

    subdivide(img, size = (size, size),
              quality = quality, filename = template, chatty = verbosity)


def prepare(filename, bgcolor = background_default, chatty = chatty_default):
    """
    Prepare a large image for tiling.
    
    Load an image from a file. Resize the image so that it is square,
    with dimensions that are an even power of two in length (e.g. 512,
    1024, 2048, ...). Then, return it.
    """

    src = Image.open(filename)

    if chatty:
        print "original size: %s" % str(src.size)
    
    full_size = (1, 1)

    while full_size[0] < src.size[0] or full_size[1] < src.size[1]:
        full_size = (full_size[0] * 2, full_size[1] * 2)
    
    img = Image.new('RGBA', full_size)
    img.paste("#" + bgcolor)
    
    src.thumbnail(full_size, scaling_filter)
    img.paste(src, (int((full_size[0] - src.size[0]) / 2),
                    int((full_size[1] - src.size[1]) / 2)))
    
    if chatty:
        print "full size: %s" % str(full_size)
        
    return img



def tile(im, level, quadrant=(0, 0), size=(512, 512),
         efficient=efficient_default, chatty=chatty_default):
    """
    Extract a single tile from a larger image.
    
    Given an image, a zoom level (int), a quadrant (column, row tuple;
    ints), and an output size, crop and size a portion of the larger
    image. If the given zoom level would result in scaling the image up,
    throw an error - no need to create information where none exists.
    """

    scale = int(math.pow(2, level))
    
    if efficient:
        #efficient: crop out the area of interest first, then scale and copy it

        inverse_size    = (float(im.size[0]) / float(size[0] * scale),
                           float(im.size[1]) / float(size[1] * scale))
        top_left        = (int(quadrant[0] *  size[0] * inverse_size[0]),
                           int(quadrant[1] *  size[1] * inverse_size[1]))
        bottom_right    = (int(top_left[0] + (size[0] * inverse_size[0])),
                           int(top_left[1] + (size[1] * inverse_size[1])))
    
        if inverse_size[0] < 1.0 or inverse_size[1] < 1.0:
            raise Exception('Requested zoom level (%d) is too high' % level)
    
        if chatty:
            print "crop(%s).resize(%s)" % (str(top_left + bottom_right),
                                           str(size))

        zoomed = im.crop(top_left + bottom_right).resize(size, scaling_filter).copy()
        return zoomed

    else:
        # inefficient: copy the whole image, scale it and then crop
        # out the area of interest

        new_size        = (size[0] * scale,         size[1] * scale)
        top_left        = (quadrant[0] * size[0],   quadrant[1] * size[1])
        bottom_right    = (top_left[0] + size[0],   top_left[1] + size[1])
        
        if new_size[0] > im.size[0] or new_size[1] > im.size[1]:
            raise Exception('Requested zoom level (%d) is too high' % level)
    
        if chatty:
            print "resize(%s).crop(%s)" % (str(new_size),
                                           str(top_left + bottom_right))

        zoomed = im.copy().resize(new_size, scaling_filter).crop(top_left + bottom_right).copy()
        return zoomed



def subdivide(img, level=0, quadrant=(0, 0), size=(512, 512),
              filename='tile-%d-%d-%d.jpg',
              quality = None, chatty = chatty_default):
    """
    Recursively subdivide a large image into small tiles.

    Given an image, a zoom level (int), a quadrant (column, row tuple;
    ints), and an output size, cut the image into even quarters and
    recursively subdivide each, then generate a combined tile from the
    resulting subdivisions. If further subdivision would result in
    scaling the image up, use tile() to turn the image itself into a
    tile.
    """

    if img.size[0] <= size[0] * math.pow(2, level):

        # looks like we've reached the bottom - the image can't be
        # subdivided further. # extract a tile from the passed image.
        out_img = tile(img, level, quadrant=quadrant, size=size)
        out_img.save(filename % (level, quadrant[0], quadrant[1]))

        if chatty:
            print '.', '  ' * level, filename % (level, quadrant[0], quadrant[1])
        return out_img

    # haven't reach the bottom.
    # subdivide deeper, construct the current image out of deeper images.
    out_img = Image.new('RGBA', (size[0] * 2, size[1] * 2))
    out_img.paste(subdivide(img = img,
                            level = (level + 1),
                            quadrant=((quadrant[0] * 2) + 0,
                                      (quadrant[1] * 2) + 0),
                            size = size,
                            filename=filename, chatty=chatty), (0,0))
    out_img.paste(subdivide(img = img,
                            level=(level + 1),
                            quadrant=((quadrant[0] * 2) + 0,
                                      (quadrant[1] * 2) + 1),
                            size = size,
                            filename=filename, chatty=chatty), (0,size[1]))
    out_img.paste(subdivide(img = img,
                            level=(level + 1),
                            quadrant=((quadrant[0] * 2) + 1,
                                      (quadrant[1] * 2) + 0),
                            size = size,
                            filename=filename, chatty=chatty), (size[0], 0))
    out_img.paste(subdivide(img,
                            level=(level + 1),
                            quadrant=((quadrant[0] * 2) + 1,
                                      (quadrant[1] * 2) + 1),
                            size = size,
                            filename=filename, chatty=chatty), (size[0], size[1]))

    out_img = out_img.resize(size, scaling_filter)

    # In the future, we may want to verify the quality. Right now we let
    # the underlying code handle bad values (other than a non-int)
    if not quality:
        out_img.save(filename % (level, quadrant[0], quadrant[1]))
    else:
        out_img.save(filename % (level, quadrant[0], quadrant[1]),
                     quality=quality)
    if chatty:
        print '-', '  ' * level, filename % (level, quadrant[0], quadrant[1])
    return out_img



if __name__ == '__main__':
    exit(main())