Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.1.0-py3.10-linux-x86_64.egg/PIL/TiffImagePlugin.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1215 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# TIFF file handling 

6# 

7# TIFF is a flexible, if somewhat aged, image file format originally 

8# defined by Aldus. Although TIFF supports a wide variety of pixel 

9# layouts and compression methods, the name doesn't really stand for 

10# "thousands of incompatible file formats," it just feels that way. 

11# 

12# To read TIFF data from a stream, the stream must be seekable. For 

13# progressive decoding, make sure to use TIFF files where the tag 

14# directory is placed first in the file. 

15# 

16# History: 

17# 1995-09-01 fl Created 

18# 1996-05-04 fl Handle JPEGTABLES tag 

19# 1996-05-18 fl Fixed COLORMAP support 

20# 1997-01-05 fl Fixed PREDICTOR support 

21# 1997-08-27 fl Added support for rational tags (from Perry Stoll) 

22# 1998-01-10 fl Fixed seek/tell (from Jan Blom) 

23# 1998-07-15 fl Use private names for internal variables 

24# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) 

25# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) 

26# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) 

27# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) 

28# 2001-12-18 fl Added workaround for broken Matrox library 

29# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) 

30# 2003-05-19 fl Check FILLORDER tag 

31# 2003-09-26 fl Added RGBa support 

32# 2004-02-24 fl Added DPI support; fixed rational write support 

33# 2005-02-07 fl Added workaround for broken Corel Draw 10 files 

34# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) 

35# 

36# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. 

37# Copyright (c) 1995-1997 by Fredrik Lundh 

38# 

39# See the README file for information on usage and redistribution. 

40# 

41from __future__ import annotations 

42 

43import io 

44import itertools 

45import logging 

46import math 

47import os 

48import struct 

49import warnings 

50from collections.abc import Iterator, MutableMapping 

51from fractions import Fraction 

52from numbers import Number, Rational 

53from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast 

54 

55from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags 

56from ._binary import i16be as i16 

57from ._binary import i32be as i32 

58from ._binary import o8 

59from ._deprecate import deprecate 

60from ._typing import StrOrBytesPath 

61from ._util import is_path 

62from .TiffTags import TYPES 

63 

64if TYPE_CHECKING: 

65 from ._typing import Buffer, IntegralLike 

66 

67logger = logging.getLogger(__name__) 

68 

69# Set these to true to force use of libtiff for reading or writing. 

70READ_LIBTIFF = False 

71WRITE_LIBTIFF = False 

72STRIP_SIZE = 65536 

73 

74II = b"II" # little-endian (Intel style) 

75MM = b"MM" # big-endian (Motorola style) 

76 

77# 

78# -------------------------------------------------------------------- 

79# Read TIFF files 

80 

81# a few tag names, just to make the code below a bit more readable 

82OSUBFILETYPE = 255 

83IMAGEWIDTH = 256 

84IMAGELENGTH = 257 

85BITSPERSAMPLE = 258 

86COMPRESSION = 259 

87PHOTOMETRIC_INTERPRETATION = 262 

88FILLORDER = 266 

89IMAGEDESCRIPTION = 270 

90STRIPOFFSETS = 273 

91SAMPLESPERPIXEL = 277 

92ROWSPERSTRIP = 278 

93STRIPBYTECOUNTS = 279 

94X_RESOLUTION = 282 

95Y_RESOLUTION = 283 

96PLANAR_CONFIGURATION = 284 

97RESOLUTION_UNIT = 296 

98TRANSFERFUNCTION = 301 

99SOFTWARE = 305 

100DATE_TIME = 306 

101ARTIST = 315 

102PREDICTOR = 317 

103COLORMAP = 320 

104TILEWIDTH = 322 

105TILELENGTH = 323 

106TILEOFFSETS = 324 

107TILEBYTECOUNTS = 325 

108SUBIFD = 330 

109EXTRASAMPLES = 338 

110SAMPLEFORMAT = 339 

111JPEGTABLES = 347 

112YCBCRSUBSAMPLING = 530 

113REFERENCEBLACKWHITE = 532 

114COPYRIGHT = 33432 

115IPTC_NAA_CHUNK = 33723 # newsphoto properties 

116PHOTOSHOP_CHUNK = 34377 # photoshop properties 

117ICCPROFILE = 34675 

118EXIFIFD = 34665 

119XMP = 700 

120JPEGQUALITY = 65537 # pseudo-tag by libtiff 

121 

122# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java 

123IMAGEJ_META_DATA_BYTE_COUNTS = 50838 

124IMAGEJ_META_DATA = 50839 

125 

126COMPRESSION_INFO = { 

127 # Compression => pil compression name 

128 1: "raw", 

129 2: "tiff_ccitt", 

130 3: "group3", 

131 4: "group4", 

132 5: "tiff_lzw", 

133 6: "tiff_jpeg", # obsolete 

134 7: "jpeg", 

135 8: "tiff_adobe_deflate", 

136 32771: "tiff_raw_16", # 16-bit padding 

137 32773: "packbits", 

138 32809: "tiff_thunderscan", 

139 32946: "tiff_deflate", 

140 34676: "tiff_sgilog", 

141 34677: "tiff_sgilog24", 

142 34925: "lzma", 

143 50000: "zstd", 

144 50001: "webp", 

145} 

146 

147COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} 

148 

149OPEN_INFO = { 

150 # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, 

151 # ExtraSamples) => mode, rawmode 

152 (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), 

153 (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), 

154 (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), 

155 (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), 

156 (II, 1, (1,), 1, (1,), ()): ("1", "1"), 

157 (MM, 1, (1,), 1, (1,), ()): ("1", "1"), 

158 (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), 

159 (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), 

160 (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), 

161 (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), 

162 (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), 

163 (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), 

164 (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), 

165 (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), 

166 (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), 

167 (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), 

168 (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), 

169 (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), 

170 (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), 

171 (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), 

172 (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), 

173 (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), 

174 (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), 

175 (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), 

176 (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), 

177 (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), 

178 (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), 

179 (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), 

180 (II, 1, (1,), 1, (8,), ()): ("L", "L"), 

181 (MM, 1, (1,), 1, (8,), ()): ("L", "L"), 

182 (II, 1, (2,), 1, (8,), ()): ("L", "L"), 

183 (MM, 1, (2,), 1, (8,), ()): ("L", "L"), 

184 (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), 

185 (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), 

186 (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), 

187 (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), 

188 (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), 

189 (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), 

190 (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), 

191 (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), 

192 (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), 

193 (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), 

194 (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), 

195 (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), 

196 (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), 

197 (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), 

198 (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), 

199 (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), 

200 (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), 

201 (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), 

202 (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), 

203 (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), 

204 (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), 

205 (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), 

206 (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples 

207 (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples 

208 (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), 

209 (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), 

210 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), 

211 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), 

212 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), 

213 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), 

214 (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), 

215 (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), 

216 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), 

217 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), 

218 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), 

219 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), 

220 (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), 

221 (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), 

222 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), 

223 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), 

224 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), 

225 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), 

226 (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 

227 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 

228 (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), 

229 (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), 

230 (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), 

231 (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), 

232 (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), 

233 (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), 

234 (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), 

235 (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), 

236 (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), 

237 (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), 

238 (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), 

239 (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), 

240 (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), 

241 (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), 

242 (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), 

243 (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), 

244 (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), 

245 (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), 

246 (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), 

247 (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), 

248 (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), 

249 (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), 

250 (II, 3, (1,), 1, (8,), ()): ("P", "P"), 

251 (MM, 3, (1,), 1, (8,), ()): ("P", "P"), 

252 (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), 

253 (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), 

254 (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), 

255 (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), 

256 (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), 

257 (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), 

258 (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), 

259 (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), 

260 (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), 

261 (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), 

262 (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), 

263 (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), 

264 (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), 

265 (II, 6, (1,), 1, (8,), ()): ("L", "L"), 

266 (MM, 6, (1,), 1, (8,), ()): ("L", "L"), 

267 # JPEG compressed images handled by LibTiff and auto-converted to RGBX 

268 # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel 

269 (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), 

270 (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), 

271 (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), 

272 (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), 

273} 

274 

275MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) 

276 

277PREFIXES = [ 

278 b"MM\x00\x2A", # Valid TIFF header with big-endian byte order 

279 b"II\x2A\x00", # Valid TIFF header with little-endian byte order 

280 b"MM\x2A\x00", # Invalid TIFF header, assume big-endian 

281 b"II\x00\x2A", # Invalid TIFF header, assume little-endian 

282 b"MM\x00\x2B", # BigTIFF with big-endian byte order 

283 b"II\x2B\x00", # BigTIFF with little-endian byte order 

284] 

285 

286if not getattr(Image.core, "libtiff_support_custom_tags", True): 

287 deprecate("Support for LibTIFF earlier than version 4", 12) 

288 

289 

290def _accept(prefix: bytes) -> bool: 

291 return prefix[:4] in PREFIXES 

292 

293 

294def _limit_rational( 

295 val: float | Fraction | IFDRational, max_val: int 

296) -> tuple[IntegralLike, IntegralLike]: 

297 inv = abs(val) > 1 

298 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) 

299 return n_d[::-1] if inv else n_d 

300 

301 

302def _limit_signed_rational( 

303 val: IFDRational, max_val: int, min_val: int 

304) -> tuple[IntegralLike, IntegralLike]: 

305 frac = Fraction(val) 

306 n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator 

307 

308 if min(float(i) for i in n_d) < min_val: 

309 n_d = _limit_rational(val, abs(min_val)) 

310 

311 n_d_float = tuple(float(i) for i in n_d) 

312 if max(n_d_float) > max_val: 

313 n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) 

314 

315 return n_d 

316 

317 

318## 

319# Wrapper for TIFF IFDs. 

320 

321_load_dispatch = {} 

322_write_dispatch = {} 

323 

324 

325def _delegate(op: str) -> Any: 

326 def delegate( 

327 self: IFDRational, *args: tuple[float, ...] 

328 ) -> bool | float | Fraction: 

329 return getattr(self._val, op)(*args) 

330 

331 return delegate 

332 

333 

334class IFDRational(Rational): 

335 """Implements a rational class where 0/0 is a legal value to match 

336 the in the wild use of exif rationals. 

337 

338 e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used 

339 """ 

340 

341 """ If the denominator is 0, store this as a float('nan'), otherwise store 

342 as a fractions.Fraction(). Delegate as appropriate 

343 

344 """ 

345 

346 __slots__ = ("_numerator", "_denominator", "_val") 

347 

348 def __init__( 

349 self, value: float | Fraction | IFDRational, denominator: int = 1 

350 ) -> None: 

351 """ 

352 :param value: either an integer numerator, a 

353 float/rational/other number, or an IFDRational 

354 :param denominator: Optional integer denominator 

355 """ 

356 self._val: Fraction | float 

357 if isinstance(value, IFDRational): 

358 self._numerator = value.numerator 

359 self._denominator = value.denominator 

360 self._val = value._val 

361 return 

362 

363 if isinstance(value, Fraction): 

364 self._numerator = value.numerator 

365 self._denominator = value.denominator 

366 else: 

367 if TYPE_CHECKING: 

368 self._numerator = cast(IntegralLike, value) 

369 else: 

370 self._numerator = value 

371 self._denominator = denominator 

372 

373 if denominator == 0: 

374 self._val = float("nan") 

375 elif denominator == 1: 

376 self._val = Fraction(value) 

377 elif int(value) == value: 

378 self._val = Fraction(int(value), denominator) 

379 else: 

380 self._val = Fraction(value / denominator) 

381 

382 @property 

383 def numerator(self) -> IntegralLike: 

384 return self._numerator 

385 

386 @property 

387 def denominator(self) -> int: 

388 return self._denominator 

389 

390 def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: 

391 """ 

392 

393 :param max_denominator: Integer, the maximum denominator value 

394 :returns: Tuple of (numerator, denominator) 

395 """ 

396 

397 if self.denominator == 0: 

398 return self.numerator, self.denominator 

399 

400 assert isinstance(self._val, Fraction) 

401 f = self._val.limit_denominator(max_denominator) 

402 return f.numerator, f.denominator 

403 

404 def __repr__(self) -> str: 

405 return str(float(self._val)) 

406 

407 def __hash__(self) -> int: 

408 return self._val.__hash__() 

409 

410 def __eq__(self, other: object) -> bool: 

411 val = self._val 

412 if isinstance(other, IFDRational): 

413 other = other._val 

414 if isinstance(other, float): 

415 val = float(val) 

416 return val == other 

417 

418 def __getstate__(self) -> list[float | Fraction | IntegralLike]: 

419 return [self._val, self._numerator, self._denominator] 

420 

421 def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: 

422 IFDRational.__init__(self, 0) 

423 _val, _numerator, _denominator = state 

424 assert isinstance(_val, (float, Fraction)) 

425 self._val = _val 

426 if TYPE_CHECKING: 

427 self._numerator = cast(IntegralLike, _numerator) 

428 else: 

429 self._numerator = _numerator 

430 assert isinstance(_denominator, int) 

431 self._denominator = _denominator 

432 

433 """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 

434 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 

435 'mod','rmod', 'pow','rpow', 'pos', 'neg', 

436 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', 

437 'ceil', 'floor', 'round'] 

438 print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) 

439 """ 

440 

441 __add__ = _delegate("__add__") 

442 __radd__ = _delegate("__radd__") 

443 __sub__ = _delegate("__sub__") 

444 __rsub__ = _delegate("__rsub__") 

445 __mul__ = _delegate("__mul__") 

446 __rmul__ = _delegate("__rmul__") 

447 __truediv__ = _delegate("__truediv__") 

448 __rtruediv__ = _delegate("__rtruediv__") 

449 __floordiv__ = _delegate("__floordiv__") 

450 __rfloordiv__ = _delegate("__rfloordiv__") 

451 __mod__ = _delegate("__mod__") 

452 __rmod__ = _delegate("__rmod__") 

453 __pow__ = _delegate("__pow__") 

454 __rpow__ = _delegate("__rpow__") 

455 __pos__ = _delegate("__pos__") 

456 __neg__ = _delegate("__neg__") 

457 __abs__ = _delegate("__abs__") 

458 __trunc__ = _delegate("__trunc__") 

459 __lt__ = _delegate("__lt__") 

460 __gt__ = _delegate("__gt__") 

461 __le__ = _delegate("__le__") 

462 __ge__ = _delegate("__ge__") 

463 __bool__ = _delegate("__bool__") 

464 __ceil__ = _delegate("__ceil__") 

465 __floor__ = _delegate("__floor__") 

466 __round__ = _delegate("__round__") 

467 # Python >= 3.11 

468 if hasattr(Fraction, "__int__"): 

469 __int__ = _delegate("__int__") 

470 

471 

472_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] 

473 

474 

475def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: 

476 def decorator(func: _LoaderFunc) -> _LoaderFunc: 

477 from .TiffTags import TYPES 

478 

479 if func.__name__.startswith("load_"): 

480 TYPES[idx] = func.__name__[5:].replace("_", " ") 

481 _load_dispatch[idx] = size, func # noqa: F821 

482 return func 

483 

484 return decorator 

485 

486 

487def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: 

488 def decorator(func: Callable[..., Any]) -> Callable[..., Any]: 

489 _write_dispatch[idx] = func # noqa: F821 

490 return func 

491 

492 return decorator 

493 

494 

495def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: 

496 from .TiffTags import TYPES 

497 

498 idx, fmt, name = idx_fmt_name 

499 TYPES[idx] = name 

500 size = struct.calcsize(f"={fmt}") 

501 

502 def basic_handler( 

503 self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True 

504 ) -> tuple[Any, ...]: 

505 return self._unpack(f"{len(data) // size}{fmt}", data) 

506 

507 _load_dispatch[idx] = size, basic_handler # noqa: F821 

508 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 

509 b"".join(self._pack(fmt, value) for value in values) 

510 ) 

511 

512 

513if TYPE_CHECKING: 

514 _IFDv2Base = MutableMapping[int, Any] 

515else: 

516 _IFDv2Base = MutableMapping 

517 

518 

519class ImageFileDirectory_v2(_IFDv2Base): 

520 """This class represents a TIFF tag directory. To speed things up, we 

521 don't decode tags unless they're asked for. 

522 

523 Exposes a dictionary interface of the tags in the directory:: 

524 

525 ifd = ImageFileDirectory_v2() 

526 ifd[key] = 'Some Data' 

527 ifd.tagtype[key] = TiffTags.ASCII 

528 print(ifd[key]) 

529 'Some Data' 

530 

531 Individual values are returned as the strings or numbers, sequences are 

532 returned as tuples of the values. 

533 

534 The tiff metadata type of each item is stored in a dictionary of 

535 tag types in 

536 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types 

537 are read from a tiff file, guessed from the type added, or added 

538 manually. 

539 

540 Data Structures: 

541 

542 * ``self.tagtype = {}`` 

543 

544 * Key: numerical TIFF tag number 

545 * Value: integer corresponding to the data type from 

546 :py:data:`.TiffTags.TYPES` 

547 

548 .. versionadded:: 3.0.0 

549 

550 'Internal' data structures: 

551 

552 * ``self._tags_v2 = {}`` 

553 

554 * Key: numerical TIFF tag number 

555 * Value: decoded data, as tuple for multiple values 

556 

557 * ``self._tagdata = {}`` 

558 

559 * Key: numerical TIFF tag number 

560 * Value: undecoded byte string from file 

561 

562 * ``self._tags_v1 = {}`` 

563 

564 * Key: numerical TIFF tag number 

565 * Value: decoded data in the v1 format 

566 

567 Tags will be found in the private attributes ``self._tagdata``, and in 

568 ``self._tags_v2`` once decoded. 

569 

570 ``self.legacy_api`` is a value for internal use, and shouldn't be changed 

571 from outside code. In cooperation with 

572 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` 

573 is true, then decoded tags will be populated into both ``_tags_v1`` and 

574 ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF 

575 save routine. Tags should be read from ``_tags_v1`` if 

576 ``legacy_api == true``. 

577 

578 """ 

579 

580 _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} 

581 _write_dispatch: dict[int, Callable[..., Any]] = {} 

582 

583 def __init__( 

584 self, 

585 ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", 

586 prefix: bytes | None = None, 

587 group: int | None = None, 

588 ) -> None: 

589 """Initialize an ImageFileDirectory. 

590 

591 To construct an ImageFileDirectory from a real file, pass the 8-byte 

592 magic header to the constructor. To only set the endianness, pass it 

593 as the 'prefix' keyword argument. 

594 

595 :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets 

596 endianness. 

597 :param prefix: Override the endianness of the file. 

598 """ 

599 if not _accept(ifh): 

600 msg = f"not a TIFF file (header {repr(ifh)} not valid)" 

601 raise SyntaxError(msg) 

602 self._prefix = prefix if prefix is not None else ifh[:2] 

603 if self._prefix == MM: 

604 self._endian = ">" 

605 elif self._prefix == II: 

606 self._endian = "<" 

607 else: 

608 msg = "not a TIFF IFD" 

609 raise SyntaxError(msg) 

610 self._bigtiff = ifh[2] == 43 

611 self.group = group 

612 self.tagtype: dict[int, int] = {} 

613 """ Dictionary of tag types """ 

614 self.reset() 

615 self.next = ( 

616 self._unpack("Q", ifh[8:])[0] 

617 if self._bigtiff 

618 else self._unpack("L", ifh[4:])[0] 

619 ) 

620 self._legacy_api = False 

621 

622 prefix = property(lambda self: self._prefix) 

623 offset = property(lambda self: self._offset) 

624 

625 @property 

626 def legacy_api(self) -> bool: 

627 return self._legacy_api 

628 

629 @legacy_api.setter 

630 def legacy_api(self, value: bool) -> NoReturn: 

631 msg = "Not allowing setting of legacy api" 

632 raise Exception(msg) 

633 

634 def reset(self) -> None: 

635 self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false 

636 self._tags_v2: dict[int, Any] = {} # main tag storage 

637 self._tagdata: dict[int, bytes] = {} 

638 self.tagtype = {} # added 2008-06-05 by Florian Hoech 

639 self._next = None 

640 self._offset: int | None = None 

641 

642 def __str__(self) -> str: 

643 return str(dict(self)) 

644 

645 def named(self) -> dict[str, Any]: 

646 """ 

647 :returns: dict of name|key: value 

648 

649 Returns the complete tag dictionary, with named tags where possible. 

650 """ 

651 return { 

652 TiffTags.lookup(code, self.group).name: value 

653 for code, value in self.items() 

654 } 

655 

656 def __len__(self) -> int: 

657 return len(set(self._tagdata) | set(self._tags_v2)) 

658 

659 def __getitem__(self, tag: int) -> Any: 

660 if tag not in self._tags_v2: # unpack on the fly 

661 data = self._tagdata[tag] 

662 typ = self.tagtype[tag] 

663 size, handler = self._load_dispatch[typ] 

664 self[tag] = handler(self, data, self.legacy_api) # check type 

665 val = self._tags_v2[tag] 

666 if self.legacy_api and not isinstance(val, (tuple, bytes)): 

667 val = (val,) 

668 return val 

669 

670 def __contains__(self, tag: object) -> bool: 

671 return tag in self._tags_v2 or tag in self._tagdata 

672 

673 def __setitem__(self, tag: int, value: Any) -> None: 

674 self._setitem(tag, value, self.legacy_api) 

675 

676 def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: 

677 basetypes = (Number, bytes, str) 

678 

679 info = TiffTags.lookup(tag, self.group) 

680 values = [value] if isinstance(value, basetypes) else value 

681 

682 if tag not in self.tagtype: 

683 if info.type: 

684 self.tagtype[tag] = info.type 

685 else: 

686 self.tagtype[tag] = TiffTags.UNDEFINED 

687 if all(isinstance(v, IFDRational) for v in values): 

688 for v in values: 

689 assert isinstance(v, IFDRational) 

690 if v < 0: 

691 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL 

692 break 

693 else: 

694 self.tagtype[tag] = TiffTags.RATIONAL 

695 elif all(isinstance(v, int) for v in values): 

696 short = True 

697 signed_short = True 

698 long = True 

699 for v in values: 

700 assert isinstance(v, int) 

701 if short and not (0 <= v < 2**16): 

702 short = False 

703 if signed_short and not (-(2**15) < v < 2**15): 

704 signed_short = False 

705 if long and v < 0: 

706 long = False 

707 if short: 

708 self.tagtype[tag] = TiffTags.SHORT 

709 elif signed_short: 

710 self.tagtype[tag] = TiffTags.SIGNED_SHORT 

711 elif long: 

712 self.tagtype[tag] = TiffTags.LONG 

713 else: 

714 self.tagtype[tag] = TiffTags.SIGNED_LONG 

715 elif all(isinstance(v, float) for v in values): 

716 self.tagtype[tag] = TiffTags.DOUBLE 

717 elif all(isinstance(v, str) for v in values): 

718 self.tagtype[tag] = TiffTags.ASCII 

719 elif all(isinstance(v, bytes) for v in values): 

720 self.tagtype[tag] = TiffTags.BYTE 

721 

722 if self.tagtype[tag] == TiffTags.UNDEFINED: 

723 values = [ 

724 v.encode("ascii", "replace") if isinstance(v, str) else v 

725 for v in values 

726 ] 

727 elif self.tagtype[tag] == TiffTags.RATIONAL: 

728 values = [float(v) if isinstance(v, int) else v for v in values] 

729 

730 is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) 

731 if not is_ifd: 

732 values = tuple( 

733 info.cvt_enum(value) if isinstance(value, str) else value 

734 for value in values 

735 ) 

736 

737 dest = self._tags_v1 if legacy_api else self._tags_v2 

738 

739 # Three branches: 

740 # Spec'd length == 1, Actual length 1, store as element 

741 # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. 

742 # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. 

743 # Don't mess with the legacy api, since it's frozen. 

744 if not is_ifd and ( 

745 (info.length == 1) 

746 or self.tagtype[tag] == TiffTags.BYTE 

747 or (info.length is None and len(values) == 1 and not legacy_api) 

748 ): 

749 # Don't mess with the legacy api, since it's frozen. 

750 if legacy_api and self.tagtype[tag] in [ 

751 TiffTags.RATIONAL, 

752 TiffTags.SIGNED_RATIONAL, 

753 ]: # rationals 

754 values = (values,) 

755 try: 

756 (dest[tag],) = values 

757 except ValueError: 

758 # We've got a builtin tag with 1 expected entry 

759 warnings.warn( 

760 f"Metadata Warning, tag {tag} had too many entries: " 

761 f"{len(values)}, expected 1" 

762 ) 

763 dest[tag] = values[0] 

764 

765 else: 

766 # Spec'd length > 1 or undefined 

767 # Unspec'd, and length > 1 

768 dest[tag] = values 

769 

770 def __delitem__(self, tag: int) -> None: 

771 self._tags_v2.pop(tag, None) 

772 self._tags_v1.pop(tag, None) 

773 self._tagdata.pop(tag, None) 

774 

775 def __iter__(self) -> Iterator[int]: 

776 return iter(set(self._tagdata) | set(self._tags_v2)) 

777 

778 def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: 

779 return struct.unpack(self._endian + fmt, data) 

780 

781 def _pack(self, fmt: str, *values: Any) -> bytes: 

782 return struct.pack(self._endian + fmt, *values) 

783 

784 list( 

785 map( 

786 _register_basic, 

787 [ 

788 (TiffTags.SHORT, "H", "short"), 

789 (TiffTags.LONG, "L", "long"), 

790 (TiffTags.SIGNED_BYTE, "b", "signed byte"), 

791 (TiffTags.SIGNED_SHORT, "h", "signed short"), 

792 (TiffTags.SIGNED_LONG, "l", "signed long"), 

793 (TiffTags.FLOAT, "f", "float"), 

794 (TiffTags.DOUBLE, "d", "double"), 

795 (TiffTags.IFD, "L", "long"), 

796 (TiffTags.LONG8, "Q", "long8"), 

797 ], 

798 ) 

799 ) 

800 

801 @_register_loader(1, 1) # Basic type, except for the legacy API. 

802 def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: 

803 return data 

804 

805 @_register_writer(1) # Basic type, except for the legacy API. 

806 def write_byte(self, data: bytes | int | IFDRational) -> bytes: 

807 if isinstance(data, IFDRational): 

808 data = int(data) 

809 if isinstance(data, int): 

810 data = bytes((data,)) 

811 return data 

812 

813 @_register_loader(2, 1) 

814 def load_string(self, data: bytes, legacy_api: bool = True) -> str: 

815 if data.endswith(b"\0"): 

816 data = data[:-1] 

817 return data.decode("latin-1", "replace") 

818 

819 @_register_writer(2) 

820 def write_string(self, value: str | bytes | int) -> bytes: 

821 # remerge of https://github.com/python-pillow/Pillow/pull/1416 

822 if isinstance(value, int): 

823 value = str(value) 

824 if not isinstance(value, bytes): 

825 value = value.encode("ascii", "replace") 

826 return value + b"\0" 

827 

828 @_register_loader(5, 8) 

829 def load_rational( 

830 self, data: bytes, legacy_api: bool = True 

831 ) -> tuple[tuple[int, int] | IFDRational, ...]: 

832 vals = self._unpack(f"{len(data) // 4}L", data) 

833 

834 def combine(a: int, b: int) -> tuple[int, int] | IFDRational: 

835 return (a, b) if legacy_api else IFDRational(a, b) 

836 

837 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) 

838 

839 @_register_writer(5) 

840 def write_rational(self, *values: IFDRational) -> bytes: 

841 return b"".join( 

842 self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values 

843 ) 

844 

845 @_register_loader(7, 1) 

846 def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: 

847 return data 

848 

849 @_register_writer(7) 

850 def write_undefined(self, value: bytes | int | IFDRational) -> bytes: 

851 if isinstance(value, IFDRational): 

852 value = int(value) 

853 if isinstance(value, int): 

854 value = str(value).encode("ascii", "replace") 

855 return value 

856 

857 @_register_loader(10, 8) 

858 def load_signed_rational( 

859 self, data: bytes, legacy_api: bool = True 

860 ) -> tuple[tuple[int, int] | IFDRational, ...]: 

861 vals = self._unpack(f"{len(data) // 4}l", data) 

862 

863 def combine(a: int, b: int) -> tuple[int, int] | IFDRational: 

864 return (a, b) if legacy_api else IFDRational(a, b) 

865 

866 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) 

867 

868 @_register_writer(10) 

869 def write_signed_rational(self, *values: IFDRational) -> bytes: 

870 return b"".join( 

871 self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) 

872 for frac in values 

873 ) 

874 

875 def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: 

876 ret = fp.read(size) 

877 if len(ret) != size: 

878 msg = ( 

879 "Corrupt EXIF data. " 

880 f"Expecting to read {size} bytes but only got {len(ret)}. " 

881 ) 

882 raise OSError(msg) 

883 return ret 

884 

885 def load(self, fp: IO[bytes]) -> None: 

886 self.reset() 

887 self._offset = fp.tell() 

888 

889 try: 

890 tag_count = ( 

891 self._unpack("Q", self._ensure_read(fp, 8)) 

892 if self._bigtiff 

893 else self._unpack("H", self._ensure_read(fp, 2)) 

894 )[0] 

895 for i in range(tag_count): 

896 tag, typ, count, data = ( 

897 self._unpack("HHQ8s", self._ensure_read(fp, 20)) 

898 if self._bigtiff 

899 else self._unpack("HHL4s", self._ensure_read(fp, 12)) 

900 ) 

901 

902 tagname = TiffTags.lookup(tag, self.group).name 

903 typname = TYPES.get(typ, "unknown") 

904 msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" 

905 

906 try: 

907 unit_size, handler = self._load_dispatch[typ] 

908 except KeyError: 

909 logger.debug("%s - unsupported type %s", msg, typ) 

910 continue # ignore unsupported type 

911 size = count * unit_size 

912 if size > (8 if self._bigtiff else 4): 

913 here = fp.tell() 

914 (offset,) = self._unpack("Q" if self._bigtiff else "L", data) 

915 msg += f" Tag Location: {here} - Data Location: {offset}" 

916 fp.seek(offset) 

917 data = ImageFile._safe_read(fp, size) 

918 fp.seek(here) 

919 else: 

920 data = data[:size] 

921 

922 if len(data) != size: 

923 warnings.warn( 

924 "Possibly corrupt EXIF data. " 

925 f"Expecting to read {size} bytes but only got {len(data)}." 

926 f" Skipping tag {tag}" 

927 ) 

928 logger.debug(msg) 

929 continue 

930 

931 if not data: 

932 logger.debug(msg) 

933 continue 

934 

935 self._tagdata[tag] = data 

936 self.tagtype[tag] = typ 

937 

938 msg += " - value: " 

939 msg += f"<table: {size} bytes>" if size > 32 else repr(data) 

940 

941 logger.debug(msg) 

942 

943 (self.next,) = ( 

944 self._unpack("Q", self._ensure_read(fp, 8)) 

945 if self._bigtiff 

946 else self._unpack("L", self._ensure_read(fp, 4)) 

947 ) 

948 except OSError as msg: 

949 warnings.warn(str(msg)) 

950 return 

951 

952 def _get_ifh(self): 

953 ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) 

954 if self._bigtiff: 

955 ifh += self._pack("HH", 8, 0) 

956 ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) 

957 

958 return ifh 

959 

960 def tobytes(self, offset: int = 0) -> bytes: 

961 # FIXME What about tagdata? 

962 result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) 

963 

964 entries: list[tuple[int, int, int, bytes, bytes]] = [] 

965 offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4 

966 stripoffsets = None 

967 

968 # pass 1: convert tags to binary format 

969 # always write tags in ascending order 

970 fmt = "Q" if self._bigtiff else "L" 

971 fmt_size = 8 if self._bigtiff else 4 

972 for tag, value in sorted(self._tags_v2.items()): 

973 if tag == STRIPOFFSETS: 

974 stripoffsets = len(entries) 

975 typ = self.tagtype[tag] 

976 logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) 

977 is_ifd = typ == TiffTags.LONG and isinstance(value, dict) 

978 if is_ifd: 

979 ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) 

980 values = self._tags_v2[tag] 

981 for ifd_tag, ifd_value in values.items(): 

982 ifd[ifd_tag] = ifd_value 

983 data = ifd.tobytes(offset) 

984 else: 

985 values = value if isinstance(value, tuple) else (value,) 

986 data = self._write_dispatch[typ](self, *values) 

987 

988 tagname = TiffTags.lookup(tag, self.group).name 

989 typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") 

990 msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " 

991 msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values) 

992 logger.debug(msg) 

993 

994 # count is sum of lengths for string and arbitrary data 

995 if is_ifd: 

996 count = 1 

997 elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: 

998 count = len(data) 

999 else: 

1000 count = len(values) 

1001 # figure out if data fits into the entry 

1002 if len(data) <= fmt_size: 

1003 entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) 

1004 else: 

1005 entries.append((tag, typ, count, self._pack(fmt, offset), data)) 

1006 offset += (len(data) + 1) // 2 * 2 # pad to word 

1007 

1008 # update strip offset data to point beyond auxiliary data 

1009 if stripoffsets is not None: 

1010 tag, typ, count, value, data = entries[stripoffsets] 

1011 if data: 

1012 size, handler = self._load_dispatch[typ] 

1013 values = [val + offset for val in handler(self, data, self.legacy_api)] 

1014 data = self._write_dispatch[typ](self, *values) 

1015 else: 

1016 value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) 

1017 entries[stripoffsets] = tag, typ, count, value, data 

1018 

1019 # pass 2: write entries to file 

1020 for tag, typ, count, value, data in entries: 

1021 logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) 

1022 result += self._pack( 

1023 "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value 

1024 ) 

1025 

1026 # -- overwrite here for multi-page -- 

1027 result += b"\0\0\0\0" # end of entries 

1028 

1029 # pass 3: write auxiliary data to file 

1030 for tag, typ, count, value, data in entries: 

1031 result += data 

1032 if len(data) & 1: 

1033 result += b"\0" 

1034 

1035 return result 

1036 

1037 def save(self, fp: IO[bytes]) -> int: 

1038 if fp.tell() == 0: # skip TIFF header on subsequent pages 

1039 fp.write(self._get_ifh()) 

1040 

1041 offset = fp.tell() 

1042 result = self.tobytes(offset) 

1043 fp.write(result) 

1044 return offset + len(result) 

1045 

1046 

1047ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1048ImageFileDirectory_v2._write_dispatch = _write_dispatch 

1049for idx, name in TYPES.items(): 

1050 name = name.replace(" ", "_") 

1051 setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) 

1052 setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) 

1053del _load_dispatch, _write_dispatch, idx, name 

1054 

1055 

1056# Legacy ImageFileDirectory support. 

1057class ImageFileDirectory_v1(ImageFileDirectory_v2): 

1058 """This class represents the **legacy** interface to a TIFF tag directory. 

1059 

1060 Exposes a dictionary interface of the tags in the directory:: 

1061 

1062 ifd = ImageFileDirectory_v1() 

1063 ifd[key] = 'Some Data' 

1064 ifd.tagtype[key] = TiffTags.ASCII 

1065 print(ifd[key]) 

1066 ('Some Data',) 

1067 

1068 Also contains a dictionary of tag types as read from the tiff image file, 

1069 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. 

1070 

1071 Values are returned as a tuple. 

1072 

1073 .. deprecated:: 3.0.0 

1074 """ 

1075 

1076 def __init__(self, *args: Any, **kwargs: Any) -> None: 

1077 super().__init__(*args, **kwargs) 

1078 self._legacy_api = True 

1079 

1080 tags = property(lambda self: self._tags_v1) 

1081 tagdata = property(lambda self: self._tagdata) 

1082 

1083 # defined in ImageFileDirectory_v2 

1084 tagtype: dict[int, int] 

1085 """Dictionary of tag types""" 

1086 

1087 @classmethod 

1088 def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: 

1089 """Returns an 

1090 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` 

1091 instance with the same data as is contained in the original 

1092 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` 

1093 instance. 

1094 

1095 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` 

1096 

1097 """ 

1098 

1099 ifd = cls(prefix=original.prefix) 

1100 ifd._tagdata = original._tagdata 

1101 ifd.tagtype = original.tagtype 

1102 ifd.next = original.next # an indicator for multipage tiffs 

1103 return ifd 

1104 

1105 def to_v2(self) -> ImageFileDirectory_v2: 

1106 """Returns an 

1107 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` 

1108 instance with the same data as is contained in the original 

1109 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` 

1110 instance. 

1111 

1112 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` 

1113 

1114 """ 

1115 

1116 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1117 ifd._tagdata = dict(self._tagdata) 

1118 ifd.tagtype = dict(self.tagtype) 

1119 ifd._tags_v2 = dict(self._tags_v2) 

1120 return ifd 

1121 

1122 def __contains__(self, tag: object) -> bool: 

1123 return tag in self._tags_v1 or tag in self._tagdata 

1124 

1125 def __len__(self) -> int: 

1126 return len(set(self._tagdata) | set(self._tags_v1)) 

1127 

1128 def __iter__(self) -> Iterator[int]: 

1129 return iter(set(self._tagdata) | set(self._tags_v1)) 

1130 

1131 def __setitem__(self, tag: int, value: Any) -> None: 

1132 for legacy_api in (False, True): 

1133 self._setitem(tag, value, legacy_api) 

1134 

1135 def __getitem__(self, tag: int) -> Any: 

1136 if tag not in self._tags_v1: # unpack on the fly 

1137 data = self._tagdata[tag] 

1138 typ = self.tagtype[tag] 

1139 size, handler = self._load_dispatch[typ] 

1140 for legacy in (False, True): 

1141 self._setitem(tag, handler(self, data, legacy), legacy) 

1142 val = self._tags_v1[tag] 

1143 if not isinstance(val, (tuple, bytes)): 

1144 val = (val,) 

1145 return val 

1146 

1147 

1148# undone -- switch this pointer 

1149ImageFileDirectory = ImageFileDirectory_v1 

1150 

1151 

1152## 

1153# Image plugin for TIFF files. 

1154 

1155 

1156class TiffImageFile(ImageFile.ImageFile): 

1157 format = "TIFF" 

1158 format_description = "Adobe TIFF" 

1159 _close_exclusive_fp_after_loading = False 

1160 

1161 def __init__( 

1162 self, 

1163 fp: StrOrBytesPath | IO[bytes], 

1164 filename: str | bytes | None = None, 

1165 ) -> None: 

1166 self.tag_v2: ImageFileDirectory_v2 

1167 """ Image file directory (tag dictionary) """ 

1168 

1169 self.tag: ImageFileDirectory_v1 

1170 """ Legacy tag entries """ 

1171 

1172 super().__init__(fp, filename) 

1173 

1174 def _open(self) -> None: 

1175 """Open the first image in a TIFF file""" 

1176 

1177 # Header 

1178 ifh = self.fp.read(8) 

1179 if ifh[2] == 43: 

1180 ifh += self.fp.read(8) 

1181 

1182 self.tag_v2 = ImageFileDirectory_v2(ifh) 

1183 

1184 # setup frame pointers 

1185 self.__first = self.__next = self.tag_v2.next 

1186 self.__frame = -1 

1187 self._fp = self.fp 

1188 self._frame_pos: list[int] = [] 

1189 self._n_frames: int | None = None 

1190 

1191 logger.debug("*** TiffImageFile._open ***") 

1192 logger.debug("- __first: %s", self.__first) 

1193 logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) 

1194 

1195 # and load the first frame 

1196 self._seek(0) 

1197 

1198 @property 

1199 def n_frames(self) -> int: 

1200 current_n_frames = self._n_frames 

1201 if current_n_frames is None: 

1202 current = self.tell() 

1203 self._seek(len(self._frame_pos)) 

1204 while self._n_frames is None: 

1205 self._seek(self.tell() + 1) 

1206 self.seek(current) 

1207 assert self._n_frames is not None 

1208 return self._n_frames 

1209 

1210 def seek(self, frame: int) -> None: 

1211 """Select a given frame as current image""" 

1212 if not self._seek_check(frame): 

1213 return 

1214 self._seek(frame) 

1215 if self._im is not None and ( 

1216 self.im.size != self._tile_size or self.im.mode != self.mode 

1217 ): 

1218 # The core image will no longer be used 

1219 self._im = None 

1220 

1221 def _seek(self, frame: int) -> None: 

1222 self.fp = self._fp 

1223 

1224 while len(self._frame_pos) <= frame: 

1225 if not self.__next: 

1226 msg = "no more images in TIFF file" 

1227 raise EOFError(msg) 

1228 logger.debug( 

1229 "Seeking to frame %s, on frame %s, __next %s, location: %s", 

1230 frame, 

1231 self.__frame, 

1232 self.__next, 

1233 self.fp.tell(), 

1234 ) 

1235 if self.__next >= 2**63: 

1236 msg = "Unable to seek to frame" 

1237 raise ValueError(msg) 

1238 self.fp.seek(self.__next) 

1239 self._frame_pos.append(self.__next) 

1240 logger.debug("Loading tags, location: %s", self.fp.tell()) 

1241 self.tag_v2.load(self.fp) 

1242 if self.tag_v2.next in self._frame_pos: 

1243 # This IFD has already been processed 

1244 # Declare this to be the end of the image 

1245 self.__next = 0 

1246 else: 

1247 self.__next = self.tag_v2.next 

1248 if self.__next == 0: 

1249 self._n_frames = frame + 1 

1250 if len(self._frame_pos) == 1: 

1251 self.is_animated = self.__next != 0 

1252 self.__frame += 1 

1253 self.fp.seek(self._frame_pos[frame]) 

1254 self.tag_v2.load(self.fp) 

1255 if XMP in self.tag_v2: 

1256 self.info["xmp"] = self.tag_v2[XMP] 

1257 elif "xmp" in self.info: 

1258 del self.info["xmp"] 

1259 self._reload_exif() 

1260 # fill the legacy tag/ifd entries 

1261 self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) 

1262 self.__frame = frame 

1263 self._setup() 

1264 

1265 def tell(self) -> int: 

1266 """Return the current frame number""" 

1267 return self.__frame 

1268 

1269 def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: 

1270 """ 

1271 Returns a dictionary of Photoshop "Image Resource Blocks". 

1272 The keys are the image resource ID. For more information, see 

1273 https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 

1274 

1275 :returns: Photoshop "Image Resource Blocks" in a dictionary. 

1276 """ 

1277 blocks = {} 

1278 val = self.tag_v2.get(ExifTags.Base.ImageResources) 

1279 if val: 

1280 while val[:4] == b"8BIM": 

1281 id = i16(val[4:6]) 

1282 n = math.ceil((val[6] + 1) / 2) * 2 

1283 size = i32(val[6 + n : 10 + n]) 

1284 data = val[10 + n : 10 + n + size] 

1285 blocks[id] = {"data": data} 

1286 

1287 val = val[math.ceil((10 + n + size) / 2) * 2 :] 

1288 return blocks 

1289 

1290 def load(self) -> Image.core.PixelAccess | None: 

1291 if self.tile and self.use_load_libtiff: 

1292 return self._load_libtiff() 

1293 return super().load() 

1294 

1295 def load_prepare(self) -> None: 

1296 if self._im is None: 

1297 Image._decompression_bomb_check(self._tile_size) 

1298 self.im = Image.core.new(self.mode, self._tile_size) 

1299 ImageFile.ImageFile.load_prepare(self) 

1300 

1301 def load_end(self) -> None: 

1302 # allow closing if we're on the first frame, there's no next 

1303 # This is the ImageFile.load path only, libtiff specific below. 

1304 if not self.is_animated: 

1305 self._close_exclusive_fp_after_loading = True 

1306 

1307 # load IFD data from fp before it is closed 

1308 exif = self.getexif() 

1309 for key in TiffTags.TAGS_V2_GROUPS: 

1310 if key not in exif: 

1311 continue 

1312 exif.get_ifd(key) 

1313 

1314 ImageOps.exif_transpose(self, in_place=True) 

1315 if ExifTags.Base.Orientation in self.tag_v2: 

1316 del self.tag_v2[ExifTags.Base.Orientation] 

1317 

1318 def _load_libtiff(self) -> Image.core.PixelAccess | None: 

1319 """Overload method triggered when we detect a compressed tiff 

1320 Calls out to libtiff""" 

1321 

1322 Image.Image.load(self) 

1323 

1324 self.load_prepare() 

1325 

1326 if not len(self.tile) == 1: 

1327 msg = "Not exactly one tile" 

1328 raise OSError(msg) 

1329 

1330 # (self._compression, (extents tuple), 

1331 # 0, (rawmode, self._compression, fp)) 

1332 extents = self.tile[0][1] 

1333 args = self.tile[0][3] 

1334 

1335 # To be nice on memory footprint, if there's a 

1336 # file descriptor, use that instead of reading 

1337 # into a string in python. 

1338 try: 

1339 fp = hasattr(self.fp, "fileno") and self.fp.fileno() 

1340 # flush the file descriptor, prevents error on pypy 2.4+ 

1341 # should also eliminate the need for fp.tell 

1342 # in _seek 

1343 if hasattr(self.fp, "flush"): 

1344 self.fp.flush() 

1345 except OSError: 

1346 # io.BytesIO have a fileno, but returns an OSError if 

1347 # it doesn't use a file descriptor. 

1348 fp = False 

1349 

1350 if fp: 

1351 assert isinstance(args, tuple) 

1352 args_list = list(args) 

1353 args_list[2] = fp 

1354 args = tuple(args_list) 

1355 

1356 decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) 

1357 try: 

1358 decoder.setimage(self.im, extents) 

1359 except ValueError as e: 

1360 msg = "Couldn't set the image" 

1361 raise OSError(msg) from e 

1362 

1363 close_self_fp = self._exclusive_fp and not self.is_animated 

1364 if hasattr(self.fp, "getvalue"): 

1365 # We've got a stringio like thing passed in. Yay for all in memory. 

1366 # The decoder needs the entire file in one shot, so there's not 

1367 # a lot we can do here other than give it the entire file. 

1368 # unless we could do something like get the address of the 

1369 # underlying string for stringio. 

1370 # 

1371 # Rearranging for supporting byteio items, since they have a fileno 

1372 # that returns an OSError if there's no underlying fp. Easier to 

1373 # deal with here by reordering. 

1374 logger.debug("have getvalue. just sending in a string from getvalue") 

1375 n, err = decoder.decode(self.fp.getvalue()) 

1376 elif fp: 

1377 # we've got a actual file on disk, pass in the fp. 

1378 logger.debug("have fileno, calling fileno version of the decoder.") 

1379 if not close_self_fp: 

1380 self.fp.seek(0) 

1381 # Save and restore the file position, because libtiff will move it 

1382 # outside of the Python runtime, and that will confuse 

1383 # io.BufferedReader and possible others. 

1384 # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), 

1385 # because the buffer read head already may not equal the actual 

1386 # file position, and fp.seek() may just adjust it's internal 

1387 # pointer and not actually seek the OS file handle. 

1388 pos = os.lseek(fp, 0, os.SEEK_CUR) 

1389 # 4 bytes, otherwise the trace might error out 

1390 n, err = decoder.decode(b"fpfp") 

1391 os.lseek(fp, pos, os.SEEK_SET) 

1392 else: 

1393 # we have something else. 

1394 logger.debug("don't have fileno or getvalue. just reading") 

1395 self.fp.seek(0) 

1396 # UNDONE -- so much for that buffer size thing. 

1397 n, err = decoder.decode(self.fp.read()) 

1398 

1399 self.tile = [] 

1400 self.readonly = 0 

1401 

1402 self.load_end() 

1403 

1404 if close_self_fp: 

1405 self.fp.close() 

1406 self.fp = None # might be shared 

1407 

1408 if err < 0: 

1409 raise OSError(err) 

1410 

1411 return Image.Image.load(self) 

1412 

1413 def _setup(self) -> None: 

1414 """Setup this image object based on current tags""" 

1415 

1416 if 0xBC01 in self.tag_v2: 

1417 msg = "Windows Media Photo files not yet supported" 

1418 raise OSError(msg) 

1419 

1420 # extract relevant tags 

1421 self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] 

1422 self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) 

1423 

1424 # photometric is a required tag, but not everyone is reading 

1425 # the specification 

1426 photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) 

1427 

1428 # old style jpeg compression images most certainly are YCbCr 

1429 if self._compression == "tiff_jpeg": 

1430 photo = 6 

1431 

1432 fillorder = self.tag_v2.get(FILLORDER, 1) 

1433 

1434 logger.debug("*** Summary ***") 

1435 logger.debug("- compression: %s", self._compression) 

1436 logger.debug("- photometric_interpretation: %s", photo) 

1437 logger.debug("- planar_configuration: %s", self._planar_configuration) 

1438 logger.debug("- fill_order: %s", fillorder) 

1439 logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) 

1440 

1441 # size 

1442 try: 

1443 xsize = self.tag_v2[IMAGEWIDTH] 

1444 ysize = self.tag_v2[IMAGELENGTH] 

1445 except KeyError as e: 

1446 msg = "Missing dimensions" 

1447 raise TypeError(msg) from e 

1448 if not isinstance(xsize, int) or not isinstance(ysize, int): 

1449 msg = "Invalid dimensions" 

1450 raise ValueError(msg) 

1451 self._tile_size = xsize, ysize 

1452 orientation = self.tag_v2.get(ExifTags.Base.Orientation) 

1453 if orientation in (5, 6, 7, 8): 

1454 self._size = ysize, xsize 

1455 else: 

1456 self._size = xsize, ysize 

1457 

1458 logger.debug("- size: %s", self.size) 

1459 

1460 sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) 

1461 if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: 

1462 # SAMPLEFORMAT is properly per band, so an RGB image will 

1463 # be (1,1,1). But, we don't support per band pixel types, 

1464 # and anything more than one band is a uint8. So, just 

1465 # take the first element. Revisit this if adding support 

1466 # for more exotic images. 

1467 sample_format = (1,) 

1468 

1469 bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) 

1470 extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) 

1471 if photo in (2, 6, 8): # RGB, YCbCr, LAB 

1472 bps_count = 3 

1473 elif photo == 5: # CMYK 

1474 bps_count = 4 

1475 else: 

1476 bps_count = 1 

1477 bps_count += len(extra_tuple) 

1478 bps_actual_count = len(bps_tuple) 

1479 samples_per_pixel = self.tag_v2.get( 

1480 SAMPLESPERPIXEL, 

1481 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, 

1482 ) 

1483 

1484 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

1485 # DOS check, samples_per_pixel can be a Long, and we extend the tuple below 

1486 logger.error( 

1487 "More samples per pixel than can be decoded: %s", samples_per_pixel 

1488 ) 

1489 msg = "Invalid value for samples per pixel" 

1490 raise SyntaxError(msg) 

1491 

1492 if samples_per_pixel < bps_actual_count: 

1493 # If a file has more values in bps_tuple than expected, 

1494 # remove the excess. 

1495 bps_tuple = bps_tuple[:samples_per_pixel] 

1496 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

1497 # If a file has only one value in bps_tuple, when it should have more, 

1498 # presume it is the same number of bits for all of the samples. 

1499 bps_tuple = bps_tuple * samples_per_pixel 

1500 

1501 if len(bps_tuple) != samples_per_pixel: 

1502 msg = "unknown data organization" 

1503 raise SyntaxError(msg) 

1504 

1505 # mode: check photometric interpretation and bits per pixel 

1506 key = ( 

1507 self.tag_v2.prefix, 

1508 photo, 

1509 sample_format, 

1510 fillorder, 

1511 bps_tuple, 

1512 extra_tuple, 

1513 ) 

1514 logger.debug("format key: %s", key) 

1515 try: 

1516 self._mode, rawmode = OPEN_INFO[key] 

1517 except KeyError as e: 

1518 logger.debug("- unsupported format") 

1519 msg = "unknown pixel mode" 

1520 raise SyntaxError(msg) from e 

1521 

1522 logger.debug("- raw mode: %s", rawmode) 

1523 logger.debug("- pil mode: %s", self.mode) 

1524 

1525 self.info["compression"] = self._compression 

1526 

1527 xres = self.tag_v2.get(X_RESOLUTION, 1) 

1528 yres = self.tag_v2.get(Y_RESOLUTION, 1) 

1529 

1530 if xres and yres: 

1531 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1532 if resunit == 2: # dots per inch 

1533 self.info["dpi"] = (xres, yres) 

1534 elif resunit == 3: # dots per centimeter. convert to dpi 

1535 self.info["dpi"] = (xres * 2.54, yres * 2.54) 

1536 elif resunit is None: # used to default to 1, but now 2) 

1537 self.info["dpi"] = (xres, yres) 

1538 # For backward compatibility, 

1539 # we also preserve the old behavior 

1540 self.info["resolution"] = xres, yres 

1541 else: # No absolute unit of measurement 

1542 self.info["resolution"] = xres, yres 

1543 

1544 # build tile descriptors 

1545 x = y = layer = 0 

1546 self.tile = [] 

1547 self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" 

1548 if self.use_load_libtiff: 

1549 # Decoder expects entire file as one tile. 

1550 # There's a buffer size limit in load (64k) 

1551 # so large g4 images will fail if we use that 

1552 # function. 

1553 # 

1554 # Setup the one tile for the whole image, then 

1555 # use the _load_libtiff function. 

1556 

1557 # libtiff handles the fillmode for us, so 1;IR should 

1558 # actually be 1;I. Including the R double reverses the 

1559 # bits, so stripes of the image are reversed. See 

1560 # https://github.com/python-pillow/Pillow/issues/279 

1561 if fillorder == 2: 

1562 # Replace fillorder with fillorder=1 

1563 key = key[:3] + (1,) + key[4:] 

1564 logger.debug("format key: %s", key) 

1565 # this should always work, since all the 

1566 # fillorder==2 modes have a corresponding 

1567 # fillorder=1 mode 

1568 self._mode, rawmode = OPEN_INFO[key] 

1569 # YCbCr images with new jpeg compression with pixels in one plane 

1570 # unpacked straight into RGB values 

1571 if ( 

1572 photo == 6 

1573 and self._compression == "jpeg" 

1574 and self._planar_configuration == 1 

1575 ): 

1576 rawmode = "RGB" 

1577 # libtiff always returns the bytes in native order. 

1578 # we're expecting image byte order. So, if the rawmode 

1579 # contains I;16, we need to convert from native to image 

1580 # byte order. 

1581 elif rawmode == "I;16": 

1582 rawmode = "I;16N" 

1583 elif rawmode.endswith(";16B") or rawmode.endswith(";16L"): 

1584 rawmode = rawmode[:-1] + "N" 

1585 

1586 # Offset in the tile tuple is 0, we go from 0,0 to 

1587 # w,h, and we only do this once -- eds 

1588 a = (rawmode, self._compression, False, self.tag_v2.offset) 

1589 self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) 

1590 

1591 elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: 

1592 # striped image 

1593 if STRIPOFFSETS in self.tag_v2: 

1594 offsets = self.tag_v2[STRIPOFFSETS] 

1595 h = self.tag_v2.get(ROWSPERSTRIP, ysize) 

1596 w = xsize 

1597 else: 

1598 # tiled image 

1599 offsets = self.tag_v2[TILEOFFSETS] 

1600 tilewidth = self.tag_v2.get(TILEWIDTH) 

1601 h = self.tag_v2.get(TILELENGTH) 

1602 if not isinstance(tilewidth, int) or not isinstance(h, int): 

1603 msg = "Invalid tile dimensions" 

1604 raise ValueError(msg) 

1605 w = tilewidth 

1606 

1607 for offset in offsets: 

1608 if x + w > xsize: 

1609 stride = w * sum(bps_tuple) / 8 # bytes per line 

1610 else: 

1611 stride = 0 

1612 

1613 tile_rawmode = rawmode 

1614 if self._planar_configuration == 2: 

1615 # each band on it's own layer 

1616 tile_rawmode = rawmode[layer] 

1617 # adjust stride width accordingly 

1618 stride /= bps_count 

1619 

1620 args = (tile_rawmode, int(stride), 1) 

1621 self.tile.append( 

1622 ImageFile._Tile( 

1623 self._compression, 

1624 (x, y, min(x + w, xsize), min(y + h, ysize)), 

1625 offset, 

1626 args, 

1627 ) 

1628 ) 

1629 x = x + w 

1630 if x >= xsize: 

1631 x, y = 0, y + h 

1632 if y >= ysize: 

1633 x = y = 0 

1634 layer += 1 

1635 else: 

1636 logger.debug("- unsupported data organization") 

1637 msg = "unknown data organization" 

1638 raise SyntaxError(msg) 

1639 

1640 # Fix up info. 

1641 if ICCPROFILE in self.tag_v2: 

1642 self.info["icc_profile"] = self.tag_v2[ICCPROFILE] 

1643 

1644 # fixup palette descriptor 

1645 

1646 if self.mode in ["P", "PA"]: 

1647 palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] 

1648 self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) 

1649 

1650 

1651# 

1652# -------------------------------------------------------------------- 

1653# Write TIFF files 

1654 

1655# little endian is default except for image modes with 

1656# explicit big endian byte-order 

1657 

1658SAVE_INFO = { 

1659 # mode => rawmode, byteorder, photometrics, 

1660 # sampleformat, bitspersample, extra 

1661 "1": ("1", II, 1, 1, (1,), None), 

1662 "L": ("L", II, 1, 1, (8,), None), 

1663 "LA": ("LA", II, 1, 1, (8, 8), 2), 

1664 "P": ("P", II, 3, 1, (8,), None), 

1665 "PA": ("PA", II, 3, 1, (8, 8), 2), 

1666 "I": ("I;32S", II, 1, 2, (32,), None), 

1667 "I;16": ("I;16", II, 1, 1, (16,), None), 

1668 "I;16S": ("I;16S", II, 1, 2, (16,), None), 

1669 "F": ("F;32F", II, 1, 3, (32,), None), 

1670 "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), 

1671 "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), 

1672 "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), 

1673 "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), 

1674 "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), 

1675 "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), 

1676 "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), 

1677 "I;16B": ("I;16B", MM, 1, 1, (16,), None), 

1678 "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), 

1679 "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), 

1680} 

1681 

1682 

1683def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

1684 try: 

1685 rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] 

1686 except KeyError as e: 

1687 msg = f"cannot write mode {im.mode} as TIFF" 

1688 raise OSError(msg) from e 

1689 

1690 encoderinfo = im.encoderinfo 

1691 encoderconfig = im.encoderconfig 

1692 

1693 ifd = ImageFileDirectory_v2(prefix=prefix) 

1694 if encoderinfo.get("big_tiff"): 

1695 ifd._bigtiff = True 

1696 

1697 try: 

1698 compression = encoderinfo["compression"] 

1699 except KeyError: 

1700 compression = im.info.get("compression") 

1701 if isinstance(compression, int): 

1702 # compression value may be from BMP. Ignore it 

1703 compression = None 

1704 if compression is None: 

1705 compression = "raw" 

1706 elif compression == "tiff_jpeg": 

1707 # OJPEG is obsolete, so use new-style JPEG compression instead 

1708 compression = "jpeg" 

1709 elif compression == "tiff_deflate": 

1710 compression = "tiff_adobe_deflate" 

1711 

1712 libtiff = WRITE_LIBTIFF or compression != "raw" 

1713 

1714 # required for color libtiff images 

1715 ifd[PLANAR_CONFIGURATION] = 1 

1716 

1717 ifd[IMAGEWIDTH] = im.size[0] 

1718 ifd[IMAGELENGTH] = im.size[1] 

1719 

1720 # write any arbitrary tags passed in as an ImageFileDirectory 

1721 if "tiffinfo" in encoderinfo: 

1722 info = encoderinfo["tiffinfo"] 

1723 elif "exif" in encoderinfo: 

1724 info = encoderinfo["exif"] 

1725 if isinstance(info, bytes): 

1726 exif = Image.Exif() 

1727 exif.load(info) 

1728 info = exif 

1729 else: 

1730 info = {} 

1731 logger.debug("Tiffinfo Keys: %s", list(info)) 

1732 if isinstance(info, ImageFileDirectory_v1): 

1733 info = info.to_v2() 

1734 for key in info: 

1735 if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: 

1736 ifd[key] = info.get_ifd(key) 

1737 else: 

1738 ifd[key] = info.get(key) 

1739 try: 

1740 ifd.tagtype[key] = info.tagtype[key] 

1741 except Exception: 

1742 pass # might not be an IFD. Might not have populated type 

1743 

1744 legacy_ifd = {} 

1745 if hasattr(im, "tag"): 

1746 legacy_ifd = im.tag.to_v2() 

1747 

1748 supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} 

1749 for tag in ( 

1750 # IFD offset that may not be correct in the saved image 

1751 EXIFIFD, 

1752 # Determined by the image format and should not be copied from legacy_ifd. 

1753 SAMPLEFORMAT, 

1754 ): 

1755 if tag in supplied_tags: 

1756 del supplied_tags[tag] 

1757 

1758 # additions written by Greg Couch, gregc@cgl.ucsf.edu 

1759 # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com 

1760 if hasattr(im, "tag_v2"): 

1761 # preserve tags from original TIFF image file 

1762 for key in ( 

1763 RESOLUTION_UNIT, 

1764 X_RESOLUTION, 

1765 Y_RESOLUTION, 

1766 IPTC_NAA_CHUNK, 

1767 PHOTOSHOP_CHUNK, 

1768 XMP, 

1769 ): 

1770 if key in im.tag_v2: 

1771 if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( 

1772 TiffTags.BYTE, 

1773 TiffTags.UNDEFINED, 

1774 ): 

1775 del supplied_tags[key] 

1776 else: 

1777 ifd[key] = im.tag_v2[key] 

1778 ifd.tagtype[key] = im.tag_v2.tagtype[key] 

1779 

1780 # preserve ICC profile (should also work when saving other formats 

1781 # which support profiles as TIFF) -- 2008-06-06 Florian Hoech 

1782 icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) 

1783 if icc: 

1784 ifd[ICCPROFILE] = icc 

1785 

1786 for key, name in [ 

1787 (IMAGEDESCRIPTION, "description"), 

1788 (X_RESOLUTION, "resolution"), 

1789 (Y_RESOLUTION, "resolution"), 

1790 (X_RESOLUTION, "x_resolution"), 

1791 (Y_RESOLUTION, "y_resolution"), 

1792 (RESOLUTION_UNIT, "resolution_unit"), 

1793 (SOFTWARE, "software"), 

1794 (DATE_TIME, "date_time"), 

1795 (ARTIST, "artist"), 

1796 (COPYRIGHT, "copyright"), 

1797 ]: 

1798 if name in encoderinfo: 

1799 ifd[key] = encoderinfo[name] 

1800 

1801 dpi = encoderinfo.get("dpi") 

1802 if dpi: 

1803 ifd[RESOLUTION_UNIT] = 2 

1804 ifd[X_RESOLUTION] = dpi[0] 

1805 ifd[Y_RESOLUTION] = dpi[1] 

1806 

1807 if bits != (1,): 

1808 ifd[BITSPERSAMPLE] = bits 

1809 if len(bits) != 1: 

1810 ifd[SAMPLESPERPIXEL] = len(bits) 

1811 if extra is not None: 

1812 ifd[EXTRASAMPLES] = extra 

1813 if format != 1: 

1814 ifd[SAMPLEFORMAT] = format 

1815 

1816 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1817 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

1818 elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: 

1819 if im.mode == "1": 

1820 inverted_im = im.copy() 

1821 px = inverted_im.load() 

1822 if px is not None: 

1823 for y in range(inverted_im.height): 

1824 for x in range(inverted_im.width): 

1825 px[x, y] = 0 if px[x, y] == 255 else 255 

1826 im = inverted_im 

1827 else: 

1828 im = ImageOps.invert(im) 

1829 

1830 if im.mode in ["P", "PA"]: 

1831 lut = im.im.getpalette("RGB", "RGB;L") 

1832 colormap = [] 

1833 colors = len(lut) // 3 

1834 for i in range(3): 

1835 colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] 

1836 colormap += [0] * (256 - colors) 

1837 ifd[COLORMAP] = colormap 

1838 # data orientation 

1839 w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] 

1840 stride = len(bits) * ((w * bits[0] + 7) // 8) 

1841 if ROWSPERSTRIP not in ifd: 

1842 # aim for given strip size (64 KB by default) when using libtiff writer 

1843 if libtiff: 

1844 im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) 

1845 rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) 

1846 # JPEG encoder expects multiple of 8 rows 

1847 if compression == "jpeg": 

1848 rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) 

1849 else: 

1850 rows_per_strip = h 

1851 if rows_per_strip == 0: 

1852 rows_per_strip = 1 

1853 ifd[ROWSPERSTRIP] = rows_per_strip 

1854 strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] 

1855 strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] 

1856 if strip_byte_counts >= 2**16: 

1857 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

1858 ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( 

1859 stride * h - strip_byte_counts * (strips_per_image - 1), 

1860 ) 

1861 ifd[STRIPOFFSETS] = tuple( 

1862 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1863 ) # this is adjusted by IFD writer 

1864 # no compression by default: 

1865 ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) 

1866 

1867 if im.mode == "YCbCr": 

1868 for tag, default_value in { 

1869 YCBCRSUBSAMPLING: (1, 1), 

1870 REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), 

1871 }.items(): 

1872 ifd.setdefault(tag, default_value) 

1873 

1874 blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] 

1875 if libtiff: 

1876 if "quality" in encoderinfo: 

1877 quality = encoderinfo["quality"] 

1878 if not isinstance(quality, int) or quality < 0 or quality > 100: 

1879 msg = "Invalid quality setting" 

1880 raise ValueError(msg) 

1881 if compression != "jpeg": 

1882 msg = "quality setting only supported for 'jpeg' compression" 

1883 raise ValueError(msg) 

1884 ifd[JPEGQUALITY] = quality 

1885 

1886 logger.debug("Saving using libtiff encoder") 

1887 logger.debug("Items: %s", sorted(ifd.items())) 

1888 _fp = 0 

1889 if hasattr(fp, "fileno"): 

1890 try: 

1891 fp.seek(0) 

1892 _fp = fp.fileno() 

1893 except io.UnsupportedOperation: 

1894 pass 

1895 

1896 # optional types for non core tags 

1897 types = {} 

1898 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1899 # based on the data in the strip. 

1900 # OSUBFILETYPE is deprecated. 

1901 # The other tags expect arrays with a certain length (fixed or depending on 

1902 # BITSPERSAMPLE, etc), passing arrays with a different length will result in 

1903 # segfaults. Block these tags until we add extra validation. 

1904 # SUBIFD may also cause a segfault. 

1905 blocklist += [ 

1906 OSUBFILETYPE, 

1907 REFERENCEBLACKWHITE, 

1908 STRIPBYTECOUNTS, 

1909 STRIPOFFSETS, 

1910 TRANSFERFUNCTION, 

1911 SUBIFD, 

1912 ] 

1913 

1914 # bits per sample is a single short in the tiff directory, not a list. 

1915 atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} 

1916 # Merge the ones that we have with (optional) more bits from 

1917 # the original file, e.g x,y resolution so that we can 

1918 # save(load('')) == original file. 

1919 for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): 

1920 # Libtiff can only process certain core items without adding 

1921 # them to the custom dictionary. 

1922 # Custom items are supported for int, float, unicode, string and byte 

1923 # values. Other types and tuples require a tagtype. 

1924 if tag not in TiffTags.LIBTIFF_CORE: 

1925 if not getattr(Image.core, "libtiff_support_custom_tags", False): 

1926 continue 

1927 

1928 if tag in TiffTags.TAGS_V2_GROUPS: 

1929 types[tag] = TiffTags.LONG8 

1930 elif tag in ifd.tagtype: 

1931 types[tag] = ifd.tagtype[tag] 

1932 elif not (isinstance(value, (int, float, str, bytes))): 

1933 continue 

1934 else: 

1935 type = TiffTags.lookup(tag).type 

1936 if type: 

1937 types[tag] = type 

1938 if tag not in atts and tag not in blocklist: 

1939 if isinstance(value, str): 

1940 atts[tag] = value.encode("ascii", "replace") + b"\0" 

1941 elif isinstance(value, IFDRational): 

1942 atts[tag] = float(value) 

1943 else: 

1944 atts[tag] = value 

1945 

1946 if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: 

1947 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1948 

1949 logger.debug("Converted items: %s", sorted(atts.items())) 

1950 

1951 # libtiff always expects the bytes in native order. 

1952 # we're storing image byte order. So, if the rawmode 

1953 # contains I;16, we need to convert from native to image 

1954 # byte order. 

1955 if im.mode in ("I;16B", "I;16"): 

1956 rawmode = "I;16N" 

1957 

1958 # Pass tags as sorted list so that the tags are set in a fixed order. 

1959 # This is required by libtiff for some tags. For example, the JPEGQUALITY 

1960 # pseudo tag requires that the COMPRESS tag was already set. 

1961 tags = list(atts.items()) 

1962 tags.sort() 

1963 a = (rawmode, compression, _fp, filename, tags, types) 

1964 encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) 

1965 encoder.setimage(im.im, (0, 0) + im.size) 

1966 while True: 

1967 errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] 

1968 if not _fp: 

1969 fp.write(data) 

1970 if errcode: 

1971 break 

1972 if errcode < 0: 

1973 msg = f"encoder error {errcode} when writing image file" 

1974 raise OSError(msg) 

1975 

1976 else: 

1977 for tag in blocklist: 

1978 del ifd[tag] 

1979 offset = ifd.save(fp) 

1980 

1981 ImageFile._save( 

1982 im, 

1983 fp, 

1984 [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], 

1985 ) 

1986 

1987 # -- helper for multi-page save -- 

1988 if "_debug_multipage" in encoderinfo: 

1989 # just to access o32 and o16 (using correct byte order) 

1990 setattr(im, "_debug_multipage", ifd) 

1991 

1992 

1993class AppendingTiffWriter(io.BytesIO): 

1994 fieldSizes = [ 

1995 0, # None 

1996 1, # byte 

1997 1, # ascii 

1998 2, # short 

1999 4, # long 

2000 8, # rational 

2001 1, # sbyte 

2002 1, # undefined 

2003 2, # sshort 

2004 4, # slong 

2005 8, # srational 

2006 4, # float 

2007 8, # double 

2008 4, # ifd 

2009 2, # unicode 

2010 4, # complex 

2011 8, # long8 

2012 ] 

2013 

2014 Tags = { 

2015 273, # StripOffsets 

2016 288, # FreeOffsets 

2017 324, # TileOffsets 

2018 519, # JPEGQTables 

2019 520, # JPEGDCTables 

2020 521, # JPEGACTables 

2021 } 

2022 

2023 def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: 

2024 self.f: IO[bytes] 

2025 if is_path(fn): 

2026 self.name = fn 

2027 self.close_fp = True 

2028 try: 

2029 self.f = open(fn, "w+b" if new else "r+b") 

2030 except OSError: 

2031 self.f = open(fn, "w+b") 

2032 else: 

2033 self.f = cast(IO[bytes], fn) 

2034 self.close_fp = False 

2035 self.beginning = self.f.tell() 

2036 self.setup() 

2037 

2038 def setup(self) -> None: 

2039 # Reset everything. 

2040 self.f.seek(self.beginning, os.SEEK_SET) 

2041 

2042 self.whereToWriteNewIFDOffset: int | None = None 

2043 self.offsetOfNewPage = 0 

2044 

2045 self.IIMM = iimm = self.f.read(4) 

2046 if not iimm: 

2047 # empty file - first page 

2048 self.isFirst = True 

2049 return 

2050 

2051 self.isFirst = False 

2052 if iimm == b"II\x2a\x00": 

2053 self.setEndian("<") 

2054 elif iimm == b"MM\x00\x2a": 

2055 self.setEndian(">") 

2056 else: 

2057 msg = "Invalid TIFF file header" 

2058 raise RuntimeError(msg) 

2059 

2060 self.skipIFDs() 

2061 self.goToEnd() 

2062 

2063 def finalize(self) -> None: 

2064 if self.isFirst: 

2065 return 

2066 

2067 # fix offsets 

2068 self.f.seek(self.offsetOfNewPage) 

2069 

2070 iimm = self.f.read(4) 

2071 if not iimm: 

2072 # Make it easy to finish a frame without committing to a new one. 

2073 return 

2074 

2075 if iimm != self.IIMM: 

2076 msg = "IIMM of new page doesn't match IIMM of first page" 

2077 raise RuntimeError(msg) 

2078 

2079 ifd_offset = self.readLong() 

2080 ifd_offset += self.offsetOfNewPage 

2081 assert self.whereToWriteNewIFDOffset is not None 

2082 self.f.seek(self.whereToWriteNewIFDOffset) 

2083 self.writeLong(ifd_offset) 

2084 self.f.seek(ifd_offset) 

2085 self.fixIFD() 

2086 

2087 def newFrame(self) -> None: 

2088 # Call this to finish a frame. 

2089 self.finalize() 

2090 self.setup() 

2091 

2092 def __enter__(self) -> AppendingTiffWriter: 

2093 return self 

2094 

2095 def __exit__(self, *args: object) -> None: 

2096 if self.close_fp: 

2097 self.close() 

2098 

2099 def tell(self) -> int: 

2100 return self.f.tell() - self.offsetOfNewPage 

2101 

2102 def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: 

2103 """ 

2104 :param offset: Distance to seek. 

2105 :param whence: Whether the distance is relative to the start, 

2106 end or current position. 

2107 :returns: The resulting position, relative to the start. 

2108 """ 

2109 if whence == os.SEEK_SET: 

2110 offset += self.offsetOfNewPage 

2111 

2112 self.f.seek(offset, whence) 

2113 return self.tell() 

2114 

2115 def goToEnd(self) -> None: 

2116 self.f.seek(0, os.SEEK_END) 

2117 pos = self.f.tell() 

2118 

2119 # pad to 16 byte boundary 

2120 pad_bytes = 16 - pos % 16 

2121 if 0 < pad_bytes < 16: 

2122 self.f.write(bytes(pad_bytes)) 

2123 self.offsetOfNewPage = self.f.tell() 

2124 

2125 def setEndian(self, endian: str) -> None: 

2126 self.endian = endian 

2127 self.longFmt = f"{self.endian}L" 

2128 self.shortFmt = f"{self.endian}H" 

2129 self.tagFormat = f"{self.endian}HHL" 

2130 

2131 def skipIFDs(self) -> None: 

2132 while True: 

2133 ifd_offset = self.readLong() 

2134 if ifd_offset == 0: 

2135 self.whereToWriteNewIFDOffset = self.f.tell() - 4 

2136 break 

2137 

2138 self.f.seek(ifd_offset) 

2139 num_tags = self.readShort() 

2140 self.f.seek(num_tags * 12, os.SEEK_CUR) 

2141 

2142 def write(self, data: Buffer, /) -> int: 

2143 return self.f.write(data) 

2144 

2145 def _fmt(self, field_size: int) -> str: 

2146 try: 

2147 return {2: "H", 4: "L", 8: "Q"}[field_size] 

2148 except KeyError: 

2149 msg = "offset is not supported" 

2150 raise RuntimeError(msg) 

2151 

2152 def _read(self, field_size: int) -> int: 

2153 (value,) = struct.unpack( 

2154 self.endian + self._fmt(field_size), self.f.read(field_size) 

2155 ) 

2156 return value 

2157 

2158 def readShort(self) -> int: 

2159 return self._read(2) 

2160 

2161 def readLong(self) -> int: 

2162 return self._read(4) 

2163 

2164 @staticmethod 

2165 def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: 

2166 if bytes_written is not None and bytes_written != expected: 

2167 msg = f"wrote only {bytes_written} bytes but wanted {expected}" 

2168 raise RuntimeError(msg) 

2169 

2170 def rewriteLastShortToLong(self, value: int) -> None: 

2171 self.f.seek(-2, os.SEEK_CUR) 

2172 bytes_written = self.f.write(struct.pack(self.longFmt, value)) 

2173 self._verify_bytes_written(bytes_written, 4) 

2174 

2175 def _rewriteLast(self, value: int, field_size: int) -> None: 

2176 self.f.seek(-field_size, os.SEEK_CUR) 

2177 bytes_written = self.f.write( 

2178 struct.pack(self.endian + self._fmt(field_size), value) 

2179 ) 

2180 self._verify_bytes_written(bytes_written, field_size) 

2181 

2182 def rewriteLastShort(self, value: int) -> None: 

2183 return self._rewriteLast(value, 2) 

2184 

2185 def rewriteLastLong(self, value: int) -> None: 

2186 return self._rewriteLast(value, 4) 

2187 

2188 def writeShort(self, value: int) -> None: 

2189 bytes_written = self.f.write(struct.pack(self.shortFmt, value)) 

2190 self._verify_bytes_written(bytes_written, 2) 

2191 

2192 def writeLong(self, value: int) -> None: 

2193 bytes_written = self.f.write(struct.pack(self.longFmt, value)) 

2194 self._verify_bytes_written(bytes_written, 4) 

2195 

2196 def close(self) -> None: 

2197 self.finalize() 

2198 if self.close_fp: 

2199 self.f.close() 

2200 

2201 def fixIFD(self) -> None: 

2202 num_tags = self.readShort() 

2203 

2204 for i in range(num_tags): 

2205 tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) 

2206 

2207 field_size = self.fieldSizes[field_type] 

2208 total_size = field_size * count 

2209 is_local = total_size <= 4 

2210 if not is_local: 

2211 offset = self.readLong() + self.offsetOfNewPage 

2212 self.rewriteLastLong(offset) 

2213 

2214 if tag in self.Tags: 

2215 cur_pos = self.f.tell() 

2216 

2217 if is_local: 

2218 self._fixOffsets(count, field_size) 

2219 self.f.seek(cur_pos + 4) 

2220 else: 

2221 self.f.seek(offset) 

2222 self._fixOffsets(count, field_size) 

2223 self.f.seek(cur_pos) 

2224 

2225 elif is_local: 

2226 # skip the locally stored value that is not an offset 

2227 self.f.seek(4, os.SEEK_CUR) 

2228 

2229 def _fixOffsets(self, count: int, field_size: int) -> None: 

2230 for i in range(count): 

2231 offset = self._read(field_size) 

2232 offset += self.offsetOfNewPage 

2233 if field_size == 2 and offset >= 65536: 

2234 # offset is now too large - we must convert shorts to longs 

2235 if count != 1: 

2236 msg = "not implemented" 

2237 raise RuntimeError(msg) # XXX TODO 

2238 

2239 # simple case - the offset is just one and therefore it is 

2240 # local (not referenced with another offset) 

2241 self.rewriteLastShortToLong(offset) 

2242 self.f.seek(-10, os.SEEK_CUR) 

2243 self.writeShort(TiffTags.LONG) # rewrite the type to LONG 

2244 self.f.seek(8, os.SEEK_CUR) 

2245 else: 

2246 self._rewriteLast(offset, field_size) 

2247 

2248 def fixOffsets( 

2249 self, count: int, isShort: bool = False, isLong: bool = False 

2250 ) -> None: 

2251 if isShort: 

2252 field_size = 2 

2253 elif isLong: 

2254 field_size = 4 

2255 else: 

2256 field_size = 0 

2257 return self._fixOffsets(count, field_size) 

2258 

2259 

2260def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

2261 encoderinfo = im.encoderinfo.copy() 

2262 encoderconfig = im.encoderconfig 

2263 append_images = list(encoderinfo.get("append_images", [])) 

2264 if not hasattr(im, "n_frames") and not append_images: 

2265 return _save(im, fp, filename) 

2266 

2267 cur_idx = im.tell() 

2268 try: 

2269 with AppendingTiffWriter(fp) as tf: 

2270 for ims in [im] + append_images: 

2271 ims.encoderinfo = encoderinfo 

2272 ims.encoderconfig = encoderconfig 

2273 if not hasattr(ims, "n_frames"): 

2274 nfr = 1 

2275 else: 

2276 nfr = ims.n_frames 

2277 

2278 for idx in range(nfr): 

2279 ims.seek(idx) 

2280 ims.load() 

2281 _save(ims, tf, filename) 

2282 tf.newFrame() 

2283 finally: 

2284 im.seek(cur_idx) 

2285 

2286 

2287# 

2288# -------------------------------------------------------------------- 

2289# Register 

2290 

2291Image.register_open(TiffImageFile.format, TiffImageFile, _accept) 

2292Image.register_save(TiffImageFile.format, _save) 

2293Image.register_save_all(TiffImageFile.format, _save_all) 

2294 

2295Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) 

2296 

2297Image.register_mime(TiffImageFile.format, "image/tiff")