Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/TiffImagePlugin.py: 50%

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

1235 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, 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 DeferredError, is_path 

62from .TiffTags import TYPES 

63 

64TYPE_CHECKING = False 

65if TYPE_CHECKING: 

66 from ._typing import Buffer, IntegralLike 

67 

68logger = logging.getLogger(__name__) 

69 

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

71READ_LIBTIFF = False 

72WRITE_LIBTIFF = False 

73STRIP_SIZE = 65536 

74 

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

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

77 

78# 

79# -------------------------------------------------------------------- 

80# Read TIFF files 

81 

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

83OSUBFILETYPE = 255 

84IMAGEWIDTH = 256 

85IMAGELENGTH = 257 

86BITSPERSAMPLE = 258 

87COMPRESSION = 259 

88PHOTOMETRIC_INTERPRETATION = 262 

89FILLORDER = 266 

90IMAGEDESCRIPTION = 270 

91STRIPOFFSETS = 273 

92SAMPLESPERPIXEL = 277 

93ROWSPERSTRIP = 278 

94STRIPBYTECOUNTS = 279 

95X_RESOLUTION = 282 

96Y_RESOLUTION = 283 

97PLANAR_CONFIGURATION = 284 

98RESOLUTION_UNIT = 296 

99TRANSFERFUNCTION = 301 

100SOFTWARE = 305 

101DATE_TIME = 306 

102ARTIST = 315 

103PREDICTOR = 317 

104COLORMAP = 320 

105TILEWIDTH = 322 

106TILELENGTH = 323 

107TILEOFFSETS = 324 

108TILEBYTECOUNTS = 325 

109SUBIFD = 330 

110EXTRASAMPLES = 338 

111SAMPLEFORMAT = 339 

112JPEGTABLES = 347 

113YCBCRSUBSAMPLING = 530 

114REFERENCEBLACKWHITE = 532 

115COPYRIGHT = 33432 

116IPTC_NAA_CHUNK = 33723 # newsphoto properties 

117PHOTOSHOP_CHUNK = 34377 # photoshop properties 

118ICCPROFILE = 34675 

119EXIFIFD = 34665 

120XMP = 700 

121JPEGQUALITY = 65537 # pseudo-tag by libtiff 

122 

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

124IMAGEJ_META_DATA_BYTE_COUNTS = 50838 

125IMAGEJ_META_DATA = 50839 

126 

127COMPRESSION_INFO = { 

128 # Compression => pil compression name 

129 1: "raw", 

130 2: "tiff_ccitt", 

131 3: "group3", 

132 4: "group4", 

133 5: "tiff_lzw", 

134 6: "tiff_jpeg", # obsolete 

135 7: "jpeg", 

136 8: "tiff_adobe_deflate", 

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

138 32773: "packbits", 

139 32809: "tiff_thunderscan", 

140 32946: "tiff_deflate", 

141 34676: "tiff_sgilog", 

142 34677: "tiff_sgilog24", 

143 34925: "lzma", 

144 50000: "zstd", 

145 50001: "webp", 

146} 

147 

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

149 

150OPEN_INFO = { 

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

152 # ExtraSamples) => mode, rawmode 

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

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

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

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

157 (II, 1, (1,), 1, (1,), ()): ("1", "1"), 

158 (MM, 1, (1,), 1, (1,), ()): ("1", "1"), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

274} 

275 

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

277 

278PREFIXES = [ 

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

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

281 b"MM\x2a\x00", # Invalid TIFF header, assume big-endian 

282 b"II\x00\x2a", # Invalid TIFF header, assume little-endian 

283 b"MM\x00\x2b", # BigTIFF with big-endian byte order 

284 b"II\x2b\x00", # BigTIFF with little-endian byte order 

285] 

286 

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

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

289 

290 

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

292 return prefix.startswith(tuple(PREFIXES)) 

293 

294 

295def _limit_rational( 

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

297) -> tuple[IntegralLike, IntegralLike]: 

298 inv = abs(val) > 1 

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

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

301 

302 

303def _limit_signed_rational( 

304 val: IFDRational, max_val: int, min_val: int 

305) -> tuple[IntegralLike, IntegralLike]: 

306 frac = Fraction(val) 

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

308 

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

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

311 

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

313 if max(n_d_float) > max_val: 

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

315 

316 return n_d 

317 

318 

319## 

320# Wrapper for TIFF IFDs. 

321 

322_load_dispatch = {} 

323_write_dispatch = {} 

324 

325 

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

327 def delegate( 

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

329 ) -> bool | float | Fraction: 

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

331 

332 return delegate 

333 

334 

335class IFDRational(Rational): 

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

337 the in the wild use of exif rationals. 

338 

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

340 """ 

341 

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

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

344 

345 """ 

346 

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

348 

349 def __init__( 

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

351 ) -> None: 

352 """ 

353 :param value: either an integer numerator, a 

354 float/rational/other number, or an IFDRational 

355 :param denominator: Optional integer denominator 

356 """ 

357 self._val: Fraction | float 

358 if isinstance(value, IFDRational): 

359 self._numerator = value.numerator 

360 self._denominator = value.denominator 

361 self._val = value._val 

362 return 

363 

364 if isinstance(value, Fraction): 

365 self._numerator = value.numerator 

366 self._denominator = value.denominator 

367 else: 

368 if TYPE_CHECKING: 

369 self._numerator = cast(IntegralLike, value) 

370 else: 

371 self._numerator = value 

372 self._denominator = denominator 

373 

374 if denominator == 0: 

375 self._val = float("nan") 

376 elif denominator == 1: 

377 self._val = Fraction(value) 

378 elif int(value) == value: 

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

380 else: 

381 self._val = Fraction(value / denominator) 

382 

383 @property 

384 def numerator(self) -> IntegralLike: 

385 return self._numerator 

386 

387 @property 

388 def denominator(self) -> int: 

389 return self._denominator 

390 

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

392 """ 

393 

394 :param max_denominator: Integer, the maximum denominator value 

395 :returns: Tuple of (numerator, denominator) 

396 """ 

397 

398 if self.denominator == 0: 

399 return self.numerator, self.denominator 

400 

401 assert isinstance(self._val, Fraction) 

402 f = self._val.limit_denominator(max_denominator) 

403 return f.numerator, f.denominator 

404 

405 def __repr__(self) -> str: 

406 return str(float(self._val)) 

407 

408 def __hash__(self) -> int: # type: ignore[override] 

409 return self._val.__hash__() 

410 

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

412 val = self._val 

413 if isinstance(other, IFDRational): 

414 other = other._val 

415 if isinstance(other, float): 

416 val = float(val) 

417 return val == other 

418 

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

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

421 

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

423 IFDRational.__init__(self, 0) 

424 _val, _numerator, _denominator = state 

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

426 self._val = _val 

427 if TYPE_CHECKING: 

428 self._numerator = cast(IntegralLike, _numerator) 

429 else: 

430 self._numerator = _numerator 

431 assert isinstance(_denominator, int) 

432 self._denominator = _denominator 

433 

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

435 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 

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

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

438 'ceil', 'floor', 'round'] 

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

440 """ 

441 

442 __add__ = _delegate("__add__") 

443 __radd__ = _delegate("__radd__") 

444 __sub__ = _delegate("__sub__") 

445 __rsub__ = _delegate("__rsub__") 

446 __mul__ = _delegate("__mul__") 

447 __rmul__ = _delegate("__rmul__") 

448 __truediv__ = _delegate("__truediv__") 

449 __rtruediv__ = _delegate("__rtruediv__") 

450 __floordiv__ = _delegate("__floordiv__") 

451 __rfloordiv__ = _delegate("__rfloordiv__") 

452 __mod__ = _delegate("__mod__") 

453 __rmod__ = _delegate("__rmod__") 

454 __pow__ = _delegate("__pow__") 

455 __rpow__ = _delegate("__rpow__") 

456 __pos__ = _delegate("__pos__") 

457 __neg__ = _delegate("__neg__") 

458 __abs__ = _delegate("__abs__") 

459 __trunc__ = _delegate("__trunc__") 

460 __lt__ = _delegate("__lt__") 

461 __gt__ = _delegate("__gt__") 

462 __le__ = _delegate("__le__") 

463 __ge__ = _delegate("__ge__") 

464 __bool__ = _delegate("__bool__") 

465 __ceil__ = _delegate("__ceil__") 

466 __floor__ = _delegate("__floor__") 

467 __round__ = _delegate("__round__") 

468 # Python >= 3.11 

469 if hasattr(Fraction, "__int__"): 

470 __int__ = _delegate("__int__") 

471 

472 

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

474 

475 

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

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

478 from .TiffTags import TYPES 

479 

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

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

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

483 return func 

484 

485 return decorator 

486 

487 

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

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

490 _write_dispatch[idx] = func # noqa: F821 

491 return func 

492 

493 return decorator 

494 

495 

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

497 from .TiffTags import TYPES 

498 

499 idx, fmt, name = idx_fmt_name 

500 TYPES[idx] = name 

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

502 

503 def basic_handler( 

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

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

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

507 

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

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

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

511 ) 

512 

513 

514if TYPE_CHECKING: 

515 _IFDv2Base = MutableMapping[int, Any] 

516else: 

517 _IFDv2Base = MutableMapping 

518 

519 

520class ImageFileDirectory_v2(_IFDv2Base): 

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

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

523 

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

525 

526 ifd = ImageFileDirectory_v2() 

527 ifd[key] = 'Some Data' 

528 ifd.tagtype[key] = TiffTags.ASCII 

529 print(ifd[key]) 

530 'Some Data' 

531 

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

533 returned as tuples of the values. 

534 

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

536 tag types in 

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

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

539 manually. 

540 

541 Data Structures: 

542 

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

544 

545 * Key: numerical TIFF tag number 

546 * Value: integer corresponding to the data type from 

547 :py:data:`.TiffTags.TYPES` 

548 

549 .. versionadded:: 3.0.0 

550 

551 'Internal' data structures: 

552 

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

554 

555 * Key: numerical TIFF tag number 

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

557 

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

559 

560 * Key: numerical TIFF tag number 

561 * Value: undecoded byte string from file 

562 

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

564 

565 * Key: numerical TIFF tag number 

566 * Value: decoded data in the v1 format 

567 

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

569 ``self._tags_v2`` once decoded. 

570 

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

572 from outside code. In cooperation with 

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

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

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

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

577 ``legacy_api == true``. 

578 

579 """ 

580 

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

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

583 

584 def __init__( 

585 self, 

586 ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", 

587 prefix: bytes | None = None, 

588 group: int | None = None, 

589 ) -> None: 

590 """Initialize an ImageFileDirectory. 

591 

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

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

594 as the 'prefix' keyword argument. 

595 

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

597 endianness. 

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

599 """ 

600 if not _accept(ifh): 

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

602 raise SyntaxError(msg) 

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

604 if self._prefix == MM: 

605 self._endian = ">" 

606 elif self._prefix == II: 

607 self._endian = "<" 

608 else: 

609 msg = "not a TIFF IFD" 

610 raise SyntaxError(msg) 

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

612 self.group = group 

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

614 """ Dictionary of tag types """ 

615 self.reset() 

616 self.next = ( 

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

618 if self._bigtiff 

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

620 ) 

621 self._legacy_api = False 

622 

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

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

625 

626 @property 

627 def legacy_api(self) -> bool: 

628 return self._legacy_api 

629 

630 @legacy_api.setter 

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

632 msg = "Not allowing setting of legacy api" 

633 raise Exception(msg) 

634 

635 def reset(self) -> None: 

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

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

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

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

640 self._next = None 

641 self._offset: int | None = None 

642 

643 def __str__(self) -> str: 

644 return str(dict(self)) 

645 

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

647 """ 

648 :returns: dict of name|key: value 

649 

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

651 """ 

652 return { 

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

654 for code, value in self.items() 

655 } 

656 

657 def __len__(self) -> int: 

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

659 

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

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

662 data = self._tagdata[tag] 

663 typ = self.tagtype[tag] 

664 size, handler = self._load_dispatch[typ] 

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

666 val = self._tags_v2[tag] 

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

668 val = (val,) 

669 return val 

670 

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

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

673 

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

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

676 

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

678 basetypes = (Number, bytes, str) 

679 

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

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

682 

683 if tag not in self.tagtype: 

684 if info.type: 

685 self.tagtype[tag] = info.type 

686 else: 

687 self.tagtype[tag] = TiffTags.UNDEFINED 

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

689 for v in values: 

690 assert isinstance(v, IFDRational) 

691 if v < 0: 

692 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL 

693 break 

694 else: 

695 self.tagtype[tag] = TiffTags.RATIONAL 

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

697 short = True 

698 signed_short = True 

699 long = True 

700 for v in values: 

701 assert isinstance(v, int) 

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

703 short = False 

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

705 signed_short = False 

706 if long and v < 0: 

707 long = False 

708 if short: 

709 self.tagtype[tag] = TiffTags.SHORT 

710 elif signed_short: 

711 self.tagtype[tag] = TiffTags.SIGNED_SHORT 

712 elif long: 

713 self.tagtype[tag] = TiffTags.LONG 

714 else: 

715 self.tagtype[tag] = TiffTags.SIGNED_LONG 

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

717 self.tagtype[tag] = TiffTags.DOUBLE 

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

719 self.tagtype[tag] = TiffTags.ASCII 

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

721 self.tagtype[tag] = TiffTags.BYTE 

722 

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

724 values = [ 

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

726 for v in values 

727 ] 

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

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

730 

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

732 if not is_ifd: 

733 values = tuple( 

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

735 for value in values 

736 ) 

737 

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

739 

740 # Three branches: 

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

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

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

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

745 if not is_ifd and ( 

746 (info.length == 1) 

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

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

749 ): 

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

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

752 TiffTags.RATIONAL, 

753 TiffTags.SIGNED_RATIONAL, 

754 ]: # rationals 

755 values = (values,) 

756 try: 

757 (dest[tag],) = values 

758 except ValueError: 

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

760 warnings.warn( 

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

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

763 ) 

764 dest[tag] = values[0] 

765 

766 else: 

767 # Spec'd length > 1 or undefined 

768 # Unspec'd, and length > 1 

769 dest[tag] = values 

770 

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

772 self._tags_v2.pop(tag, None) 

773 self._tags_v1.pop(tag, None) 

774 self._tagdata.pop(tag, None) 

775 

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

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

778 

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

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

781 

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

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

784 

785 list( 

786 map( 

787 _register_basic, 

788 [ 

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

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

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

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

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

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

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

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

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

798 ], 

799 ) 

800 ) 

801 

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

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

804 return data 

805 

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

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

808 if isinstance(data, IFDRational): 

809 data = int(data) 

810 if isinstance(data, int): 

811 data = bytes((data,)) 

812 return data 

813 

814 @_register_loader(2, 1) 

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

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

817 data = data[:-1] 

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

819 

820 @_register_writer(2) 

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

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

823 if isinstance(value, int): 

824 value = str(value) 

825 if not isinstance(value, bytes): 

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

827 return value + b"\0" 

828 

829 @_register_loader(5, 8) 

830 def load_rational( 

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

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

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

834 

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

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

837 

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

839 

840 @_register_writer(5) 

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

842 return b"".join( 

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

844 ) 

845 

846 @_register_loader(7, 1) 

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

848 return data 

849 

850 @_register_writer(7) 

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

852 if isinstance(value, IFDRational): 

853 value = int(value) 

854 if isinstance(value, int): 

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

856 return value 

857 

858 @_register_loader(10, 8) 

859 def load_signed_rational( 

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

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

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

863 

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

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

866 

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

868 

869 @_register_writer(10) 

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

871 return b"".join( 

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

873 for frac in values 

874 ) 

875 

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

877 ret = fp.read(size) 

878 if len(ret) != size: 

879 msg = ( 

880 "Corrupt EXIF data. " 

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

882 ) 

883 raise OSError(msg) 

884 return ret 

885 

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

887 self.reset() 

888 self._offset = fp.tell() 

889 

890 try: 

891 tag_count = ( 

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

893 if self._bigtiff 

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

895 )[0] 

896 for i in range(tag_count): 

897 tag, typ, count, data = ( 

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

899 if self._bigtiff 

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

901 ) 

902 

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

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

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

906 

907 try: 

908 unit_size, handler = self._load_dispatch[typ] 

909 except KeyError: 

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

911 continue # ignore unsupported type 

912 size = count * unit_size 

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

914 here = fp.tell() 

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

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

917 fp.seek(offset) 

918 data = ImageFile._safe_read(fp, size) 

919 fp.seek(here) 

920 else: 

921 data = data[:size] 

922 

923 if len(data) != size: 

924 warnings.warn( 

925 "Possibly corrupt EXIF data. " 

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

927 f" Skipping tag {tag}" 

928 ) 

929 logger.debug(msg) 

930 continue 

931 

932 if not data: 

933 logger.debug(msg) 

934 continue 

935 

936 self._tagdata[tag] = data 

937 self.tagtype[tag] = typ 

938 

939 msg += " - value: " 

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

941 

942 logger.debug(msg) 

943 

944 (self.next,) = ( 

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

946 if self._bigtiff 

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

948 ) 

949 except OSError as msg: 

950 warnings.warn(str(msg)) 

951 return 

952 

953 def _get_ifh(self) -> bytes: 

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

955 if self._bigtiff: 

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

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

958 

959 return ifh 

960 

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

962 # FIXME What about tagdata? 

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

964 

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

966 

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

968 fmt_size = 8 if self._bigtiff else 4 

969 offset += ( 

970 len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size 

971 ) 

972 stripoffsets = None 

973 

974 # pass 1: convert tags to binary format 

975 # always write tags in ascending order 

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

977 if tag == STRIPOFFSETS: 

978 stripoffsets = len(entries) 

979 typ = self.tagtype[tag] 

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

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

982 if is_ifd: 

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

984 values = self._tags_v2[tag] 

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

986 ifd[ifd_tag] = ifd_value 

987 data = ifd.tobytes(offset) 

988 else: 

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

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

991 

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

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

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

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

996 logger.debug(msg) 

997 

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

999 if is_ifd: 

1000 count = 1 

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

1002 count = len(data) 

1003 else: 

1004 count = len(values) 

1005 # figure out if data fits into the entry 

1006 if len(data) <= fmt_size: 

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

1008 else: 

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

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

1011 

1012 # update strip offset data to point beyond auxiliary data 

1013 if stripoffsets is not None: 

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

1015 if data: 

1016 size, handler = self._load_dispatch[typ] 

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

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

1019 else: 

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

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

1022 

1023 # pass 2: write entries to file 

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

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

1026 result += self._pack( 

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

1028 ) 

1029 

1030 # -- overwrite here for multi-page -- 

1031 result += self._pack(fmt, 0) # end of entries 

1032 

1033 # pass 3: write auxiliary data to file 

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

1035 result += data 

1036 if len(data) & 1: 

1037 result += b"\0" 

1038 

1039 return result 

1040 

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

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

1043 fp.write(self._get_ifh()) 

1044 

1045 offset = fp.tell() 

1046 result = self.tobytes(offset) 

1047 fp.write(result) 

1048 return offset + len(result) 

1049 

1050 

1051ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1052ImageFileDirectory_v2._write_dispatch = _write_dispatch 

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

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

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

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

1057del _load_dispatch, _write_dispatch, idx, name 

1058 

1059 

1060# Legacy ImageFileDirectory support. 

1061class ImageFileDirectory_v1(ImageFileDirectory_v2): 

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

1063 

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

1065 

1066 ifd = ImageFileDirectory_v1() 

1067 ifd[key] = 'Some Data' 

1068 ifd.tagtype[key] = TiffTags.ASCII 

1069 print(ifd[key]) 

1070 ('Some Data',) 

1071 

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

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

1074 

1075 Values are returned as a tuple. 

1076 

1077 .. deprecated:: 3.0.0 

1078 """ 

1079 

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

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

1082 self._legacy_api = True 

1083 

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

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

1086 

1087 # defined in ImageFileDirectory_v2 

1088 tagtype: dict[int, int] 

1089 """Dictionary of tag types""" 

1090 

1091 @classmethod 

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

1093 """Returns an 

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

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

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

1097 instance. 

1098 

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

1100 

1101 """ 

1102 

1103 ifd = cls(prefix=original.prefix) 

1104 ifd._tagdata = original._tagdata 

1105 ifd.tagtype = original.tagtype 

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

1107 return ifd 

1108 

1109 def to_v2(self) -> ImageFileDirectory_v2: 

1110 """Returns an 

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

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

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

1114 instance. 

1115 

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

1117 

1118 """ 

1119 

1120 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1121 ifd._tagdata = dict(self._tagdata) 

1122 ifd.tagtype = dict(self.tagtype) 

1123 ifd._tags_v2 = dict(self._tags_v2) 

1124 return ifd 

1125 

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

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

1128 

1129 def __len__(self) -> int: 

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

1131 

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

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

1134 

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

1136 for legacy_api in (False, True): 

1137 self._setitem(tag, value, legacy_api) 

1138 

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

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

1141 data = self._tagdata[tag] 

1142 typ = self.tagtype[tag] 

1143 size, handler = self._load_dispatch[typ] 

1144 for legacy in (False, True): 

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

1146 val = self._tags_v1[tag] 

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

1148 val = (val,) 

1149 return val 

1150 

1151 

1152# undone -- switch this pointer 

1153ImageFileDirectory = ImageFileDirectory_v1 

1154 

1155 

1156## 

1157# Image plugin for TIFF files. 

1158 

1159 

1160class TiffImageFile(ImageFile.ImageFile): 

1161 format = "TIFF" 

1162 format_description = "Adobe TIFF" 

1163 _close_exclusive_fp_after_loading = False 

1164 

1165 def __init__( 

1166 self, 

1167 fp: StrOrBytesPath | IO[bytes], 

1168 filename: str | bytes | None = None, 

1169 ) -> None: 

1170 self.tag_v2: ImageFileDirectory_v2 

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

1172 

1173 self.tag: ImageFileDirectory_v1 

1174 """ Legacy tag entries """ 

1175 

1176 super().__init__(fp, filename) 

1177 

1178 def _open(self) -> None: 

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

1180 

1181 # Header 

1182 ifh = self.fp.read(8) 

1183 if ifh[2] == 43: 

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

1185 

1186 self.tag_v2 = ImageFileDirectory_v2(ifh) 

1187 

1188 # setup frame pointers 

1189 self.__first = self.__next = self.tag_v2.next 

1190 self.__frame = -1 

1191 self._fp = self.fp 

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

1193 self._n_frames: int | None = None 

1194 

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

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

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

1198 

1199 # and load the first frame 

1200 self._seek(0) 

1201 

1202 @property 

1203 def n_frames(self) -> int: 

1204 current_n_frames = self._n_frames 

1205 if current_n_frames is None: 

1206 current = self.tell() 

1207 self._seek(len(self._frame_pos)) 

1208 while self._n_frames is None: 

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

1210 self.seek(current) 

1211 assert self._n_frames is not None 

1212 return self._n_frames 

1213 

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

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

1216 if not self._seek_check(frame): 

1217 return 

1218 self._seek(frame) 

1219 if self._im is not None and ( 

1220 self.im.size != self._tile_size 

1221 or self.im.mode != self.mode 

1222 or self.readonly 

1223 ): 

1224 self._im = None 

1225 

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

1227 if isinstance(self._fp, DeferredError): 

1228 raise self._fp.ex 

1229 self.fp = self._fp 

1230 

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

1232 if not self.__next: 

1233 msg = "no more images in TIFF file" 

1234 raise EOFError(msg) 

1235 logger.debug( 

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

1237 frame, 

1238 self.__frame, 

1239 self.__next, 

1240 self.fp.tell(), 

1241 ) 

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

1243 msg = "Unable to seek to frame" 

1244 raise ValueError(msg) 

1245 self.fp.seek(self.__next) 

1246 self._frame_pos.append(self.__next) 

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

1248 self.tag_v2.load(self.fp) 

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

1250 # This IFD has already been processed 

1251 # Declare this to be the end of the image 

1252 self.__next = 0 

1253 else: 

1254 self.__next = self.tag_v2.next 

1255 if self.__next == 0: 

1256 self._n_frames = frame + 1 

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

1258 self.is_animated = self.__next != 0 

1259 self.__frame += 1 

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

1261 self.tag_v2.load(self.fp) 

1262 if XMP in self.tag_v2: 

1263 xmp = self.tag_v2[XMP] 

1264 if isinstance(xmp, tuple) and len(xmp) == 1: 

1265 xmp = xmp[0] 

1266 self.info["xmp"] = xmp 

1267 elif "xmp" in self.info: 

1268 del self.info["xmp"] 

1269 self._reload_exif() 

1270 # fill the legacy tag/ifd entries 

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

1272 self.__frame = frame 

1273 self._setup() 

1274 

1275 def tell(self) -> int: 

1276 """Return the current frame number""" 

1277 return self.__frame 

1278 

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

1280 """ 

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

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

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

1284 

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

1286 """ 

1287 blocks = {} 

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

1289 if val: 

1290 while val.startswith(b"8BIM"): 

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

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

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

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

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

1296 

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

1298 return blocks 

1299 

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

1301 if self.tile and self.use_load_libtiff: 

1302 return self._load_libtiff() 

1303 return super().load() 

1304 

1305 def load_prepare(self) -> None: 

1306 if self._im is None: 

1307 Image._decompression_bomb_check(self._tile_size) 

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

1309 ImageFile.ImageFile.load_prepare(self) 

1310 

1311 def load_end(self) -> None: 

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

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

1314 if not self.is_animated: 

1315 self._close_exclusive_fp_after_loading = True 

1316 

1317 # load IFD data from fp before it is closed 

1318 exif = self.getexif() 

1319 for key in TiffTags.TAGS_V2_GROUPS: 

1320 if key not in exif: 

1321 continue 

1322 exif.get_ifd(key) 

1323 

1324 ImageOps.exif_transpose(self, in_place=True) 

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

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

1327 

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

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

1330 Calls out to libtiff""" 

1331 

1332 Image.Image.load(self) 

1333 

1334 self.load_prepare() 

1335 

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

1337 msg = "Not exactly one tile" 

1338 raise OSError(msg) 

1339 

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

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

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

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

1344 

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

1346 # file descriptor, use that instead of reading 

1347 # into a string in python. 

1348 try: 

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

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

1351 # should also eliminate the need for fp.tell 

1352 # in _seek 

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

1354 self.fp.flush() 

1355 except OSError: 

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

1357 # it doesn't use a file descriptor. 

1358 fp = False 

1359 

1360 if fp: 

1361 assert isinstance(args, tuple) 

1362 args_list = list(args) 

1363 args_list[2] = fp 

1364 args = tuple(args_list) 

1365 

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

1367 try: 

1368 decoder.setimage(self.im, extents) 

1369 except ValueError as e: 

1370 msg = "Couldn't set the image" 

1371 raise OSError(msg) from e 

1372 

1373 close_self_fp = self._exclusive_fp and not self.is_animated 

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

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

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

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

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

1379 # underlying string for stringio. 

1380 # 

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

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

1383 # deal with here by reordering. 

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

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

1386 elif fp: 

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

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

1389 if not close_self_fp: 

1390 self.fp.seek(0) 

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

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

1393 # io.BufferedReader and possible others. 

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

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

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

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

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

1399 # 4 bytes, otherwise the trace might error out 

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

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

1402 else: 

1403 # we have something else. 

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

1405 self.fp.seek(0) 

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

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

1408 

1409 self.tile = [] 

1410 self.readonly = 0 

1411 

1412 self.load_end() 

1413 

1414 if close_self_fp: 

1415 self.fp.close() 

1416 self.fp = None # might be shared 

1417 

1418 if err < 0: 

1419 msg = f"decoder error {err}" 

1420 raise OSError(msg) 

1421 

1422 return Image.Image.load(self) 

1423 

1424 def _setup(self) -> None: 

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

1426 

1427 if 0xBC01 in self.tag_v2: 

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

1429 raise OSError(msg) 

1430 

1431 # extract relevant tags 

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

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

1434 

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

1436 # the specification 

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

1438 

1439 # old style jpeg compression images most certainly are YCbCr 

1440 if self._compression == "tiff_jpeg": 

1441 photo = 6 

1442 

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

1444 

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

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

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

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

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

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

1451 

1452 # size 

1453 try: 

1454 xsize = self.tag_v2[IMAGEWIDTH] 

1455 ysize = self.tag_v2[IMAGELENGTH] 

1456 except KeyError as e: 

1457 msg = "Missing dimensions" 

1458 raise TypeError(msg) from e 

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

1460 msg = "Invalid dimensions" 

1461 raise ValueError(msg) 

1462 self._tile_size = xsize, ysize 

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

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

1465 self._size = ysize, xsize 

1466 else: 

1467 self._size = xsize, ysize 

1468 

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

1470 

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

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

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

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

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

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

1477 # for more exotic images. 

1478 sample_format = (1,) 

1479 

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

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

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

1483 bps_count = 3 

1484 elif photo == 5: # CMYK 

1485 bps_count = 4 

1486 else: 

1487 bps_count = 1 

1488 bps_count += len(extra_tuple) 

1489 bps_actual_count = len(bps_tuple) 

1490 samples_per_pixel = self.tag_v2.get( 

1491 SAMPLESPERPIXEL, 

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

1493 ) 

1494 

1495 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

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

1497 logger.error( 

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

1499 ) 

1500 msg = "Invalid value for samples per pixel" 

1501 raise SyntaxError(msg) 

1502 

1503 if samples_per_pixel < bps_actual_count: 

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

1505 # remove the excess. 

1506 bps_tuple = bps_tuple[:samples_per_pixel] 

1507 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

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

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

1510 bps_tuple = bps_tuple * samples_per_pixel 

1511 

1512 if len(bps_tuple) != samples_per_pixel: 

1513 msg = "unknown data organization" 

1514 raise SyntaxError(msg) 

1515 

1516 # mode: check photometric interpretation and bits per pixel 

1517 key = ( 

1518 self.tag_v2.prefix, 

1519 photo, 

1520 sample_format, 

1521 fillorder, 

1522 bps_tuple, 

1523 extra_tuple, 

1524 ) 

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

1526 try: 

1527 self._mode, rawmode = OPEN_INFO[key] 

1528 except KeyError as e: 

1529 logger.debug("- unsupported format") 

1530 msg = "unknown pixel mode" 

1531 raise SyntaxError(msg) from e 

1532 

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

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

1535 

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

1537 

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

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

1540 

1541 if xres and yres: 

1542 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1543 if resunit == 2: # dots per inch 

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

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

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

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

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

1549 # For backward compatibility, 

1550 # we also preserve the old behavior 

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

1552 else: # No absolute unit of measurement 

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

1554 

1555 # build tile descriptors 

1556 x = y = layer = 0 

1557 self.tile = [] 

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

1559 if self.use_load_libtiff: 

1560 # Decoder expects entire file as one tile. 

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

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

1563 # function. 

1564 # 

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

1566 # use the _load_libtiff function. 

1567 

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

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

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

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

1572 if fillorder == 2: 

1573 # Replace fillorder with fillorder=1 

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

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

1576 # this should always work, since all the 

1577 # fillorder==2 modes have a corresponding 

1578 # fillorder=1 mode 

1579 self._mode, rawmode = OPEN_INFO[key] 

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

1581 # unpacked straight into RGB values 

1582 if ( 

1583 photo == 6 

1584 and self._compression == "jpeg" 

1585 and self._planar_configuration == 1 

1586 ): 

1587 rawmode = "RGB" 

1588 # libtiff always returns the bytes in native order. 

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

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

1591 # byte order. 

1592 elif rawmode == "I;16": 

1593 rawmode = "I;16N" 

1594 elif rawmode.endswith((";16B", ";16L")): 

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

1596 

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

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

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

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

1601 

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

1603 # striped image 

1604 if STRIPOFFSETS in self.tag_v2: 

1605 offsets = self.tag_v2[STRIPOFFSETS] 

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

1607 w = xsize 

1608 else: 

1609 # tiled image 

1610 offsets = self.tag_v2[TILEOFFSETS] 

1611 tilewidth = self.tag_v2.get(TILEWIDTH) 

1612 h = self.tag_v2.get(TILELENGTH) 

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

1614 msg = "Invalid tile dimensions" 

1615 raise ValueError(msg) 

1616 w = tilewidth 

1617 

1618 if w == xsize and h == ysize and self._planar_configuration != 2: 

1619 # Every tile covers the image. Only use the last offset 

1620 offsets = offsets[-1:] 

1621 

1622 for offset in offsets: 

1623 if x + w > xsize: 

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

1625 else: 

1626 stride = 0 

1627 

1628 tile_rawmode = rawmode 

1629 if self._planar_configuration == 2: 

1630 # each band on it's own layer 

1631 tile_rawmode = rawmode[layer] 

1632 # adjust stride width accordingly 

1633 stride /= bps_count 

1634 

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

1636 self.tile.append( 

1637 ImageFile._Tile( 

1638 self._compression, 

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

1640 offset, 

1641 args, 

1642 ) 

1643 ) 

1644 x += w 

1645 if x >= xsize: 

1646 x, y = 0, y + h 

1647 if y >= ysize: 

1648 y = 0 

1649 layer += 1 

1650 else: 

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

1652 msg = "unknown data organization" 

1653 raise SyntaxError(msg) 

1654 

1655 # Fix up info. 

1656 if ICCPROFILE in self.tag_v2: 

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

1658 

1659 # fixup palette descriptor 

1660 

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

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

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

1664 

1665 

1666# 

1667# -------------------------------------------------------------------- 

1668# Write TIFF files 

1669 

1670# little endian is default except for image modes with 

1671# explicit big endian byte-order 

1672 

1673SAVE_INFO = { 

1674 # mode => rawmode, byteorder, photometrics, 

1675 # sampleformat, bitspersample, extra 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1695} 

1696 

1697 

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

1699 try: 

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

1701 except KeyError as e: 

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

1703 raise OSError(msg) from e 

1704 

1705 encoderinfo = im.encoderinfo 

1706 encoderconfig = im.encoderconfig 

1707 

1708 ifd = ImageFileDirectory_v2(prefix=prefix) 

1709 if encoderinfo.get("big_tiff"): 

1710 ifd._bigtiff = True 

1711 

1712 try: 

1713 compression = encoderinfo["compression"] 

1714 except KeyError: 

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

1716 if isinstance(compression, int): 

1717 # compression value may be from BMP. Ignore it 

1718 compression = None 

1719 if compression is None: 

1720 compression = "raw" 

1721 elif compression == "tiff_jpeg": 

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

1723 compression = "jpeg" 

1724 elif compression == "tiff_deflate": 

1725 compression = "tiff_adobe_deflate" 

1726 

1727 libtiff = WRITE_LIBTIFF or compression != "raw" 

1728 

1729 # required for color libtiff images 

1730 ifd[PLANAR_CONFIGURATION] = 1 

1731 

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

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

1734 

1735 # write any arbitrary tags passed in as an ImageFileDirectory 

1736 if "tiffinfo" in encoderinfo: 

1737 info = encoderinfo["tiffinfo"] 

1738 elif "exif" in encoderinfo: 

1739 info = encoderinfo["exif"] 

1740 if isinstance(info, bytes): 

1741 exif = Image.Exif() 

1742 exif.load(info) 

1743 info = exif 

1744 else: 

1745 info = {} 

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

1747 if isinstance(info, ImageFileDirectory_v1): 

1748 info = info.to_v2() 

1749 for key in info: 

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

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

1752 else: 

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

1754 try: 

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

1756 except Exception: 

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

1758 

1759 legacy_ifd = {} 

1760 if hasattr(im, "tag"): 

1761 legacy_ifd = im.tag.to_v2() 

1762 

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

1764 for tag in ( 

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

1766 EXIFIFD, 

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

1768 SAMPLEFORMAT, 

1769 ): 

1770 if tag in supplied_tags: 

1771 del supplied_tags[tag] 

1772 

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

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

1775 if hasattr(im, "tag_v2"): 

1776 # preserve tags from original TIFF image file 

1777 for key in ( 

1778 RESOLUTION_UNIT, 

1779 X_RESOLUTION, 

1780 Y_RESOLUTION, 

1781 IPTC_NAA_CHUNK, 

1782 PHOTOSHOP_CHUNK, 

1783 XMP, 

1784 ): 

1785 if key in im.tag_v2: 

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

1787 TiffTags.BYTE, 

1788 TiffTags.UNDEFINED, 

1789 ): 

1790 del supplied_tags[key] 

1791 else: 

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

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

1794 

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

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

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

1798 if icc: 

1799 ifd[ICCPROFILE] = icc 

1800 

1801 for key, name in [ 

1802 (IMAGEDESCRIPTION, "description"), 

1803 (X_RESOLUTION, "resolution"), 

1804 (Y_RESOLUTION, "resolution"), 

1805 (X_RESOLUTION, "x_resolution"), 

1806 (Y_RESOLUTION, "y_resolution"), 

1807 (RESOLUTION_UNIT, "resolution_unit"), 

1808 (SOFTWARE, "software"), 

1809 (DATE_TIME, "date_time"), 

1810 (ARTIST, "artist"), 

1811 (COPYRIGHT, "copyright"), 

1812 ]: 

1813 if name in encoderinfo: 

1814 ifd[key] = encoderinfo[name] 

1815 

1816 dpi = encoderinfo.get("dpi") 

1817 if dpi: 

1818 ifd[RESOLUTION_UNIT] = 2 

1819 ifd[X_RESOLUTION] = dpi[0] 

1820 ifd[Y_RESOLUTION] = dpi[1] 

1821 

1822 if bits != (1,): 

1823 ifd[BITSPERSAMPLE] = bits 

1824 if len(bits) != 1: 

1825 ifd[SAMPLESPERPIXEL] = len(bits) 

1826 if extra is not None: 

1827 ifd[EXTRASAMPLES] = extra 

1828 if format != 1: 

1829 ifd[SAMPLEFORMAT] = format 

1830 

1831 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1832 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

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

1834 if im.mode == "1": 

1835 inverted_im = im.copy() 

1836 px = inverted_im.load() 

1837 if px is not None: 

1838 for y in range(inverted_im.height): 

1839 for x in range(inverted_im.width): 

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

1841 im = inverted_im 

1842 else: 

1843 im = ImageOps.invert(im) 

1844 

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

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

1847 colormap = [] 

1848 colors = len(lut) // 3 

1849 for i in range(3): 

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

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

1852 ifd[COLORMAP] = colormap 

1853 # data orientation 

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

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

1856 if ROWSPERSTRIP not in ifd: 

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

1858 if libtiff: 

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

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

1861 # JPEG encoder expects multiple of 8 rows 

1862 if compression == "jpeg": 

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

1864 else: 

1865 rows_per_strip = h 

1866 if rows_per_strip == 0: 

1867 rows_per_strip = 1 

1868 ifd[ROWSPERSTRIP] = rows_per_strip 

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

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

1871 if strip_byte_counts >= 2**16: 

1872 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

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

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

1875 ) 

1876 ifd[STRIPOFFSETS] = tuple( 

1877 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1878 ) # this is adjusted by IFD writer 

1879 # no compression by default: 

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

1881 

1882 if im.mode == "YCbCr": 

1883 for tag, default_value in { 

1884 YCBCRSUBSAMPLING: (1, 1), 

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

1886 }.items(): 

1887 ifd.setdefault(tag, default_value) 

1888 

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

1890 if libtiff: 

1891 if "quality" in encoderinfo: 

1892 quality = encoderinfo["quality"] 

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

1894 msg = "Invalid quality setting" 

1895 raise ValueError(msg) 

1896 if compression != "jpeg": 

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

1898 raise ValueError(msg) 

1899 ifd[JPEGQUALITY] = quality 

1900 

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

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

1903 _fp = 0 

1904 if hasattr(fp, "fileno"): 

1905 try: 

1906 fp.seek(0) 

1907 _fp = fp.fileno() 

1908 except io.UnsupportedOperation: 

1909 pass 

1910 

1911 # optional types for non core tags 

1912 types = {} 

1913 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1914 # based on the data in the strip. 

1915 # OSUBFILETYPE is deprecated. 

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

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

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

1919 # SUBIFD may also cause a segfault. 

1920 blocklist += [ 

1921 OSUBFILETYPE, 

1922 REFERENCEBLACKWHITE, 

1923 STRIPBYTECOUNTS, 

1924 STRIPOFFSETS, 

1925 TRANSFERFUNCTION, 

1926 SUBIFD, 

1927 ] 

1928 

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

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

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

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

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

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

1935 # Libtiff can only process certain core items without adding 

1936 # them to the custom dictionary. 

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

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

1939 if tag not in TiffTags.LIBTIFF_CORE: 

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

1941 continue 

1942 

1943 if tag in TiffTags.TAGS_V2_GROUPS: 

1944 types[tag] = TiffTags.LONG8 

1945 elif tag in ifd.tagtype: 

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

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

1948 continue 

1949 else: 

1950 type = TiffTags.lookup(tag).type 

1951 if type: 

1952 types[tag] = type 

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

1954 if isinstance(value, str): 

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

1956 elif isinstance(value, IFDRational): 

1957 atts[tag] = float(value) 

1958 else: 

1959 atts[tag] = value 

1960 

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

1962 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1963 

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

1965 

1966 # libtiff always expects the bytes in native order. 

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

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

1969 # byte order. 

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

1971 rawmode = "I;16N" 

1972 

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

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

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

1976 tags = list(atts.items()) 

1977 tags.sort() 

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

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

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

1981 while True: 

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

1983 if not _fp: 

1984 fp.write(data) 

1985 if errcode: 

1986 break 

1987 if errcode < 0: 

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

1989 raise OSError(msg) 

1990 

1991 else: 

1992 for tag in blocklist: 

1993 del ifd[tag] 

1994 offset = ifd.save(fp) 

1995 

1996 ImageFile._save( 

1997 im, 

1998 fp, 

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

2000 ) 

2001 

2002 # -- helper for multi-page save -- 

2003 if "_debug_multipage" in encoderinfo: 

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

2005 setattr(im, "_debug_multipage", ifd) 

2006 

2007 

2008class AppendingTiffWriter(io.BytesIO): 

2009 fieldSizes = [ 

2010 0, # None 

2011 1, # byte 

2012 1, # ascii 

2013 2, # short 

2014 4, # long 

2015 8, # rational 

2016 1, # sbyte 

2017 1, # undefined 

2018 2, # sshort 

2019 4, # slong 

2020 8, # srational 

2021 4, # float 

2022 8, # double 

2023 4, # ifd 

2024 2, # unicode 

2025 4, # complex 

2026 8, # long8 

2027 ] 

2028 

2029 Tags = { 

2030 273, # StripOffsets 

2031 288, # FreeOffsets 

2032 324, # TileOffsets 

2033 519, # JPEGQTables 

2034 520, # JPEGDCTables 

2035 521, # JPEGACTables 

2036 } 

2037 

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

2039 self.f: IO[bytes] 

2040 if is_path(fn): 

2041 self.name = fn 

2042 self.close_fp = True 

2043 try: 

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

2045 except OSError: 

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

2047 else: 

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

2049 self.close_fp = False 

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

2051 self.setup() 

2052 

2053 def setup(self) -> None: 

2054 # Reset everything. 

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

2056 

2057 self.whereToWriteNewIFDOffset: int | None = None 

2058 self.offsetOfNewPage = 0 

2059 

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

2061 self._bigtiff = b"\x2b" in iimm 

2062 if not iimm: 

2063 # empty file - first page 

2064 self.isFirst = True 

2065 return 

2066 

2067 self.isFirst = False 

2068 if iimm not in PREFIXES: 

2069 msg = "Invalid TIFF file header" 

2070 raise RuntimeError(msg) 

2071 

2072 self.setEndian("<" if iimm.startswith(II) else ">") 

2073 

2074 if self._bigtiff: 

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

2076 self.skipIFDs() 

2077 self.goToEnd() 

2078 

2079 def finalize(self) -> None: 

2080 if self.isFirst: 

2081 return 

2082 

2083 # fix offsets 

2084 self.f.seek(self.offsetOfNewPage) 

2085 

2086 iimm = self.f.read(4) 

2087 if not iimm: 

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

2089 return 

2090 

2091 if iimm != self.IIMM: 

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

2093 raise RuntimeError(msg) 

2094 

2095 if self._bigtiff: 

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

2097 ifd_offset = self._read(8 if self._bigtiff else 4) 

2098 ifd_offset += self.offsetOfNewPage 

2099 assert self.whereToWriteNewIFDOffset is not None 

2100 self.f.seek(self.whereToWriteNewIFDOffset) 

2101 self._write(ifd_offset, 8 if self._bigtiff else 4) 

2102 self.f.seek(ifd_offset) 

2103 self.fixIFD() 

2104 

2105 def newFrame(self) -> None: 

2106 # Call this to finish a frame. 

2107 self.finalize() 

2108 self.setup() 

2109 

2110 def __enter__(self) -> AppendingTiffWriter: 

2111 return self 

2112 

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

2114 if self.close_fp: 

2115 self.close() 

2116 

2117 def tell(self) -> int: 

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

2119 

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

2121 """ 

2122 :param offset: Distance to seek. 

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

2124 end or current position. 

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

2126 """ 

2127 if whence == os.SEEK_SET: 

2128 offset += self.offsetOfNewPage 

2129 

2130 self.f.seek(offset, whence) 

2131 return self.tell() 

2132 

2133 def goToEnd(self) -> None: 

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

2135 pos = self.f.tell() 

2136 

2137 # pad to 16 byte boundary 

2138 pad_bytes = 16 - pos % 16 

2139 if 0 < pad_bytes < 16: 

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

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

2142 

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

2144 self.endian = endian 

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

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

2147 self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") 

2148 

2149 def skipIFDs(self) -> None: 

2150 while True: 

2151 ifd_offset = self._read(8 if self._bigtiff else 4) 

2152 if ifd_offset == 0: 

2153 self.whereToWriteNewIFDOffset = self.f.tell() - ( 

2154 8 if self._bigtiff else 4 

2155 ) 

2156 break 

2157 

2158 self.f.seek(ifd_offset) 

2159 num_tags = self._read(8 if self._bigtiff else 2) 

2160 self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) 

2161 

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

2163 return self.f.write(data) 

2164 

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

2166 try: 

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

2168 except KeyError: 

2169 msg = "offset is not supported" 

2170 raise RuntimeError(msg) 

2171 

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

2173 (value,) = struct.unpack( 

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

2175 ) 

2176 return value 

2177 

2178 def readShort(self) -> int: 

2179 return self._read(2) 

2180 

2181 def readLong(self) -> int: 

2182 return self._read(4) 

2183 

2184 @staticmethod 

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

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

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

2188 raise RuntimeError(msg) 

2189 

2190 def _rewriteLast( 

2191 self, value: int, field_size: int, new_field_size: int = 0 

2192 ) -> None: 

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

2194 if not new_field_size: 

2195 new_field_size = field_size 

2196 bytes_written = self.f.write( 

2197 struct.pack(self.endian + self._fmt(new_field_size), value) 

2198 ) 

2199 self._verify_bytes_written(bytes_written, new_field_size) 

2200 

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

2202 self._rewriteLast(value, 2, 4) 

2203 

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

2205 return self._rewriteLast(value, 2) 

2206 

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

2208 return self._rewriteLast(value, 4) 

2209 

2210 def _write(self, value: int, field_size: int) -> None: 

2211 bytes_written = self.f.write( 

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

2213 ) 

2214 self._verify_bytes_written(bytes_written, field_size) 

2215 

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

2217 self._write(value, 2) 

2218 

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

2220 self._write(value, 4) 

2221 

2222 def close(self) -> None: 

2223 self.finalize() 

2224 if self.close_fp: 

2225 self.f.close() 

2226 

2227 def fixIFD(self) -> None: 

2228 num_tags = self._read(8 if self._bigtiff else 2) 

2229 

2230 for i in range(num_tags): 

2231 tag, field_type, count = struct.unpack( 

2232 self.tagFormat, self.f.read(12 if self._bigtiff else 8) 

2233 ) 

2234 

2235 field_size = self.fieldSizes[field_type] 

2236 total_size = field_size * count 

2237 fmt_size = 8 if self._bigtiff else 4 

2238 is_local = total_size <= fmt_size 

2239 if not is_local: 

2240 offset = self._read(fmt_size) + self.offsetOfNewPage 

2241 self._rewriteLast(offset, fmt_size) 

2242 

2243 if tag in self.Tags: 

2244 cur_pos = self.f.tell() 

2245 

2246 logger.debug( 

2247 "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", 

2248 TiffTags.lookup(tag).name, 

2249 tag, 

2250 TYPES.get(field_type, "unknown"), 

2251 field_type, 

2252 field_size, 

2253 count, 

2254 ) 

2255 

2256 if is_local: 

2257 self._fixOffsets(count, field_size) 

2258 self.f.seek(cur_pos + fmt_size) 

2259 else: 

2260 self.f.seek(offset) 

2261 self._fixOffsets(count, field_size) 

2262 self.f.seek(cur_pos) 

2263 

2264 elif is_local: 

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

2266 self.f.seek(fmt_size, os.SEEK_CUR) 

2267 

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

2269 for i in range(count): 

2270 offset = self._read(field_size) 

2271 offset += self.offsetOfNewPage 

2272 

2273 new_field_size = 0 

2274 if self._bigtiff and field_size in (2, 4) and offset >= 2**32: 

2275 # offset is now too large - we must convert long to long8 

2276 new_field_size = 8 

2277 elif field_size == 2 and offset >= 2**16: 

2278 # offset is now too large - we must convert short to long 

2279 new_field_size = 4 

2280 if new_field_size: 

2281 if count != 1: 

2282 msg = "not implemented" 

2283 raise RuntimeError(msg) # XXX TODO 

2284 

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

2286 # local (not referenced with another offset) 

2287 self._rewriteLast(offset, field_size, new_field_size) 

2288 # Move back past the new offset, past 'count', and before 'field_type' 

2289 rewind = -new_field_size - 4 - 2 

2290 self.f.seek(rewind, os.SEEK_CUR) 

2291 self.writeShort(new_field_size) # rewrite the type 

2292 self.f.seek(2 - rewind, os.SEEK_CUR) 

2293 else: 

2294 self._rewriteLast(offset, field_size) 

2295 

2296 def fixOffsets( 

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

2298 ) -> None: 

2299 if isShort: 

2300 field_size = 2 

2301 elif isLong: 

2302 field_size = 4 

2303 else: 

2304 field_size = 0 

2305 return self._fixOffsets(count, field_size) 

2306 

2307 

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

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

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

2311 return _save(im, fp, filename) 

2312 

2313 cur_idx = im.tell() 

2314 try: 

2315 with AppendingTiffWriter(fp) as tf: 

2316 for ims in [im] + append_images: 

2317 if not hasattr(ims, "encoderinfo"): 

2318 ims.encoderinfo = {} 

2319 if not hasattr(ims, "encoderconfig"): 

2320 ims.encoderconfig = () 

2321 nfr = getattr(ims, "n_frames", 1) 

2322 

2323 for idx in range(nfr): 

2324 ims.seek(idx) 

2325 ims.load() 

2326 _save(ims, tf, filename) 

2327 tf.newFrame() 

2328 finally: 

2329 im.seek(cur_idx) 

2330 

2331 

2332# 

2333# -------------------------------------------------------------------- 

2334# Register 

2335 

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

2337Image.register_save(TiffImageFile.format, _save) 

2338Image.register_save_all(TiffImageFile.format, _save_all) 

2339 

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

2341 

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