luxia--
[Photo.git] / ppm.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright © 2009 Benoît Pin <pin@cri.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 """ PPM File support module
21
22
23
24 """
25
26 from subprocess import Popen, PIPE
27 from tempfile import TemporaryFile
28 import os
29 from math import ceil
30 from PIL.Image import open as imgopen
31 from PIL.Image import fromstring
32 from PIL.Image import ANTIALIAS
33 from cStringIO import StringIO
34
35 DGJPEG = 'djpeg'
36 RESIZING_TILE_SIZE = 1024
37
38 class PPMFile(object) :
39
40 def __init__(self, f, tileSize=256, isRaw=False) :
41 # convert jpeg -> ppm with djpeg
42 if not isRaw :
43 # print 'djpeg'
44 self.fp = TemporaryFile(mode='w+')
45 p = Popen(DGJPEG, stdin=f, stdout=self.fp, stderr=PIPE, shell=True)
46 p.wait()
47 err = p.stderr.read()
48 if err :
49 raise SystemError, err
50 else :
51 self.fp = f
52
53 # get image specs with PIL
54 self.fp.seek(0)
55 im = imgopen(self.fp)
56 decoder, region, offset, parameters = im.tile[0]
57 x, y, width, height = region
58 del im
59 assert decoder == 'raw'
60 mode = parameters[0]
61 assert mode in ('RGB', 'L'), "Unsupported mode %s" % mode
62
63 if mode == 'RGB' :
64 sampleSize = 3
65 elif mode == 'L' :
66 sampleSize = 1
67
68 self.width = width
69 self.height = height
70 self.offset = offset
71 self.mode = parameters[0]
72 self.sampleSize = sampleSize
73 self._setTileSize(tileSize)
74
75 def _setTileSize(self, tileSize) :
76 self.tileSize = tileSize
77 self.tilesX = int(ceil(float(self.width) / self.tileSize))
78 self.tilesY = int(ceil(float(self.height) / self.tileSize))
79
80 def getTile(self, xt, yt) :
81 f = self.fp
82 ss = self.sampleSize
83 x = xt * self.tileSize
84 y = yt * self.tileSize
85 start = (self.width * y + x) * ss + self.offset
86
87 tw = th = self.tileSize
88
89 bw = self.width - x
90 if bw < self.tileSize :
91 tw = bw
92 bh = self.height - y
93 if bh < self.tileSize :
94 th = bh
95
96 assert tw > 0 and th > 0, "Tile requested out of image."
97
98 size = (tw, th)
99 tll = tw * ss
100 jump = (self.width - tw) * ss
101
102 f.seek(start)
103 data = StringIO()
104
105 for line in xrange(size[1]) :
106 data.write(f.read(tll))
107 f.seek(jump, 1)
108
109 data.seek(0)
110 im = fromstring(self.mode, size, data.read())
111 return im
112
113 def getTileSequence(self):
114 seq = []
115 for y in xrange(self.tilesY) :
116 for x in xrange(self.tilesX) :
117 seq.append((x, y))
118 return seq
119
120 def resize(self, ratio=None, maxLength=None) :
121 if ratio and maxLength :
122 raise AttributeError("'ratio' and 'size' are mutually exclusive.")
123 if maxLength :
124 maxFullLength = max(self.width, self.height)
125 ratio = float(maxLength) / maxFullLength
126
127 tileSizeBak = self.tileSize
128
129 self._setTileSize(RESIZING_TILE_SIZE)
130
131 width = height = 0
132 # cumul des arrondis
133 width = int(round(self.tileSize * ratio)) * (self.tilesX -1)
134 width += int(round((self.width - self.tileSize * (self.tilesX -1)) * ratio))
135
136 height = int(round(self.tileSize * ratio)) * (self.tilesY -1)
137 height += int(round((self.height - self.tileSize * (self.tilesY -1)) * ratio))
138
139 magic = self.mode == 'RGB' and 6 or 5
140 head = 'P%d %d %d 255\n' % (magic, width, height)
141 offset = len(head)
142
143 out = TemporaryFile(mode='w+')
144 out.write(head)
145
146 ss = self.sampleSize
147 rTll = int(round(self.tileSize * ratio))
148
149 for x, y in self.getTileSequence() :
150 # print 'resize', (x,y)
151 tile = self.getTile(x,y)
152 tileSize = tile.size
153 size = map(lambda l : int(round(l * ratio)), tileSize)
154
155 if size[0] and size[1] :
156 resized = tile.resize(size, ANTIALIAS)
157 data = resized.tostring()
158
159 start = (y * width + x) * ss * rTll + offset
160 jump = (width - size[0]) * ss
161
162 out.seek(start)
163 tll = size[0] * ss
164
165 # écriture dans le bon ordre (c'est quand même plus agréable à l'œil)
166 for l in xrange(size[1]) :
167 lineData = data[l*tll:(l+1)*tll]
168 out.write(lineData)
169 out.seek(jump, 1)
170
171 out.seek(0,2)
172 length = out.tell()
173 assert length - len(head) == width * height * ss, (length - len(head), width * height * ss)
174 out.seek(0)
175
176 self._setTileSize(tileSizeBak)
177 return PPMFile(out, tileSize=tileSizeBak, isRaw=True)
178
179 def getImage(self) :
180 self.fp.seek(0)
181 return imgopen(self.fp)
182
183 def __del__(self) :
184 self.fp.close()
185
186
187 if __name__ == '__main__' :
188 f = open('/Users/pinbe/Desktop/Chauve_souris.jpg')
189 try :
190 ppm = PPMFile(f, tileSize=256)
191 rppm = ppm.resize(maxLength=800)
192 im = rppm.getImage()
193 im.show()
194 for x, y in ppm.getTileSequence() :
195 im = ppm.getTile(x, y)
196 im.save('testoutput/%d_%d.jpg' % (x, y), 'JPEG', quality=90)
197 finally :
198 f.close()