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

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

1231 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 Callable, MutableMapping 

51from fractions import Fraction 

52from numbers import Number, Rational 

53from typing import IO, Any, 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 ._util import DeferredError, is_path 

60from .TiffTags import TYPES 

61 

62TYPE_CHECKING = False 

63if TYPE_CHECKING: 

64 from collections.abc import Iterator 

65 from typing import NoReturn 

66 

67 from ._typing import Buffer, IntegralLike, StrOrBytesPath 

68 

69logger = logging.getLogger(__name__) 

70 

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

72READ_LIBTIFF = False 

73WRITE_LIBTIFF = False 

74STRIP_SIZE = 65536 

75 

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

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

78 

79# 

80# -------------------------------------------------------------------- 

81# Read TIFF files 

82 

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

84OSUBFILETYPE = 255 

85IMAGEWIDTH = 256 

86IMAGELENGTH = 257 

87BITSPERSAMPLE = 258 

88COMPRESSION = 259 

89PHOTOMETRIC_INTERPRETATION = 262 

90FILLORDER = 266 

91IMAGEDESCRIPTION = 270 

92STRIPOFFSETS = 273 

93SAMPLESPERPIXEL = 277 

94ROWSPERSTRIP = 278 

95STRIPBYTECOUNTS = 279 

96X_RESOLUTION = 282 

97Y_RESOLUTION = 283 

98PLANAR_CONFIGURATION = 284 

99RESOLUTION_UNIT = 296 

100TRANSFERFUNCTION = 301 

101SOFTWARE = 305 

102DATE_TIME = 306 

103ARTIST = 315 

104PREDICTOR = 317 

105COLORMAP = 320 

106TILEWIDTH = 322 

107TILELENGTH = 323 

108TILEOFFSETS = 324 

109TILEBYTECOUNTS = 325 

110SUBIFD = 330 

111EXTRASAMPLES = 338 

112SAMPLEFORMAT = 339 

113JPEGTABLES = 347 

114YCBCRSUBSAMPLING = 530 

115REFERENCEBLACKWHITE = 532 

116COPYRIGHT = 33432 

117IPTC_NAA_CHUNK = 33723 # newsphoto properties 

118PHOTOSHOP_CHUNK = 34377 # photoshop properties 

119ICCPROFILE = 34675 

120EXIFIFD = 34665 

121XMP = 700 

122JPEGQUALITY = 65537 # pseudo-tag by libtiff 

123 

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

125IMAGEJ_META_DATA_BYTE_COUNTS = 50838 

126IMAGEJ_META_DATA = 50839 

127 

128COMPRESSION_INFO = { 

129 # Compression => pil compression name 

130 1: "raw", 

131 2: "tiff_ccitt", 

132 3: "group3", 

133 4: "group4", 

134 5: "tiff_lzw", 

135 6: "tiff_jpeg", # obsolete 

136 7: "jpeg", 

137 8: "tiff_adobe_deflate", 

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

139 32773: "packbits", 

140 32809: "tiff_thunderscan", 

141 32946: "tiff_deflate", 

142 34676: "tiff_sgilog", 

143 34677: "tiff_sgilog24", 

144 34925: "lzma", 

145 50000: "zstd", 

146 50001: "webp", 

147} 

148 

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

150 

151OPEN_INFO = { 

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

153 # ExtraSamples) => mode, rawmode 

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

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

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

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

158 (II, 1, (1,), 1, (1,), ()): ("1", "1"), 

159 (MM, 1, (1,), 1, (1,), ()): ("1", "1"), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

275} 

276 

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

278 

279PREFIXES = [ 

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

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

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

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

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

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

286] 

287 

288 

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

290 return prefix.startswith(tuple(PREFIXES)) 

291 

292 

293def _limit_rational( 

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

295) -> tuple[IntegralLike, IntegralLike]: 

296 inv = abs(val) > 1 

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

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

299 

300 

301def _limit_signed_rational( 

302 val: IFDRational, max_val: int, min_val: int 

303) -> tuple[IntegralLike, IntegralLike]: 

304 frac = Fraction(val) 

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

306 

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

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

309 

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

311 if max(n_d_float) > max_val: 

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

313 

314 return n_d 

315 

316 

317## 

318# Wrapper for TIFF IFDs. 

319 

320_load_dispatch = {} 

321_write_dispatch = {} 

322 

323 

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

325 def delegate( 

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

327 ) -> bool | float | Fraction: 

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

329 

330 return delegate 

331 

332 

333class IFDRational(Rational): 

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

335 the in the wild use of exif rationals. 

336 

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

338 """ 

339 

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

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

342 

343 """ 

344 

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

346 

347 def __init__( 

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

349 ) -> None: 

350 """ 

351 :param value: either an integer numerator, a 

352 float/rational/other number, or an IFDRational 

353 :param denominator: Optional integer denominator 

354 """ 

355 self._val: Fraction | float 

356 if isinstance(value, IFDRational): 

357 self._numerator = value.numerator 

358 self._denominator = value.denominator 

359 self._val = value._val 

360 return 

361 

362 if isinstance(value, Fraction): 

363 self._numerator = value.numerator 

364 self._denominator = value.denominator 

365 else: 

366 if TYPE_CHECKING: 

367 self._numerator = cast(IntegralLike, value) 

368 else: 

369 self._numerator = value 

370 self._denominator = denominator 

371 

372 if denominator == 0: 

373 self._val = float("nan") 

374 elif denominator == 1: 

375 self._val = Fraction(value) 

376 elif int(value) == value: 

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

378 else: 

379 self._val = Fraction(value / denominator) 

380 

381 @property 

382 def numerator(self) -> IntegralLike: 

383 return self._numerator 

384 

385 @property 

386 def denominator(self) -> int: 

387 return self._denominator 

388 

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

390 """ 

391 

392 :param max_denominator: Integer, the maximum denominator value 

393 :returns: Tuple of (numerator, denominator) 

394 """ 

395 

396 if self.denominator == 0: 

397 return self.numerator, self.denominator 

398 

399 assert isinstance(self._val, Fraction) 

400 f = self._val.limit_denominator(max_denominator) 

401 return f.numerator, f.denominator 

402 

403 def __repr__(self) -> str: 

404 return str(float(self._val)) 

405 

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

407 return self._val.__hash__() 

408 

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

410 val = self._val 

411 if isinstance(other, IFDRational): 

412 other = other._val 

413 if isinstance(other, float): 

414 val = float(val) 

415 return val == other 

416 

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

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

419 

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

421 IFDRational.__init__(self, 0) 

422 _val, _numerator, _denominator = state 

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

424 self._val = _val 

425 if TYPE_CHECKING: 

426 self._numerator = cast(IntegralLike, _numerator) 

427 else: 

428 self._numerator = _numerator 

429 assert isinstance(_denominator, int) 

430 self._denominator = _denominator 

431 

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

433 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 

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

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

436 'ceil', 'floor', 'round'] 

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

438 """ 

439 

440 __add__ = _delegate("__add__") 

441 __radd__ = _delegate("__radd__") 

442 __sub__ = _delegate("__sub__") 

443 __rsub__ = _delegate("__rsub__") 

444 __mul__ = _delegate("__mul__") 

445 __rmul__ = _delegate("__rmul__") 

446 __truediv__ = _delegate("__truediv__") 

447 __rtruediv__ = _delegate("__rtruediv__") 

448 __floordiv__ = _delegate("__floordiv__") 

449 __rfloordiv__ = _delegate("__rfloordiv__") 

450 __mod__ = _delegate("__mod__") 

451 __rmod__ = _delegate("__rmod__") 

452 __pow__ = _delegate("__pow__") 

453 __rpow__ = _delegate("__rpow__") 

454 __pos__ = _delegate("__pos__") 

455 __neg__ = _delegate("__neg__") 

456 __abs__ = _delegate("__abs__") 

457 __trunc__ = _delegate("__trunc__") 

458 __lt__ = _delegate("__lt__") 

459 __gt__ = _delegate("__gt__") 

460 __le__ = _delegate("__le__") 

461 __ge__ = _delegate("__ge__") 

462 __bool__ = _delegate("__bool__") 

463 __ceil__ = _delegate("__ceil__") 

464 __floor__ = _delegate("__floor__") 

465 __round__ = _delegate("__round__") 

466 # Python >= 3.11 

467 if hasattr(Fraction, "__int__"): 

468 __int__ = _delegate("__int__") 

469 

470 

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

472 

473 

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

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

476 from .TiffTags import TYPES 

477 

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

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

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

481 return func 

482 

483 return decorator 

484 

485 

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

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

488 _write_dispatch[idx] = func # noqa: F821 

489 return func 

490 

491 return decorator 

492 

493 

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

495 from .TiffTags import TYPES 

496 

497 idx, fmt, name = idx_fmt_name 

498 TYPES[idx] = name 

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

500 

501 def basic_handler( 

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

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

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

505 

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

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

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

509 ) 

510 

511 

512if TYPE_CHECKING: 

513 _IFDv2Base = MutableMapping[int, Any] 

514else: 

515 _IFDv2Base = MutableMapping 

516 

517 

518class ImageFileDirectory_v2(_IFDv2Base): 

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

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

521 

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

523 

524 ifd = ImageFileDirectory_v2() 

525 ifd[key] = 'Some Data' 

526 ifd.tagtype[key] = TiffTags.ASCII 

527 print(ifd[key]) 

528 'Some Data' 

529 

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

531 returned as tuples of the values. 

532 

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

534 tag types in 

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

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

537 manually. 

538 

539 Data Structures: 

540 

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

542 

543 * Key: numerical TIFF tag number 

544 * Value: integer corresponding to the data type from 

545 :py:data:`.TiffTags.TYPES` 

546 

547 .. versionadded:: 3.0.0 

548 

549 'Internal' data structures: 

550 

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

552 

553 * Key: numerical TIFF tag number 

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

555 

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

557 

558 * Key: numerical TIFF tag number 

559 * Value: undecoded byte string from file 

560 

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

562 

563 * Key: numerical TIFF tag number 

564 * Value: decoded data in the v1 format 

565 

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

567 ``self._tags_v2`` once decoded. 

568 

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

570 from outside code. In cooperation with 

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

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

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

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

575 ``legacy_api == true``. 

576 

577 """ 

578 

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

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

581 

582 def __init__( 

583 self, 

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

585 prefix: bytes | None = None, 

586 group: int | None = None, 

587 ) -> None: 

588 """Initialize an ImageFileDirectory. 

589 

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

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

592 as the 'prefix' keyword argument. 

593 

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

595 endianness. 

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

597 """ 

598 if not _accept(ifh): 

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

600 raise SyntaxError(msg) 

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

602 if self._prefix == MM: 

603 self._endian = ">" 

604 elif self._prefix == II: 

605 self._endian = "<" 

606 else: 

607 msg = "not a TIFF IFD" 

608 raise SyntaxError(msg) 

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

610 self.group = group 

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

612 """ Dictionary of tag types """ 

613 self.reset() 

614 self.next = ( 

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

616 if self._bigtiff 

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

618 ) 

619 self._legacy_api = False 

620 

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

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

623 

624 @property 

625 def legacy_api(self) -> bool: 

626 return self._legacy_api 

627 

628 @legacy_api.setter 

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

630 msg = "Not allowing setting of legacy api" 

631 raise Exception(msg) 

632 

633 def reset(self) -> None: 

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

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

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

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

638 self._next = None 

639 self._offset: int | None = None 

640 

641 def __str__(self) -> str: 

642 return str(dict(self)) 

643 

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

645 """ 

646 :returns: dict of name|key: value 

647 

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

649 """ 

650 return { 

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

652 for code, value in self.items() 

653 } 

654 

655 def __len__(self) -> int: 

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

657 

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

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

660 data = self._tagdata[tag] 

661 typ = self.tagtype[tag] 

662 size, handler = self._load_dispatch[typ] 

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

664 val = self._tags_v2[tag] 

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

666 val = (val,) 

667 return val 

668 

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

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

671 

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

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

674 

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

676 basetypes = (Number, bytes, str) 

677 

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

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

680 

681 if tag not in self.tagtype: 

682 if info.type: 

683 self.tagtype[tag] = info.type 

684 else: 

685 self.tagtype[tag] = TiffTags.UNDEFINED 

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

687 for v in values: 

688 assert isinstance(v, IFDRational) 

689 if v < 0: 

690 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL 

691 break 

692 else: 

693 self.tagtype[tag] = TiffTags.RATIONAL 

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

695 short = True 

696 signed_short = True 

697 long = True 

698 for v in values: 

699 assert isinstance(v, int) 

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

701 short = False 

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

703 signed_short = False 

704 if long and v < 0: 

705 long = False 

706 if short: 

707 self.tagtype[tag] = TiffTags.SHORT 

708 elif signed_short: 

709 self.tagtype[tag] = TiffTags.SIGNED_SHORT 

710 elif long: 

711 self.tagtype[tag] = TiffTags.LONG 

712 else: 

713 self.tagtype[tag] = TiffTags.SIGNED_LONG 

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

715 self.tagtype[tag] = TiffTags.DOUBLE 

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

717 self.tagtype[tag] = TiffTags.ASCII 

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

719 self.tagtype[tag] = TiffTags.BYTE 

720 

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

722 values = [ 

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

724 for v in values 

725 ] 

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

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

728 

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

730 if not is_ifd: 

731 values = tuple( 

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

733 for value in values 

734 ) 

735 

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

737 

738 # Three branches: 

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

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

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

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

743 if not is_ifd and ( 

744 (info.length == 1) 

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

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

747 ): 

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

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

750 TiffTags.RATIONAL, 

751 TiffTags.SIGNED_RATIONAL, 

752 ]: # rationals 

753 values = (values,) 

754 try: 

755 (dest[tag],) = values 

756 except ValueError: 

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

758 warnings.warn( 

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

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

761 ) 

762 dest[tag] = values[0] 

763 

764 else: 

765 # Spec'd length > 1 or undefined 

766 # Unspec'd, and length > 1 

767 dest[tag] = values 

768 

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

770 self._tags_v2.pop(tag, None) 

771 self._tags_v1.pop(tag, None) 

772 self._tagdata.pop(tag, None) 

773 

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

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

776 

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

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

779 

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

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

782 

783 list( 

784 map( 

785 _register_basic, 

786 [ 

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

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

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

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

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

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

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

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

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

796 ], 

797 ) 

798 ) 

799 

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

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

802 return data 

803 

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

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

806 if isinstance(data, IFDRational): 

807 data = int(data) 

808 if isinstance(data, int): 

809 data = bytes((data,)) 

810 return data 

811 

812 @_register_loader(2, 1) 

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

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

815 data = data[:-1] 

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

817 

818 @_register_writer(2) 

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

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

821 if isinstance(value, int): 

822 value = str(value) 

823 if not isinstance(value, bytes): 

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

825 return value + b"\0" 

826 

827 @_register_loader(5, 8) 

828 def load_rational( 

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

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

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

832 

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

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

835 

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

837 

838 @_register_writer(5) 

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

840 return b"".join( 

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

842 ) 

843 

844 @_register_loader(7, 1) 

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

846 return data 

847 

848 @_register_writer(7) 

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

850 if isinstance(value, IFDRational): 

851 value = int(value) 

852 if isinstance(value, int): 

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

854 return value 

855 

856 @_register_loader(10, 8) 

857 def load_signed_rational( 

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

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

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

861 

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

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

864 

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

866 

867 @_register_writer(10) 

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

869 return b"".join( 

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

871 for frac in values 

872 ) 

873 

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

875 ret = fp.read(size) 

876 if len(ret) != size: 

877 msg = ( 

878 "Corrupt EXIF data. " 

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

880 ) 

881 raise OSError(msg) 

882 return ret 

883 

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

885 self.reset() 

886 self._offset = fp.tell() 

887 

888 try: 

889 tag_count = ( 

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

891 if self._bigtiff 

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

893 )[0] 

894 for i in range(tag_count): 

895 tag, typ, count, data = ( 

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

897 if self._bigtiff 

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

899 ) 

900 

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

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

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

904 

905 try: 

906 unit_size, handler = self._load_dispatch[typ] 

907 except KeyError: 

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

909 continue # ignore unsupported type 

910 size = count * unit_size 

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

912 here = fp.tell() 

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

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

915 fp.seek(offset) 

916 data = ImageFile._safe_read(fp, size) 

917 fp.seek(here) 

918 else: 

919 data = data[:size] 

920 

921 if len(data) != size: 

922 warnings.warn( 

923 "Possibly corrupt EXIF data. " 

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

925 f" Skipping tag {tag}" 

926 ) 

927 logger.debug(msg) 

928 continue 

929 

930 if not data: 

931 logger.debug(msg) 

932 continue 

933 

934 self._tagdata[tag] = data 

935 self.tagtype[tag] = typ 

936 

937 msg += " - value: " 

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

939 

940 logger.debug(msg) 

941 

942 (self.next,) = ( 

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

944 if self._bigtiff 

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

946 ) 

947 except OSError as msg: 

948 warnings.warn(str(msg)) 

949 return 

950 

951 def _get_ifh(self) -> bytes: 

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

953 if self._bigtiff: 

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

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

956 

957 return ifh 

958 

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

960 # FIXME What about tagdata? 

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

962 

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

964 

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

966 fmt_size = 8 if self._bigtiff else 4 

967 offset += ( 

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

969 ) 

970 stripoffsets = None 

971 

972 # pass 1: convert tags to binary format 

973 # always write tags in ascending order 

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

975 if tag == STRIPOFFSETS: 

976 stripoffsets = len(entries) 

977 typ = self.tagtype[tag] 

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

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

980 if is_ifd: 

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

982 values = self._tags_v2[tag] 

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

984 ifd[ifd_tag] = ifd_value 

985 data = ifd.tobytes(offset) 

986 else: 

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

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

989 

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

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

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

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

994 logger.debug(msg) 

995 

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

997 if is_ifd: 

998 count = 1 

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

1000 count = len(data) 

1001 else: 

1002 count = len(values) 

1003 # figure out if data fits into the entry 

1004 if len(data) <= fmt_size: 

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

1006 else: 

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

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

1009 

1010 # update strip offset data to point beyond auxiliary data 

1011 if stripoffsets is not None: 

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

1013 if data: 

1014 size, handler = self._load_dispatch[typ] 

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

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

1017 else: 

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

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

1020 

1021 # pass 2: write entries to file 

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

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

1024 result += self._pack( 

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

1026 ) 

1027 

1028 # -- overwrite here for multi-page -- 

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

1030 

1031 # pass 3: write auxiliary data to file 

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

1033 result += data 

1034 if len(data) & 1: 

1035 result += b"\0" 

1036 

1037 return result 

1038 

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

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

1041 fp.write(self._get_ifh()) 

1042 

1043 offset = fp.tell() 

1044 result = self.tobytes(offset) 

1045 fp.write(result) 

1046 return offset + len(result) 

1047 

1048 

1049ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1050ImageFileDirectory_v2._write_dispatch = _write_dispatch 

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

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

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

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

1055del _load_dispatch, _write_dispatch, idx, name 

1056 

1057 

1058# Legacy ImageFileDirectory support. 

1059class ImageFileDirectory_v1(ImageFileDirectory_v2): 

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

1061 

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

1063 

1064 ifd = ImageFileDirectory_v1() 

1065 ifd[key] = 'Some Data' 

1066 ifd.tagtype[key] = TiffTags.ASCII 

1067 print(ifd[key]) 

1068 ('Some Data',) 

1069 

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

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

1072 

1073 Values are returned as a tuple. 

1074 

1075 .. deprecated:: 3.0.0 

1076 """ 

1077 

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

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

1080 self._legacy_api = True 

1081 

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

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

1084 

1085 # defined in ImageFileDirectory_v2 

1086 tagtype: dict[int, int] 

1087 """Dictionary of tag types""" 

1088 

1089 @classmethod 

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

1091 """Returns an 

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

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

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

1095 instance. 

1096 

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

1098 

1099 """ 

1100 

1101 ifd = cls(prefix=original.prefix) 

1102 ifd._tagdata = original._tagdata 

1103 ifd.tagtype = original.tagtype 

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

1105 return ifd 

1106 

1107 def to_v2(self) -> ImageFileDirectory_v2: 

1108 """Returns an 

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

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

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

1112 instance. 

1113 

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

1115 

1116 """ 

1117 

1118 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1119 ifd._tagdata = dict(self._tagdata) 

1120 ifd.tagtype = dict(self.tagtype) 

1121 ifd._tags_v2 = dict(self._tags_v2) 

1122 return ifd 

1123 

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

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

1126 

1127 def __len__(self) -> int: 

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

1129 

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

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

1132 

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

1134 for legacy_api in (False, True): 

1135 self._setitem(tag, value, legacy_api) 

1136 

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

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

1139 data = self._tagdata[tag] 

1140 typ = self.tagtype[tag] 

1141 size, handler = self._load_dispatch[typ] 

1142 for legacy in (False, True): 

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

1144 val = self._tags_v1[tag] 

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

1146 val = (val,) 

1147 return val 

1148 

1149 

1150# undone -- switch this pointer 

1151ImageFileDirectory = ImageFileDirectory_v1 

1152 

1153 

1154## 

1155# Image plugin for TIFF files. 

1156 

1157 

1158class TiffImageFile(ImageFile.ImageFile): 

1159 format = "TIFF" 

1160 format_description = "Adobe TIFF" 

1161 _close_exclusive_fp_after_loading = False 

1162 

1163 def __init__( 

1164 self, 

1165 fp: StrOrBytesPath | IO[bytes], 

1166 filename: str | bytes | None = None, 

1167 ) -> None: 

1168 self.tag_v2: ImageFileDirectory_v2 

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

1170 

1171 self.tag: ImageFileDirectory_v1 

1172 """ Legacy tag entries """ 

1173 

1174 super().__init__(fp, filename) 

1175 

1176 def _open(self) -> None: 

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

1178 

1179 # Header 

1180 ifh = self.fp.read(8) 

1181 if ifh[2] == 43: 

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

1183 

1184 self.tag_v2 = ImageFileDirectory_v2(ifh) 

1185 

1186 # setup frame pointers 

1187 self.__first = self.__next = self.tag_v2.next 

1188 self.__frame = -1 

1189 self._fp = self.fp 

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

1191 self._n_frames: int | None = None 

1192 

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

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

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

1196 

1197 # and load the first frame 

1198 self._seek(0) 

1199 

1200 @property 

1201 def n_frames(self) -> int: 

1202 current_n_frames = self._n_frames 

1203 if current_n_frames is None: 

1204 current = self.tell() 

1205 self._seek(len(self._frame_pos)) 

1206 while self._n_frames is None: 

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

1208 self.seek(current) 

1209 assert self._n_frames is not None 

1210 return self._n_frames 

1211 

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

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

1214 if not self._seek_check(frame): 

1215 return 

1216 self._seek(frame) 

1217 if self._im is not None and ( 

1218 self.im.size != self._tile_size 

1219 or self.im.mode != self.mode 

1220 or self.readonly 

1221 ): 

1222 self._im = None 

1223 

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

1225 if isinstance(self._fp, DeferredError): 

1226 raise self._fp.ex 

1227 self.fp = self._fp 

1228 

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

1230 if not self.__next: 

1231 msg = "no more images in TIFF file" 

1232 raise EOFError(msg) 

1233 logger.debug( 

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

1235 frame, 

1236 self.__frame, 

1237 self.__next, 

1238 self.fp.tell(), 

1239 ) 

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

1241 msg = "Unable to seek to frame" 

1242 raise ValueError(msg) 

1243 self.fp.seek(self.__next) 

1244 self._frame_pos.append(self.__next) 

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

1246 self.tag_v2.load(self.fp) 

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

1248 # This IFD has already been processed 

1249 # Declare this to be the end of the image 

1250 self.__next = 0 

1251 else: 

1252 self.__next = self.tag_v2.next 

1253 if self.__next == 0: 

1254 self._n_frames = frame + 1 

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

1256 self.is_animated = self.__next != 0 

1257 self.__frame += 1 

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

1259 self.tag_v2.load(self.fp) 

1260 if XMP in self.tag_v2: 

1261 xmp = self.tag_v2[XMP] 

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

1263 xmp = xmp[0] 

1264 self.info["xmp"] = xmp 

1265 elif "xmp" in self.info: 

1266 del self.info["xmp"] 

1267 self._reload_exif() 

1268 # fill the legacy tag/ifd entries 

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

1270 self.__frame = frame 

1271 self._setup() 

1272 

1273 def tell(self) -> int: 

1274 """Return the current frame number""" 

1275 return self.__frame 

1276 

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

1278 """ 

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

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

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

1282 

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

1284 """ 

1285 blocks = {} 

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

1287 if val: 

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

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

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

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

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

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

1294 

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

1296 return blocks 

1297 

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

1299 if self.tile and self.use_load_libtiff: 

1300 return self._load_libtiff() 

1301 return super().load() 

1302 

1303 def load_prepare(self) -> None: 

1304 if self._im is None: 

1305 Image._decompression_bomb_check(self._tile_size) 

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

1307 ImageFile.ImageFile.load_prepare(self) 

1308 

1309 def load_end(self) -> None: 

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

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

1312 if not self.is_animated: 

1313 self._close_exclusive_fp_after_loading = True 

1314 

1315 # load IFD data from fp before it is closed 

1316 exif = self.getexif() 

1317 for key in TiffTags.TAGS_V2_GROUPS: 

1318 if key not in exif: 

1319 continue 

1320 exif.get_ifd(key) 

1321 

1322 ImageOps.exif_transpose(self, in_place=True) 

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

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

1325 

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

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

1328 Calls out to libtiff""" 

1329 

1330 Image.Image.load(self) 

1331 

1332 self.load_prepare() 

1333 

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

1335 msg = "Not exactly one tile" 

1336 raise OSError(msg) 

1337 

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

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

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

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

1342 

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

1344 # file descriptor, use that instead of reading 

1345 # into a string in python. 

1346 try: 

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

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

1349 # should also eliminate the need for fp.tell 

1350 # in _seek 

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

1352 self.fp.flush() 

1353 except OSError: 

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

1355 # it doesn't use a file descriptor. 

1356 fp = False 

1357 

1358 if fp: 

1359 assert isinstance(args, tuple) 

1360 args_list = list(args) 

1361 args_list[2] = fp 

1362 args = tuple(args_list) 

1363 

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

1365 try: 

1366 decoder.setimage(self.im, extents) 

1367 except ValueError as e: 

1368 msg = "Couldn't set the image" 

1369 raise OSError(msg) from e 

1370 

1371 close_self_fp = self._exclusive_fp and not self.is_animated 

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

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

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

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

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

1377 # underlying string for stringio. 

1378 # 

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

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

1381 # deal with here by reordering. 

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

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

1384 elif fp: 

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

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

1387 if not close_self_fp: 

1388 self.fp.seek(0) 

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

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

1391 # io.BufferedReader and possible others. 

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

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

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

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

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

1397 # 4 bytes, otherwise the trace might error out 

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

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

1400 else: 

1401 # we have something else. 

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

1403 self.fp.seek(0) 

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

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

1406 

1407 self.tile = [] 

1408 self.readonly = 0 

1409 

1410 self.load_end() 

1411 

1412 if close_self_fp: 

1413 self.fp.close() 

1414 self.fp = None # might be shared 

1415 

1416 if err < 0: 

1417 msg = f"decoder error {err}" 

1418 raise OSError(msg) 

1419 

1420 return Image.Image.load(self) 

1421 

1422 def _setup(self) -> None: 

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

1424 

1425 if 0xBC01 in self.tag_v2: 

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

1427 raise OSError(msg) 

1428 

1429 # extract relevant tags 

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

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

1432 

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

1434 # the specification 

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

1436 

1437 # old style jpeg compression images most certainly are YCbCr 

1438 if self._compression == "tiff_jpeg": 

1439 photo = 6 

1440 

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

1442 

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

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

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

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

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

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

1449 

1450 # size 

1451 try: 

1452 xsize = self.tag_v2[IMAGEWIDTH] 

1453 ysize = self.tag_v2[IMAGELENGTH] 

1454 except KeyError as e: 

1455 msg = "Missing dimensions" 

1456 raise TypeError(msg) from e 

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

1458 msg = "Invalid dimensions" 

1459 raise ValueError(msg) 

1460 self._tile_size = xsize, ysize 

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

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

1463 self._size = ysize, xsize 

1464 else: 

1465 self._size = xsize, ysize 

1466 

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

1468 

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

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

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

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

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

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

1475 # for more exotic images. 

1476 sample_format = (1,) 

1477 

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

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

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

1481 bps_count = 3 

1482 elif photo == 5: # CMYK 

1483 bps_count = 4 

1484 else: 

1485 bps_count = 1 

1486 bps_count += len(extra_tuple) 

1487 bps_actual_count = len(bps_tuple) 

1488 samples_per_pixel = self.tag_v2.get( 

1489 SAMPLESPERPIXEL, 

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

1491 ) 

1492 

1493 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

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

1495 logger.error( 

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

1497 ) 

1498 msg = "Invalid value for samples per pixel" 

1499 raise SyntaxError(msg) 

1500 

1501 if samples_per_pixel < bps_actual_count: 

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

1503 # remove the excess. 

1504 bps_tuple = bps_tuple[:samples_per_pixel] 

1505 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

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

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

1508 bps_tuple = bps_tuple * samples_per_pixel 

1509 

1510 if len(bps_tuple) != samples_per_pixel: 

1511 msg = "unknown data organization" 

1512 raise SyntaxError(msg) 

1513 

1514 # mode: check photometric interpretation and bits per pixel 

1515 key = ( 

1516 self.tag_v2.prefix, 

1517 photo, 

1518 sample_format, 

1519 fillorder, 

1520 bps_tuple, 

1521 extra_tuple, 

1522 ) 

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

1524 try: 

1525 self._mode, rawmode = OPEN_INFO[key] 

1526 except KeyError as e: 

1527 logger.debug("- unsupported format") 

1528 msg = "unknown pixel mode" 

1529 raise SyntaxError(msg) from e 

1530 

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

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

1533 

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

1535 

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

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

1538 

1539 if xres and yres: 

1540 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1541 if resunit == 2: # dots per inch 

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

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

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

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

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

1547 # For backward compatibility, 

1548 # we also preserve the old behavior 

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

1550 else: # No absolute unit of measurement 

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

1552 

1553 # build tile descriptors 

1554 x = y = layer = 0 

1555 self.tile = [] 

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

1557 if self.use_load_libtiff: 

1558 # Decoder expects entire file as one tile. 

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

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

1561 # function. 

1562 # 

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

1564 # use the _load_libtiff function. 

1565 

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

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

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

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

1570 if fillorder == 2: 

1571 # Replace fillorder with fillorder=1 

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

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

1574 # this should always work, since all the 

1575 # fillorder==2 modes have a corresponding 

1576 # fillorder=1 mode 

1577 self._mode, rawmode = OPEN_INFO[key] 

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

1579 # unpacked straight into RGB values 

1580 if ( 

1581 photo == 6 

1582 and self._compression == "jpeg" 

1583 and self._planar_configuration == 1 

1584 ): 

1585 rawmode = "RGB" 

1586 # libtiff always returns the bytes in native order. 

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

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

1589 # byte order. 

1590 elif rawmode == "I;16": 

1591 rawmode = "I;16N" 

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

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

1594 

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

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

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

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

1599 

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

1601 # striped image 

1602 if STRIPOFFSETS in self.tag_v2: 

1603 offsets = self.tag_v2[STRIPOFFSETS] 

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

1605 w = xsize 

1606 else: 

1607 # tiled image 

1608 offsets = self.tag_v2[TILEOFFSETS] 

1609 tilewidth = self.tag_v2.get(TILEWIDTH) 

1610 h = self.tag_v2.get(TILELENGTH) 

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

1612 msg = "Invalid tile dimensions" 

1613 raise ValueError(msg) 

1614 w = tilewidth 

1615 

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

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

1618 offsets = offsets[-1:] 

1619 

1620 for offset in offsets: 

1621 if x + w > xsize: 

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

1623 else: 

1624 stride = 0 

1625 

1626 tile_rawmode = rawmode 

1627 if self._planar_configuration == 2: 

1628 # each band on it's own layer 

1629 tile_rawmode = rawmode[layer] 

1630 # adjust stride width accordingly 

1631 stride /= bps_count 

1632 

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

1634 self.tile.append( 

1635 ImageFile._Tile( 

1636 self._compression, 

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

1638 offset, 

1639 args, 

1640 ) 

1641 ) 

1642 x += w 

1643 if x >= xsize: 

1644 x, y = 0, y + h 

1645 if y >= ysize: 

1646 y = 0 

1647 layer += 1 

1648 else: 

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

1650 msg = "unknown data organization" 

1651 raise SyntaxError(msg) 

1652 

1653 # Fix up info. 

1654 if ICCPROFILE in self.tag_v2: 

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

1656 

1657 # fixup palette descriptor 

1658 

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

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

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

1662 

1663 

1664# 

1665# -------------------------------------------------------------------- 

1666# Write TIFF files 

1667 

1668# little endian is default except for image modes with 

1669# explicit big endian byte-order 

1670 

1671SAVE_INFO = { 

1672 # mode => rawmode, byteorder, photometrics, 

1673 # sampleformat, bitspersample, extra 

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

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

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

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

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

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

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

1681 "I;16L": ("I;16L", II, 1, 1, (16,), None), 

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

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

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

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

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

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

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

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

1690} 

1691 

1692 

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

1694 try: 

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

1696 except KeyError as e: 

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

1698 raise OSError(msg) from e 

1699 

1700 encoderinfo = im.encoderinfo 

1701 encoderconfig = im.encoderconfig 

1702 

1703 ifd = ImageFileDirectory_v2(prefix=prefix) 

1704 if encoderinfo.get("big_tiff"): 

1705 ifd._bigtiff = True 

1706 

1707 try: 

1708 compression = encoderinfo["compression"] 

1709 except KeyError: 

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

1711 if isinstance(compression, int): 

1712 # compression value may be from BMP. Ignore it 

1713 compression = None 

1714 if compression is None: 

1715 compression = "raw" 

1716 elif compression == "tiff_jpeg": 

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

1718 compression = "jpeg" 

1719 elif compression == "tiff_deflate": 

1720 compression = "tiff_adobe_deflate" 

1721 

1722 libtiff = WRITE_LIBTIFF or compression != "raw" 

1723 

1724 # required for color libtiff images 

1725 ifd[PLANAR_CONFIGURATION] = 1 

1726 

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

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

1729 

1730 # write any arbitrary tags passed in as an ImageFileDirectory 

1731 if "tiffinfo" in encoderinfo: 

1732 info = encoderinfo["tiffinfo"] 

1733 elif "exif" in encoderinfo: 

1734 info = encoderinfo["exif"] 

1735 if isinstance(info, bytes): 

1736 exif = Image.Exif() 

1737 exif.load(info) 

1738 info = exif 

1739 else: 

1740 info = {} 

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

1742 if isinstance(info, ImageFileDirectory_v1): 

1743 info = info.to_v2() 

1744 for key in info: 

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

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

1747 else: 

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

1749 try: 

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

1751 except Exception: 

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

1753 

1754 legacy_ifd = {} 

1755 if hasattr(im, "tag"): 

1756 legacy_ifd = im.tag.to_v2() 

1757 

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

1759 for tag in ( 

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

1761 EXIFIFD, 

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

1763 SAMPLEFORMAT, 

1764 ): 

1765 if tag in supplied_tags: 

1766 del supplied_tags[tag] 

1767 

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

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

1770 if hasattr(im, "tag_v2"): 

1771 # preserve tags from original TIFF image file 

1772 for key in ( 

1773 RESOLUTION_UNIT, 

1774 X_RESOLUTION, 

1775 Y_RESOLUTION, 

1776 IPTC_NAA_CHUNK, 

1777 PHOTOSHOP_CHUNK, 

1778 XMP, 

1779 ): 

1780 if key in im.tag_v2: 

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

1782 TiffTags.BYTE, 

1783 TiffTags.UNDEFINED, 

1784 ): 

1785 del supplied_tags[key] 

1786 else: 

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

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

1789 

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

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

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

1793 if icc: 

1794 ifd[ICCPROFILE] = icc 

1795 

1796 for key, name in [ 

1797 (IMAGEDESCRIPTION, "description"), 

1798 (X_RESOLUTION, "resolution"), 

1799 (Y_RESOLUTION, "resolution"), 

1800 (X_RESOLUTION, "x_resolution"), 

1801 (Y_RESOLUTION, "y_resolution"), 

1802 (RESOLUTION_UNIT, "resolution_unit"), 

1803 (SOFTWARE, "software"), 

1804 (DATE_TIME, "date_time"), 

1805 (ARTIST, "artist"), 

1806 (COPYRIGHT, "copyright"), 

1807 ]: 

1808 if name in encoderinfo: 

1809 ifd[key] = encoderinfo[name] 

1810 

1811 dpi = encoderinfo.get("dpi") 

1812 if dpi: 

1813 ifd[RESOLUTION_UNIT] = 2 

1814 ifd[X_RESOLUTION] = dpi[0] 

1815 ifd[Y_RESOLUTION] = dpi[1] 

1816 

1817 if bits != (1,): 

1818 ifd[BITSPERSAMPLE] = bits 

1819 if len(bits) != 1: 

1820 ifd[SAMPLESPERPIXEL] = len(bits) 

1821 if extra is not None: 

1822 ifd[EXTRASAMPLES] = extra 

1823 if format != 1: 

1824 ifd[SAMPLEFORMAT] = format 

1825 

1826 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1827 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

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

1829 if im.mode == "1": 

1830 inverted_im = im.copy() 

1831 px = inverted_im.load() 

1832 if px is not None: 

1833 for y in range(inverted_im.height): 

1834 for x in range(inverted_im.width): 

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

1836 im = inverted_im 

1837 else: 

1838 im = ImageOps.invert(im) 

1839 

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

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

1842 colormap = [] 

1843 colors = len(lut) // 3 

1844 for i in range(3): 

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

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

1847 ifd[COLORMAP] = colormap 

1848 # data orientation 

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

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

1851 if ROWSPERSTRIP not in ifd: 

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

1853 if libtiff: 

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

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

1856 # JPEG encoder expects multiple of 8 rows 

1857 if compression == "jpeg": 

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

1859 else: 

1860 rows_per_strip = h 

1861 if rows_per_strip == 0: 

1862 rows_per_strip = 1 

1863 ifd[ROWSPERSTRIP] = rows_per_strip 

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

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

1866 if strip_byte_counts >= 2**16: 

1867 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

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

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

1870 ) 

1871 ifd[STRIPOFFSETS] = tuple( 

1872 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1873 ) # this is adjusted by IFD writer 

1874 # no compression by default: 

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

1876 

1877 if im.mode == "YCbCr": 

1878 for tag, default_value in { 

1879 YCBCRSUBSAMPLING: (1, 1), 

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

1881 }.items(): 

1882 ifd.setdefault(tag, default_value) 

1883 

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

1885 if libtiff: 

1886 if "quality" in encoderinfo: 

1887 quality = encoderinfo["quality"] 

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

1889 msg = "Invalid quality setting" 

1890 raise ValueError(msg) 

1891 if compression != "jpeg": 

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

1893 raise ValueError(msg) 

1894 ifd[JPEGQUALITY] = quality 

1895 

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

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

1898 _fp = 0 

1899 if hasattr(fp, "fileno"): 

1900 try: 

1901 fp.seek(0) 

1902 _fp = fp.fileno() 

1903 except io.UnsupportedOperation: 

1904 pass 

1905 

1906 # optional types for non core tags 

1907 types = {} 

1908 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1909 # based on the data in the strip. 

1910 # OSUBFILETYPE is deprecated. 

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

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

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

1914 # SUBIFD may also cause a segfault. 

1915 blocklist += [ 

1916 OSUBFILETYPE, 

1917 REFERENCEBLACKWHITE, 

1918 STRIPBYTECOUNTS, 

1919 STRIPOFFSETS, 

1920 TRANSFERFUNCTION, 

1921 SUBIFD, 

1922 ] 

1923 

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

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

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

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

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

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

1930 # Libtiff can only process certain core items without adding 

1931 # them to the custom dictionary. 

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

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

1934 if tag not in TiffTags.LIBTIFF_CORE: 

1935 if tag in TiffTags.TAGS_V2_GROUPS: 

1936 types[tag] = TiffTags.LONG8 

1937 elif tag in ifd.tagtype: 

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

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

1940 continue 

1941 else: 

1942 type = TiffTags.lookup(tag).type 

1943 if type: 

1944 types[tag] = type 

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

1946 if isinstance(value, str): 

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

1948 elif isinstance(value, IFDRational): 

1949 atts[tag] = float(value) 

1950 else: 

1951 atts[tag] = value 

1952 

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

1954 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1955 

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

1957 

1958 # libtiff always expects the bytes in native order. 

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

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

1961 # byte order. 

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

1963 rawmode = "I;16N" 

1964 

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

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

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

1968 tags = list(atts.items()) 

1969 tags.sort() 

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

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

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

1973 while True: 

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

1975 if not _fp: 

1976 fp.write(data) 

1977 if errcode: 

1978 break 

1979 if errcode < 0: 

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

1981 raise OSError(msg) 

1982 

1983 else: 

1984 for tag in blocklist: 

1985 del ifd[tag] 

1986 offset = ifd.save(fp) 

1987 

1988 ImageFile._save( 

1989 im, 

1990 fp, 

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

1992 ) 

1993 

1994 # -- helper for multi-page save -- 

1995 if "_debug_multipage" in encoderinfo: 

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

1997 setattr(im, "_debug_multipage", ifd) 

1998 

1999 

2000class AppendingTiffWriter(io.BytesIO): 

2001 fieldSizes = [ 

2002 0, # None 

2003 1, # byte 

2004 1, # ascii 

2005 2, # short 

2006 4, # long 

2007 8, # rational 

2008 1, # sbyte 

2009 1, # undefined 

2010 2, # sshort 

2011 4, # slong 

2012 8, # srational 

2013 4, # float 

2014 8, # double 

2015 4, # ifd 

2016 2, # unicode 

2017 4, # complex 

2018 8, # long8 

2019 ] 

2020 

2021 Tags = { 

2022 273, # StripOffsets 

2023 288, # FreeOffsets 

2024 324, # TileOffsets 

2025 519, # JPEGQTables 

2026 520, # JPEGDCTables 

2027 521, # JPEGACTables 

2028 } 

2029 

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

2031 self.f: IO[bytes] 

2032 if is_path(fn): 

2033 self.name = fn 

2034 self.close_fp = True 

2035 try: 

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

2037 except OSError: 

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

2039 else: 

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

2041 self.close_fp = False 

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

2043 self.setup() 

2044 

2045 def setup(self) -> None: 

2046 # Reset everything. 

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

2048 

2049 self.whereToWriteNewIFDOffset: int | None = None 

2050 self.offsetOfNewPage = 0 

2051 

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

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

2054 if not iimm: 

2055 # empty file - first page 

2056 self.isFirst = True 

2057 return 

2058 

2059 self.isFirst = False 

2060 if iimm not in PREFIXES: 

2061 msg = "Invalid TIFF file header" 

2062 raise RuntimeError(msg) 

2063 

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

2065 

2066 if self._bigtiff: 

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

2068 self.skipIFDs() 

2069 self.goToEnd() 

2070 

2071 def finalize(self) -> None: 

2072 if self.isFirst: 

2073 return 

2074 

2075 # fix offsets 

2076 self.f.seek(self.offsetOfNewPage) 

2077 

2078 iimm = self.f.read(4) 

2079 if not iimm: 

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

2081 return 

2082 

2083 if iimm != self.IIMM: 

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

2085 raise RuntimeError(msg) 

2086 

2087 if self._bigtiff: 

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

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

2090 ifd_offset += self.offsetOfNewPage 

2091 assert self.whereToWriteNewIFDOffset is not None 

2092 self.f.seek(self.whereToWriteNewIFDOffset) 

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

2094 self.f.seek(ifd_offset) 

2095 self.fixIFD() 

2096 

2097 def newFrame(self) -> None: 

2098 # Call this to finish a frame. 

2099 self.finalize() 

2100 self.setup() 

2101 

2102 def __enter__(self) -> AppendingTiffWriter: 

2103 return self 

2104 

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

2106 if self.close_fp: 

2107 self.close() 

2108 

2109 def tell(self) -> int: 

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

2111 

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

2113 """ 

2114 :param offset: Distance to seek. 

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

2116 end or current position. 

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

2118 """ 

2119 if whence == os.SEEK_SET: 

2120 offset += self.offsetOfNewPage 

2121 

2122 self.f.seek(offset, whence) 

2123 return self.tell() 

2124 

2125 def goToEnd(self) -> None: 

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

2127 pos = self.f.tell() 

2128 

2129 # pad to 16 byte boundary 

2130 pad_bytes = 16 - pos % 16 

2131 if 0 < pad_bytes < 16: 

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

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

2134 

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

2136 self.endian = endian 

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

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

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

2140 

2141 def skipIFDs(self) -> None: 

2142 while True: 

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

2144 if ifd_offset == 0: 

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

2146 8 if self._bigtiff else 4 

2147 ) 

2148 break 

2149 

2150 self.f.seek(ifd_offset) 

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

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

2153 

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

2155 return self.f.write(data) 

2156 

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

2158 try: 

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

2160 except KeyError: 

2161 msg = "offset is not supported" 

2162 raise RuntimeError(msg) 

2163 

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

2165 (value,) = struct.unpack( 

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

2167 ) 

2168 return value 

2169 

2170 def readShort(self) -> int: 

2171 return self._read(2) 

2172 

2173 def readLong(self) -> int: 

2174 return self._read(4) 

2175 

2176 @staticmethod 

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

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

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

2180 raise RuntimeError(msg) 

2181 

2182 def _rewriteLast( 

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

2184 ) -> None: 

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

2186 if not new_field_size: 

2187 new_field_size = field_size 

2188 bytes_written = self.f.write( 

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

2190 ) 

2191 self._verify_bytes_written(bytes_written, new_field_size) 

2192 

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

2194 self._rewriteLast(value, 2, 4) 

2195 

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

2197 return self._rewriteLast(value, 2) 

2198 

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

2200 return self._rewriteLast(value, 4) 

2201 

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

2203 bytes_written = self.f.write( 

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

2205 ) 

2206 self._verify_bytes_written(bytes_written, field_size) 

2207 

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

2209 self._write(value, 2) 

2210 

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

2212 self._write(value, 4) 

2213 

2214 def close(self) -> None: 

2215 self.finalize() 

2216 if self.close_fp: 

2217 self.f.close() 

2218 

2219 def fixIFD(self) -> None: 

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

2221 

2222 for i in range(num_tags): 

2223 tag, field_type, count = struct.unpack( 

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

2225 ) 

2226 

2227 field_size = self.fieldSizes[field_type] 

2228 total_size = field_size * count 

2229 fmt_size = 8 if self._bigtiff else 4 

2230 is_local = total_size <= fmt_size 

2231 if not is_local: 

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

2233 self._rewriteLast(offset, fmt_size) 

2234 

2235 if tag in self.Tags: 

2236 cur_pos = self.f.tell() 

2237 

2238 logger.debug( 

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

2240 TiffTags.lookup(tag).name, 

2241 tag, 

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

2243 field_type, 

2244 field_size, 

2245 count, 

2246 ) 

2247 

2248 if is_local: 

2249 self._fixOffsets(count, field_size) 

2250 self.f.seek(cur_pos + fmt_size) 

2251 else: 

2252 self.f.seek(offset) 

2253 self._fixOffsets(count, field_size) 

2254 self.f.seek(cur_pos) 

2255 

2256 elif is_local: 

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

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

2259 

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

2261 for i in range(count): 

2262 offset = self._read(field_size) 

2263 offset += self.offsetOfNewPage 

2264 

2265 new_field_size = 0 

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

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

2268 new_field_size = 8 

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

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

2271 new_field_size = 4 

2272 if new_field_size: 

2273 if count != 1: 

2274 msg = "not implemented" 

2275 raise RuntimeError(msg) # XXX TODO 

2276 

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

2278 # local (not referenced with another offset) 

2279 self._rewriteLast(offset, field_size, new_field_size) 

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

2281 rewind = -new_field_size - 4 - 2 

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

2283 self.writeShort(new_field_size) # rewrite the type 

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

2285 else: 

2286 self._rewriteLast(offset, field_size) 

2287 

2288 def fixOffsets( 

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

2290 ) -> None: 

2291 if isShort: 

2292 field_size = 2 

2293 elif isLong: 

2294 field_size = 4 

2295 else: 

2296 field_size = 0 

2297 return self._fixOffsets(count, field_size) 

2298 

2299 

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

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

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

2303 return _save(im, fp, filename) 

2304 

2305 cur_idx = im.tell() 

2306 try: 

2307 with AppendingTiffWriter(fp) as tf: 

2308 for ims in [im] + append_images: 

2309 encoderinfo = ims._attach_default_encoderinfo(im) 

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

2311 ims.encoderconfig = () 

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

2313 

2314 for idx in range(nfr): 

2315 ims.seek(idx) 

2316 ims.load() 

2317 _save(ims, tf, filename) 

2318 tf.newFrame() 

2319 ims.encoderinfo = encoderinfo 

2320 finally: 

2321 im.seek(cur_idx) 

2322 

2323 

2324# 

2325# -------------------------------------------------------------------- 

2326# Register 

2327 

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

2329Image.register_save(TiffImageFile.format, _save) 

2330Image.register_save_all(TiffImageFile.format, _save_all) 

2331 

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

2333 

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