eggification
[Photo.git] / Products / Photo / TileSupport.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Photo is a part of Plinn - http://plinn.org #
4 # Copyright (C) 2004-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Tile support module
21
22
23
24 """
25
26 from AccessControl import ClassSecurityInfo
27 from AccessControl import Unauthorized
28 from AccessControl import getSecurityManager
29 from AccessControl.Permissions import view, change_images_and_files
30 from PIL import Image as PILImage
31 from math import ceil
32 from blobbases import Image
33 from xmputils import TIFF_ORIENTATIONS
34 from cache import memoizedmethod
35 from BTrees.OOBTree import OOBTree
36 from BTrees.IOBTree import IOBTree
37 from ppm import PPMFile
38 from threading import Lock
39 from subprocess import Popen, PIPE
40 from tempfile import TemporaryFile
41
42 JPEG_ROTATE = 'jpegtran -rotate %d'
43 JPEG_FLIP = 'jpegtran -flip horizontal'
44
45 def runOnce(lock):
46 """ Decorator. exit if already running """
47
48 def wrapper(f):
49 def method(*args, **kw):
50 if not lock.locked() :
51 lock.acquire()
52 try:
53 return f(*args, **kw)
54 finally:
55 lock.release()
56 else :
57 return False
58 return method
59 return wrapper
60
61
62
63 class TileSupport :
64 """ Mixin class to generate tiles from image """
65
66 security = ClassSecurityInfo()
67 tileSize = 256
68 tileGenerationLock = Lock()
69
70 def __init__(self) :
71 self._tiles = OOBTree()
72
73 security.declarePrivate('makeTilesAt')
74 @runOnce(tileGenerationLock)
75 def makeTilesAt(self, zoom):
76 """generates tiles at zoom level"""
77
78 if self._tiles.has_key(zoom) :
79 return True
80
81 assert zoom <= 1, "zoom arg must be <= 1 found: %s" % zoom
82
83 ppm = self._getPPM()
84 if zoom < 1 :
85 ppm = ppm.resize(ratio=zoom)
86
87 self._makeTilesAt(zoom, ppm)
88 return True
89
90 def _getPPM(self) :
91 bf = self._getJpegBlob()
92 f = bf.open('r')
93
94 orientation = self.tiffOrientation()
95 rotation, flip = TIFF_ORIENTATIONS.get(orientation, (0, False))
96
97 if rotation and flip :
98 tf = TemporaryFile(mode='w+')
99 pRot = Popen(JPEG_ROTATE % rotation
100 , stdin=f
101 , stdout=PIPE
102 , shell=True)
103 pFlip = Popen(JPEG_FLIP
104 , stdin=pRot.stdout
105 , stdout=tf
106 , shell=True)
107 pFlip.wait()
108 f.close()
109 tf.seek(0)
110 f = tf
111
112 elif rotation :
113 tf = TemporaryFile(mode='w+')
114 pRot = Popen(JPEG_ROTATE % rotation
115 , stdin=f
116 , stdout=tf
117 , shell=True)
118 pRot.wait()
119 f.close()
120 tf.seek(0)
121 f = tf
122
123 elif flip :
124 tf = TemporaryFile(mode='w+')
125 pFlip = Popen(JPEG_FLIP
126 , stdin=f
127 , stdout=tf
128 , shell=True)
129 pFlip.wait()
130 f.close()
131 tf.seek(0)
132 f = tf
133
134 ppm = PPMFile(f, tileSize=self.tileSize)
135 f.close()
136 return ppm
137
138 def _makeTilesAt(self, zoom, ppm):
139 hooks = self._getAfterTilingHooks()
140 self._tiles[zoom] = IOBTree()
141 bgColor = getattr(self, 'tiles_background_color', '#fff')
142
143 for x in xrange(ppm.tilesX) :
144 self._tiles[zoom][x] = IOBTree()
145 for y in xrange(ppm.tilesY) :
146 tile = ppm.getTile(x, y)
147 for hook in hooks :
148 hook(self, tile)
149
150 # fill with solid color
151 if min(tile.size) < self.tileSize :
152 blankTile = PILImage.new('RGB', (self.tileSize, self.tileSize), bgColor)
153 box = (0,0) + tile.size
154 blankTile.paste(tile, box)
155 tile = blankTile
156
157 zImg = Image('tile', 'tile', '', content_type='image/jpeg')
158 out = zImg.open('w')
159 tile.save(out, 'JPEG', quality=90)
160 zImg.updateFormat(out.tell(), tile.size, 'image/jpeg')
161 out.close()
162
163 self._tiles[zoom][x][y] = zImg
164
165 def _getAfterTilingHooks(self) :
166 return []
167
168
169 security.declareProtected(view, 'getAvailableZooms')
170 def getAvailableZooms(self):
171 zooms = list(self._tiles.keys())
172 zooms.sort()
173 return zooms
174
175 security.declareProtected(view, 'getTile')
176 def getTile(self, REQUEST, RESPONSE, zoom=1, x=0, y=0):
177 """ publishes tile
178 """
179 zoom, x, y = float(zoom), int(x), int(y)
180 if not self._tiles.has_key(zoom) :
181 sm = getSecurityManager()
182 if not sm.checkPermission(change_images_and_files, self) :
183 raise Unauthorized("Tiling arbitrary zoom unauthorized")
184 if self.makeTilesAt(zoom) :
185 tile = self._tiles[zoom][x][y]
186 return tile.index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
187 else :
188 tile = self._tiles[zoom][x][y]
189 return tile.index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
190