1#
2# The Python Imaging Library.
3# $Id$
4#
5# IFUNC IM file handling for PIL
6#
7# history:
8# 1995-09-01 fl Created.
9# 1997-01-03 fl Save palette images
10# 1997-01-08 fl Added sequence support
11# 1997-01-23 fl Added P and RGB save support
12# 1997-05-31 fl Read floating point images
13# 1997-06-22 fl Save floating point images
14# 1997-08-27 fl Read and save 1-bit images
15# 1998-06-25 fl Added support for RGB+LUT images
16# 1998-07-02 fl Added support for YCC images
17# 1998-07-15 fl Renamed offset attribute to avoid name clash
18# 1998-12-29 fl Added I;16 support
19# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
20# 2003-09-26 fl Added LA/PA support
21#
22# Copyright (c) 1997-2003 by Secret Labs AB.
23# Copyright (c) 1995-2001 by Fredrik Lundh.
24#
25# See the README file for information on usage and redistribution.
26#
27from __future__ import annotations
28
29import os
30import re
31from typing import IO, Any
32
33from . import Image, ImageFile, ImagePalette
34from ._util import DeferredError
35
36# --------------------------------------------------------------------
37# Standard tags
38
39COMMENT = "Comment"
40DATE = "Date"
41EQUIPMENT = "Digitalization equipment"
42FRAMES = "File size (no of images)"
43LUT = "Lut"
44NAME = "Name"
45SCALE = "Scale (x,y)"
46SIZE = "Image size (x*y)"
47MODE = "Image type"
48
49TAGS = {
50 COMMENT: 0,
51 DATE: 0,
52 EQUIPMENT: 0,
53 FRAMES: 0,
54 LUT: 0,
55 NAME: 0,
56 SCALE: 0,
57 SIZE: 0,
58 MODE: 0,
59}
60
61OPEN = {
62 # ifunc93/p3cfunc formats
63 "0 1 image": ("1", "1"),
64 "L 1 image": ("1", "1"),
65 "Greyscale image": ("L", "L"),
66 "Grayscale image": ("L", "L"),
67 "RGB image": ("RGB", "RGB;L"),
68 "RLB image": ("RGB", "RLB"),
69 "RYB image": ("RGB", "RLB"),
70 "B1 image": ("1", "1"),
71 "B2 image": ("P", "P;2"),
72 "B4 image": ("P", "P;4"),
73 "X 24 image": ("RGB", "RGB"),
74 "L 32 S image": ("I", "I;32"),
75 "L 32 F image": ("F", "F;32"),
76 # old p3cfunc formats
77 "RGB3 image": ("RGB", "RGB;T"),
78 "RYB3 image": ("RGB", "RYB;T"),
79 # extensions
80 "LA image": ("LA", "LA;L"),
81 "PA image": ("LA", "PA;L"),
82 "RGBA image": ("RGBA", "RGBA;L"),
83 "RGBX image": ("RGB", "RGBX;L"),
84 "CMYK image": ("CMYK", "CMYK;L"),
85 "YCC image": ("YCbCr", "YCbCr;L"),
86}
87
88# ifunc95 extensions
89for i in ["8", "8S", "16", "16S", "32", "32F"]:
90 OPEN[f"L {i} image"] = ("F", f"F;{i}")
91 OPEN[f"L*{i} image"] = ("F", f"F;{i}")
92for i in ["16", "16L", "16B"]:
93 OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
94 OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
95for i in ["32S"]:
96 OPEN[f"L {i} image"] = ("I", f"I;{i}")
97 OPEN[f"L*{i} image"] = ("I", f"I;{i}")
98for j in range(2, 33):
99 OPEN[f"L*{j} image"] = ("F", f"F;{j}")
100
101
102# --------------------------------------------------------------------
103# Read IM directory
104
105split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
106
107
108def number(s: Any) -> float:
109 try:
110 return int(s)
111 except ValueError:
112 return float(s)
113
114
115##
116# Image plugin for the IFUNC IM file format.
117
118
119class ImImageFile(ImageFile.ImageFile):
120 format = "IM"
121 format_description = "IFUNC Image Memory"
122 _close_exclusive_fp_after_loading = False
123
124 def _open(self) -> None:
125 # Quick rejection: if there's not an LF among the first
126 # 100 bytes, this is (probably) not a text header.
127
128 if b"\n" not in self.fp.read(100):
129 msg = "not an IM file"
130 raise SyntaxError(msg)
131 self.fp.seek(0)
132
133 n = 0
134
135 # Default values
136 self.info[MODE] = "L"
137 self.info[SIZE] = (512, 512)
138 self.info[FRAMES] = 1
139
140 self.rawmode = "L"
141
142 while True:
143 s = self.fp.read(1)
144
145 # Some versions of IFUNC uses \n\r instead of \r\n...
146 if s == b"\r":
147 continue
148
149 if not s or s == b"\0" or s == b"\x1a":
150 break
151
152 # FIXME: this may read whole file if not a text file
153 s = s + self.fp.readline()
154
155 if len(s) > 100:
156 msg = "not an IM file"
157 raise SyntaxError(msg)
158
159 if s.endswith(b"\r\n"):
160 s = s[:-2]
161 elif s.endswith(b"\n"):
162 s = s[:-1]
163
164 try:
165 m = split.match(s)
166 except re.error as e:
167 msg = "not an IM file"
168 raise SyntaxError(msg) from e
169
170 if m:
171 k, v = m.group(1, 2)
172
173 # Don't know if this is the correct encoding,
174 # but a decent guess (I guess)
175 k = k.decode("latin-1", "replace")
176 v = v.decode("latin-1", "replace")
177
178 # Convert value as appropriate
179 if k in [FRAMES, SCALE, SIZE]:
180 v = v.replace("*", ",")
181 v = tuple(map(number, v.split(",")))
182 if len(v) == 1:
183 v = v[0]
184 elif k == MODE and v in OPEN:
185 v, self.rawmode = OPEN[v]
186
187 # Add to dictionary. Note that COMMENT tags are
188 # combined into a list of strings.
189 if k == COMMENT:
190 if k in self.info:
191 self.info[k].append(v)
192 else:
193 self.info[k] = [v]
194 else:
195 self.info[k] = v
196
197 if k in TAGS:
198 n += 1
199
200 else:
201 msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
202 raise SyntaxError(msg)
203
204 if not n:
205 msg = "Not an IM file"
206 raise SyntaxError(msg)
207
208 # Basic attributes
209 self._size = self.info[SIZE]
210 self._mode = self.info[MODE]
211
212 # Skip forward to start of image data
213 while s and not s.startswith(b"\x1a"):
214 s = self.fp.read(1)
215 if not s:
216 msg = "File truncated"
217 raise SyntaxError(msg)
218
219 if LUT in self.info:
220 # convert lookup table to palette or lut attribute
221 palette = self.fp.read(768)
222 greyscale = 1 # greyscale palette
223 linear = 1 # linear greyscale palette
224 for i in range(256):
225 if palette[i] == palette[i + 256] == palette[i + 512]:
226 if palette[i] != i:
227 linear = 0
228 else:
229 greyscale = 0
230 if self.mode in ["L", "LA", "P", "PA"]:
231 if greyscale:
232 if not linear:
233 self.lut = list(palette[:256])
234 else:
235 if self.mode in ["L", "P"]:
236 self._mode = self.rawmode = "P"
237 elif self.mode in ["LA", "PA"]:
238 self._mode = "PA"
239 self.rawmode = "PA;L"
240 self.palette = ImagePalette.raw("RGB;L", palette)
241 elif self.mode == "RGB":
242 if not greyscale or not linear:
243 self.lut = list(palette)
244
245 self.frame = 0
246
247 self.__offset = offs = self.fp.tell()
248
249 self._fp = self.fp # FIXME: hack
250
251 if self.rawmode.startswith("F;"):
252 # ifunc95 formats
253 try:
254 # use bit decoder (if necessary)
255 bits = int(self.rawmode[2:])
256 if bits not in [8, 16, 32]:
257 self.tile = [
258 ImageFile._Tile(
259 "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
260 )
261 ]
262 return
263 except ValueError:
264 pass
265
266 if self.rawmode in ["RGB;T", "RYB;T"]:
267 # Old LabEye/3PC files. Would be very surprised if anyone
268 # ever stumbled upon such a file ;-)
269 size = self.size[0] * self.size[1]
270 self.tile = [
271 ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
272 ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
273 ImageFile._Tile(
274 "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
275 ),
276 ]
277 else:
278 # LabEye/IFUNC files
279 self.tile = [
280 ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
281 ]
282
283 @property
284 def n_frames(self) -> int:
285 return self.info[FRAMES]
286
287 @property
288 def is_animated(self) -> bool:
289 return self.info[FRAMES] > 1
290
291 def seek(self, frame: int) -> None:
292 if not self._seek_check(frame):
293 return
294 if isinstance(self._fp, DeferredError):
295 raise self._fp.ex
296
297 self.frame = frame
298
299 if self.mode == "1":
300 bits = 1
301 else:
302 bits = 8 * len(self.mode)
303
304 size = ((self.size[0] * bits + 7) // 8) * self.size[1]
305 offs = self.__offset + frame * size
306
307 self.fp = self._fp
308
309 self.tile = [
310 ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
311 ]
312
313 def tell(self) -> int:
314 return self.frame
315
316
317#
318# --------------------------------------------------------------------
319# Save IM files
320
321
322SAVE = {
323 # mode: (im type, raw mode)
324 "1": ("0 1", "1"),
325 "L": ("Greyscale", "L"),
326 "LA": ("LA", "LA;L"),
327 "P": ("Greyscale", "P"),
328 "PA": ("LA", "PA;L"),
329 "I": ("L 32S", "I;32S"),
330 "I;16": ("L 16", "I;16"),
331 "I;16L": ("L 16L", "I;16L"),
332 "I;16B": ("L 16B", "I;16B"),
333 "F": ("L 32F", "F;32F"),
334 "RGB": ("RGB", "RGB;L"),
335 "RGBA": ("RGBA", "RGBA;L"),
336 "RGBX": ("RGBX", "RGBX;L"),
337 "CMYK": ("CMYK", "CMYK;L"),
338 "YCbCr": ("YCC", "YCbCr;L"),
339}
340
341
342def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
343 try:
344 image_type, rawmode = SAVE[im.mode]
345 except KeyError as e:
346 msg = f"Cannot save {im.mode} images as IM"
347 raise ValueError(msg) from e
348
349 frames = im.encoderinfo.get("frames", 1)
350
351 fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
352 if filename:
353 # Each line must be 100 characters or less,
354 # or: SyntaxError("not an IM file")
355 # 8 characters are used for "Name: " and "\r\n"
356 # Keep just the filename, ditch the potentially overlong path
357 if isinstance(filename, bytes):
358 filename = filename.decode("ascii")
359 name, ext = os.path.splitext(os.path.basename(filename))
360 name = "".join([name[: 92 - len(ext)], ext])
361
362 fp.write(f"Name: {name}\r\n".encode("ascii"))
363 fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
364 fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
365 if im.mode in ["P", "PA"]:
366 fp.write(b"Lut: 1\r\n")
367 fp.write(b"\000" * (511 - fp.tell()) + b"\032")
368 if im.mode in ["P", "PA"]:
369 im_palette = im.im.getpalette("RGB", "RGB;L")
370 colors = len(im_palette) // 3
371 palette = b""
372 for i in range(3):
373 palette += im_palette[colors * i : colors * (i + 1)]
374 palette += b"\x00" * (256 - colors)
375 fp.write(palette) # 768 bytes
376 ImageFile._save(
377 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
378 )
379
380
381#
382# --------------------------------------------------------------------
383# Registry
384
385
386Image.register_open(ImImageFile.format, ImImageFile)
387Image.register_save(ImImageFile.format, _save)
388
389Image.register_extension(ImImageFile.format, ".im")