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