1#
2# THIS IS WORK IN PROGRESS
3#
4# The Python Imaging Library.
5# $Id$
6#
7# FlashPix support for PIL
8#
9# History:
10# 97-01-25 fl Created (reads uncompressed RGB images only)
11#
12# Copyright (c) Secret Labs AB 1997.
13# Copyright (c) Fredrik Lundh 1997.
14#
15# See the README file for information on usage and redistribution.
16#
17from __future__ import annotations
18
19import olefile
20
21from . import Image, ImageFile
22from ._binary import i32le as i32
23
24# we map from colour field tuples to (mode, rawmode) descriptors
25MODES = {
26 # opacity
27 (0x00007FFE,): ("A", "L"),
28 # monochrome
29 (0x00010000,): ("L", "L"),
30 (0x00018000, 0x00017FFE): ("RGBA", "LA"),
31 # photo YCC
32 (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
33 (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
34 # standard RGB (NIFRGB)
35 (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
36 (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
37}
38
39
40#
41# --------------------------------------------------------------------
42
43
44def _accept(prefix: bytes) -> bool:
45 return prefix[:8] == olefile.MAGIC
46
47
48##
49# Image plugin for the FlashPix images.
50
51
52class FpxImageFile(ImageFile.ImageFile):
53 format = "FPX"
54 format_description = "FlashPix"
55
56 def _open(self):
57 #
58 # read the OLE directory and see if this is a likely
59 # to be a FlashPix file
60
61 try:
62 self.ole = olefile.OleFileIO(self.fp)
63 except OSError as e:
64 msg = "not an FPX file; invalid OLE file"
65 raise SyntaxError(msg) from e
66
67 if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
68 msg = "not an FPX file; bad root CLSID"
69 raise SyntaxError(msg)
70
71 self._open_index(1)
72
73 def _open_index(self, index: int = 1) -> None:
74 #
75 # get the Image Contents Property Set
76
77 prop = self.ole.getproperties(
78 [f"Data Object Store {index:06d}", "\005Image Contents"]
79 )
80
81 # size (highest resolution)
82
83 self._size = prop[0x1000002], prop[0x1000003]
84
85 size = max(self.size)
86 i = 1
87 while size > 64:
88 size = size // 2
89 i += 1
90 self.maxid = i - 1
91
92 # mode. instead of using a single field for this, flashpix
93 # requires you to specify the mode for each channel in each
94 # resolution subimage, and leaves it to the decoder to make
95 # sure that they all match. for now, we'll cheat and assume
96 # that this is always the case.
97
98 id = self.maxid << 16
99
100 s = prop[0x2000002 | id]
101
102 bands = i32(s, 4)
103 if bands > 4:
104 msg = "Invalid number of bands"
105 raise OSError(msg)
106
107 # note: for now, we ignore the "uncalibrated" flag
108 colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))
109
110 self._mode, self.rawmode = MODES[colors]
111
112 # load JPEG tables, if any
113 self.jpeg = {}
114 for i in range(256):
115 id = 0x3000001 | (i << 16)
116 if id in prop:
117 self.jpeg[i] = prop[id]
118
119 self._open_subimage(1, self.maxid)
120
121 def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
122 #
123 # setup tile descriptors for a given subimage
124
125 stream = [
126 f"Data Object Store {index:06d}",
127 f"Resolution {subimage:04d}",
128 "Subimage 0000 Header",
129 ]
130
131 fp = self.ole.openstream(stream)
132
133 # skip prefix
134 fp.read(28)
135
136 # header stream
137 s = fp.read(36)
138
139 size = i32(s, 4), i32(s, 8)
140 # tilecount = i32(s, 12)
141 tilesize = i32(s, 16), i32(s, 20)
142 # channels = i32(s, 24)
143 offset = i32(s, 28)
144 length = i32(s, 32)
145
146 if size != self.size:
147 msg = "subimage mismatch"
148 raise OSError(msg)
149
150 # get tile descriptors
151 fp.seek(28 + offset)
152 s = fp.read(i32(s, 12) * length)
153
154 x = y = 0
155 xsize, ysize = size
156 xtile, ytile = tilesize
157 self.tile = []
158
159 for i in range(0, len(s), length):
160 x1 = min(xsize, x + xtile)
161 y1 = min(ysize, y + ytile)
162
163 compression = i32(s, i + 8)
164
165 if compression == 0:
166 self.tile.append(
167 (
168 "raw",
169 (x, y, x1, y1),
170 i32(s, i) + 28,
171 (self.rawmode,),
172 )
173 )
174
175 elif compression == 1:
176 # FIXME: the fill decoder is not implemented
177 self.tile.append(
178 (
179 "fill",
180 (x, y, x1, y1),
181 i32(s, i) + 28,
182 (self.rawmode, s[12:16]),
183 )
184 )
185
186 elif compression == 2:
187 internal_color_conversion = s[14]
188 jpeg_tables = s[15]
189 rawmode = self.rawmode
190
191 if internal_color_conversion:
192 # The image is stored as usual (usually YCbCr).
193 if rawmode == "RGBA":
194 # For "RGBA", data is stored as YCbCrA based on
195 # negative RGB. The following trick works around
196 # this problem :
197 jpegmode, rawmode = "YCbCrK", "CMYK"
198 else:
199 jpegmode = None # let the decoder decide
200
201 else:
202 # The image is stored as defined by rawmode
203 jpegmode = rawmode
204
205 self.tile.append(
206 (
207 "jpeg",
208 (x, y, x1, y1),
209 i32(s, i) + 28,
210 (rawmode, jpegmode),
211 )
212 )
213
214 # FIXME: jpeg tables are tile dependent; the prefix
215 # data must be placed in the tile descriptor itself!
216
217 if jpeg_tables:
218 self.tile_prefix = self.jpeg[jpeg_tables]
219
220 else:
221 msg = "unknown/invalid compression"
222 raise OSError(msg)
223
224 x = x + xtile
225 if x >= xsize:
226 x, y = 0, y + ytile
227 if y >= ysize:
228 break # isn't really required
229
230 self.stream = stream
231 self._fp = self.fp
232 self.fp = None
233
234 def load(self):
235 if not self.fp:
236 self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
237
238 return ImageFile.ImageFile.load(self)
239
240 def close(self) -> None:
241 self.ole.close()
242 super().close()
243
244 def __exit__(self, *args: object) -> None:
245 self.ole.close()
246 super().__exit__()
247
248
249#
250# --------------------------------------------------------------------
251
252
253Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
254
255Image.register_extension(FpxImageFile.format, ".fpx")