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

1243 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 (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

276} 

277 

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

279 

280PREFIXES = [ 

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

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

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

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

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

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

287] 

288 

289 

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

291 return prefix.startswith(tuple(PREFIXES)) 

292 

293 

294def _limit_rational( 

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

296) -> tuple[IntegralLike, IntegralLike]: 

297 inv = abs(val) > 1 

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

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

300 

301 

302def _limit_signed_rational( 

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

304) -> tuple[IntegralLike, IntegralLike]: 

305 frac = Fraction(val) 

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

307 

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

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

310 

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

312 if max(n_d_float) > max_val: 

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

314 

315 return n_d 

316 

317 

318## 

319# Wrapper for TIFF IFDs. 

320 

321_load_dispatch = {} 

322_write_dispatch = {} 

323 

324 

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

326 def delegate( 

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

328 ) -> bool | float | Fraction: 

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

330 

331 return delegate 

332 

333 

334class IFDRational(Rational): 

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

336 the in the wild use of exif rationals. 

337 

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

339 """ 

340 

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

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

343 

344 """ 

345 

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

347 

348 def __init__( 

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

350 ) -> None: 

351 """ 

352 :param value: either an integer numerator, a 

353 float/rational/other number, or an IFDRational 

354 :param denominator: Optional integer denominator 

355 """ 

356 self._val: Fraction | float 

357 if isinstance(value, IFDRational): 

358 self._numerator = value.numerator 

359 self._denominator = value.denominator 

360 self._val = value._val 

361 return 

362 

363 if isinstance(value, Fraction): 

364 self._numerator = value.numerator 

365 self._denominator = value.denominator 

366 else: 

367 if TYPE_CHECKING: 

368 self._numerator = cast(IntegralLike, value) 

369 else: 

370 self._numerator = value 

371 self._denominator = denominator 

372 

373 if denominator == 0: 

374 self._val = float("nan") 

375 elif denominator == 1: 

376 self._val = Fraction(value) 

377 elif int(value) == value: 

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

379 else: 

380 self._val = Fraction(value / denominator) 

381 

382 @property 

383 def numerator(self) -> IntegralLike: 

384 return self._numerator 

385 

386 @property 

387 def denominator(self) -> int: 

388 return self._denominator 

389 

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

391 """ 

392 

393 :param max_denominator: Integer, the maximum denominator value 

394 :returns: Tuple of (numerator, denominator) 

395 """ 

396 

397 if self.denominator == 0: 

398 return self.numerator, self.denominator 

399 

400 assert isinstance(self._val, Fraction) 

401 f = self._val.limit_denominator(max_denominator) 

402 return f.numerator, f.denominator 

403 

404 def __repr__(self) -> str: 

405 return str(float(self._val)) 

406 

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

408 return self._val.__hash__() 

409 

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

411 val = self._val 

412 if isinstance(other, IFDRational): 

413 other = other._val 

414 if isinstance(other, float): 

415 val = float(val) 

416 return val == other 

417 

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

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

420 

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

422 IFDRational.__init__(self, 0) 

423 _val, _numerator, _denominator = state 

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

425 self._val = _val 

426 if TYPE_CHECKING: 

427 self._numerator = cast(IntegralLike, _numerator) 

428 else: 

429 self._numerator = _numerator 

430 assert isinstance(_denominator, int) 

431 self._denominator = _denominator 

432 

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

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

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

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

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

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

439 """ 

440 

441 __add__ = _delegate("__add__") 

442 __radd__ = _delegate("__radd__") 

443 __sub__ = _delegate("__sub__") 

444 __rsub__ = _delegate("__rsub__") 

445 __mul__ = _delegate("__mul__") 

446 __rmul__ = _delegate("__rmul__") 

447 __truediv__ = _delegate("__truediv__") 

448 __rtruediv__ = _delegate("__rtruediv__") 

449 __floordiv__ = _delegate("__floordiv__") 

450 __rfloordiv__ = _delegate("__rfloordiv__") 

451 __mod__ = _delegate("__mod__") 

452 __rmod__ = _delegate("__rmod__") 

453 __pow__ = _delegate("__pow__") 

454 __rpow__ = _delegate("__rpow__") 

455 __pos__ = _delegate("__pos__") 

456 __neg__ = _delegate("__neg__") 

457 __abs__ = _delegate("__abs__") 

458 __trunc__ = _delegate("__trunc__") 

459 __lt__ = _delegate("__lt__") 

460 __gt__ = _delegate("__gt__") 

461 __le__ = _delegate("__le__") 

462 __ge__ = _delegate("__ge__") 

463 __bool__ = _delegate("__bool__") 

464 __ceil__ = _delegate("__ceil__") 

465 __floor__ = _delegate("__floor__") 

466 __round__ = _delegate("__round__") 

467 # Python >= 3.11 

468 if hasattr(Fraction, "__int__"): 

469 __int__ = _delegate("__int__") 

470 

471 

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

473 

474 

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

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

477 from .TiffTags import TYPES 

478 

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

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

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

482 return func 

483 

484 return decorator 

485 

486 

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

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

489 _write_dispatch[idx] = func # noqa: F821 

490 return func 

491 

492 return decorator 

493 

494 

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

496 from .TiffTags import TYPES 

497 

498 idx, fmt, name = idx_fmt_name 

499 TYPES[idx] = name 

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

501 

502 def basic_handler( 

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

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

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

506 

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

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

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

510 ) 

511 

512 

513if TYPE_CHECKING: 

514 _IFDv2Base = MutableMapping[int, Any] 

515else: 

516 _IFDv2Base = MutableMapping 

517 

518 

519class ImageFileDirectory_v2(_IFDv2Base): 

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

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

522 

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

524 

525 ifd = ImageFileDirectory_v2() 

526 ifd[key] = 'Some Data' 

527 ifd.tagtype[key] = TiffTags.ASCII 

528 print(ifd[key]) 

529 'Some Data' 

530 

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

532 returned as tuples of the values. 

533 

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

535 tag types in 

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

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

538 manually. 

539 

540 Data Structures: 

541 

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

543 

544 * Key: numerical TIFF tag number 

545 * Value: integer corresponding to the data type from 

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

547 

548 .. versionadded:: 3.0.0 

549 

550 'Internal' data structures: 

551 

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

553 

554 * Key: numerical TIFF tag number 

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

556 

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

558 

559 * Key: numerical TIFF tag number 

560 * Value: undecoded byte string from file 

561 

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

563 

564 * Key: numerical TIFF tag number 

565 * Value: decoded data in the v1 format 

566 

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

568 ``self._tags_v2`` once decoded. 

569 

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

571 from outside code. In cooperation with 

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

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

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

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

576 ``legacy_api == true``. 

577 

578 """ 

579 

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

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

582 

583 def __init__( 

584 self, 

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

586 prefix: bytes | None = None, 

587 group: int | None = None, 

588 ) -> None: 

589 """Initialize an ImageFileDirectory. 

590 

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

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

593 as the 'prefix' keyword argument. 

594 

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

596 endianness. 

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

598 """ 

599 if not _accept(ifh): 

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

601 raise SyntaxError(msg) 

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

603 if self._prefix == MM: 

604 self._endian = ">" 

605 elif self._prefix == II: 

606 self._endian = "<" 

607 else: 

608 msg = "not a TIFF IFD" 

609 raise SyntaxError(msg) 

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

611 self.group = group 

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

613 """ Dictionary of tag types """ 

614 self.reset() 

615 self.next = ( 

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

617 if self._bigtiff 

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

619 ) 

620 self._legacy_api = False 

621 

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

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

624 

625 @property 

626 def legacy_api(self) -> bool: 

627 return self._legacy_api 

628 

629 @legacy_api.setter 

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

631 msg = "Not allowing setting of legacy api" 

632 raise Exception(msg) 

633 

634 def reset(self) -> None: 

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

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

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

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

639 self._next = None 

640 self._offset: int | None = None 

641 

642 def __str__(self) -> str: 

643 return str(dict(self)) 

644 

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

646 """ 

647 :returns: dict of name|key: value 

648 

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

650 """ 

651 return { 

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

653 for code, value in self.items() 

654 } 

655 

656 def __len__(self) -> int: 

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

658 

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

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

661 data = self._tagdata[tag] 

662 typ = self.tagtype[tag] 

663 size, handler = self._load_dispatch[typ] 

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

665 val = self._tags_v2[tag] 

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

667 val = (val,) 

668 return val 

669 

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

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

672 

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

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

675 

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

677 basetypes = (Number, bytes, str) 

678 

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

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

681 

682 if tag not in self.tagtype: 

683 if info.type: 

684 self.tagtype[tag] = info.type 

685 else: 

686 self.tagtype[tag] = TiffTags.UNDEFINED 

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

688 for v in values: 

689 assert isinstance(v, IFDRational) 

690 if v < 0: 

691 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL 

692 break 

693 else: 

694 self.tagtype[tag] = TiffTags.RATIONAL 

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

696 short = True 

697 signed_short = True 

698 long = True 

699 for v in values: 

700 assert isinstance(v, int) 

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

702 short = False 

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

704 signed_short = False 

705 if long and v < 0: 

706 long = False 

707 if short: 

708 self.tagtype[tag] = TiffTags.SHORT 

709 elif signed_short: 

710 self.tagtype[tag] = TiffTags.SIGNED_SHORT 

711 elif long: 

712 self.tagtype[tag] = TiffTags.LONG 

713 else: 

714 self.tagtype[tag] = TiffTags.SIGNED_LONG 

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

716 self.tagtype[tag] = TiffTags.DOUBLE 

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

718 self.tagtype[tag] = TiffTags.ASCII 

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

720 self.tagtype[tag] = TiffTags.BYTE 

721 

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

723 values = [ 

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

725 for v in values 

726 ] 

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

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

729 

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

731 if not is_ifd: 

732 values = tuple( 

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

734 for value in values 

735 ) 

736 

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

738 

739 # Three branches: 

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

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

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

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

744 if not is_ifd and ( 

745 (info.length == 1) 

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

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

748 ): 

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

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

751 TiffTags.RATIONAL, 

752 TiffTags.SIGNED_RATIONAL, 

753 ]: # rationals 

754 values = (values,) 

755 try: 

756 (dest[tag],) = values 

757 except ValueError: 

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

759 warnings.warn( 

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

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

762 ) 

763 dest[tag] = values[0] 

764 

765 else: 

766 # Spec'd length > 1 or undefined 

767 # Unspec'd, and length > 1 

768 dest[tag] = values 

769 

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

771 self._tags_v2.pop(tag, None) 

772 self._tags_v1.pop(tag, None) 

773 self._tagdata.pop(tag, None) 

774 

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

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

777 

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

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

780 

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

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

783 

784 list( 

785 map( 

786 _register_basic, 

787 [ 

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

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

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

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

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

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

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

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

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

797 ], 

798 ) 

799 ) 

800 

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

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

803 return data 

804 

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

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

807 if isinstance(data, IFDRational): 

808 data = int(data) 

809 if isinstance(data, int): 

810 data = bytes((data,)) 

811 return data 

812 

813 @_register_loader(2, 1) 

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

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

816 data = data[:-1] 

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

818 

819 @_register_writer(2) 

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

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

822 if isinstance(value, int): 

823 value = str(value) 

824 if not isinstance(value, bytes): 

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

826 return value + b"\0" 

827 

828 @_register_loader(5, 8) 

829 def load_rational( 

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

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

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

833 

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

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

836 

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

838 

839 @_register_writer(5) 

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

841 return b"".join( 

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

843 ) 

844 

845 @_register_loader(7, 1) 

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

847 return data 

848 

849 @_register_writer(7) 

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

851 if isinstance(value, IFDRational): 

852 value = int(value) 

853 if isinstance(value, int): 

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

855 return value 

856 

857 @_register_loader(10, 8) 

858 def load_signed_rational( 

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

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

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

862 

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

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

865 

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

867 

868 @_register_writer(10) 

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

870 return b"".join( 

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

872 for frac in values 

873 ) 

874 

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

876 ret = fp.read(size) 

877 if len(ret) != size: 

878 msg = ( 

879 "Corrupt EXIF data. " 

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

881 ) 

882 raise OSError(msg) 

883 return ret 

884 

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

886 self.reset() 

887 self._offset = fp.tell() 

888 

889 try: 

890 tag_count = ( 

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

892 if self._bigtiff 

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

894 )[0] 

895 for i in range(tag_count): 

896 tag, typ, count, data = ( 

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

898 if self._bigtiff 

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

900 ) 

901 

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

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

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

905 

906 try: 

907 unit_size, handler = self._load_dispatch[typ] 

908 except KeyError: 

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

910 continue # ignore unsupported type 

911 size = count * unit_size 

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

913 here = fp.tell() 

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

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

916 fp.seek(offset) 

917 data = ImageFile._safe_read(fp, size) 

918 fp.seek(here) 

919 else: 

920 data = data[:size] 

921 

922 if len(data) != size: 

923 warnings.warn( 

924 "Possibly corrupt EXIF data. " 

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

926 f" Skipping tag {tag}" 

927 ) 

928 logger.debug(msg) 

929 continue 

930 

931 if not data: 

932 logger.debug(msg) 

933 continue 

934 

935 self._tagdata[tag] = data 

936 self.tagtype[tag] = typ 

937 

938 msg += " - value: " 

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

940 

941 logger.debug(msg) 

942 

943 (self.next,) = ( 

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

945 if self._bigtiff 

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

947 ) 

948 except OSError as msg: 

949 warnings.warn(str(msg)) 

950 return 

951 

952 def _get_ifh(self) -> bytes: 

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

954 if self._bigtiff: 

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

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

957 

958 return ifh 

959 

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

961 # FIXME What about tagdata? 

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

963 

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

965 

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

967 fmt_size = 8 if self._bigtiff else 4 

968 offset += ( 

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

970 ) 

971 stripoffsets = None 

972 

973 # pass 1: convert tags to binary format 

974 # always write tags in ascending order 

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

976 if tag == STRIPOFFSETS: 

977 stripoffsets = len(entries) 

978 typ = self.tagtype[tag] 

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

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

981 if is_ifd: 

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

983 values = self._tags_v2[tag] 

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

985 ifd[ifd_tag] = ifd_value 

986 data = ifd.tobytes(offset) 

987 else: 

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

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

990 

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

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

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

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

995 logger.debug(msg) 

996 

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

998 if is_ifd: 

999 count = 1 

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

1001 count = len(data) 

1002 else: 

1003 count = len(values) 

1004 # figure out if data fits into the entry 

1005 if len(data) <= fmt_size: 

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

1007 else: 

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

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

1010 

1011 # update strip offset data to point beyond auxiliary data 

1012 if stripoffsets is not None: 

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

1014 if data: 

1015 size, handler = self._load_dispatch[typ] 

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

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

1018 else: 

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

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

1021 

1022 # pass 2: write entries to file 

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

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

1025 result += self._pack( 

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

1027 ) 

1028 

1029 # -- overwrite here for multi-page -- 

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

1031 

1032 # pass 3: write auxiliary data to file 

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

1034 result += data 

1035 if len(data) & 1: 

1036 result += b"\0" 

1037 

1038 return result 

1039 

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

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

1042 fp.write(self._get_ifh()) 

1043 

1044 offset = fp.tell() 

1045 result = self.tobytes(offset) 

1046 fp.write(result) 

1047 return offset + len(result) 

1048 

1049 

1050ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1051ImageFileDirectory_v2._write_dispatch = _write_dispatch 

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

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

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

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

1056del _load_dispatch, _write_dispatch, idx, name 

1057 

1058 

1059# Legacy ImageFileDirectory support. 

1060class ImageFileDirectory_v1(ImageFileDirectory_v2): 

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

1062 

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

1064 

1065 ifd = ImageFileDirectory_v1() 

1066 ifd[key] = 'Some Data' 

1067 ifd.tagtype[key] = TiffTags.ASCII 

1068 print(ifd[key]) 

1069 ('Some Data',) 

1070 

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

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

1073 

1074 Values are returned as a tuple. 

1075 

1076 .. deprecated:: 3.0.0 

1077 """ 

1078 

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

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

1081 self._legacy_api = True 

1082 

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

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

1085 

1086 # defined in ImageFileDirectory_v2 

1087 tagtype: dict[int, int] 

1088 """Dictionary of tag types""" 

1089 

1090 @classmethod 

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

1092 """Returns an 

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

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

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

1096 instance. 

1097 

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

1099 

1100 """ 

1101 

1102 ifd = cls(prefix=original.prefix) 

1103 ifd._tagdata = original._tagdata 

1104 ifd.tagtype = original.tagtype 

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

1106 return ifd 

1107 

1108 def to_v2(self) -> ImageFileDirectory_v2: 

1109 """Returns an 

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

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

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

1113 instance. 

1114 

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

1116 

1117 """ 

1118 

1119 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1120 ifd._tagdata = dict(self._tagdata) 

1121 ifd.tagtype = dict(self.tagtype) 

1122 ifd._tags_v2 = dict(self._tags_v2) 

1123 return ifd 

1124 

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

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

1127 

1128 def __len__(self) -> int: 

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

1130 

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

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

1133 

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

1135 for legacy_api in (False, True): 

1136 self._setitem(tag, value, legacy_api) 

1137 

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

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

1140 data = self._tagdata[tag] 

1141 typ = self.tagtype[tag] 

1142 size, handler = self._load_dispatch[typ] 

1143 for legacy in (False, True): 

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

1145 val = self._tags_v1[tag] 

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

1147 val = (val,) 

1148 return val 

1149 

1150 

1151# undone -- switch this pointer 

1152ImageFileDirectory = ImageFileDirectory_v1 

1153 

1154 

1155## 

1156# Image plugin for TIFF files. 

1157 

1158 

1159class TiffImageFile(ImageFile.ImageFile): 

1160 format = "TIFF" 

1161 format_description = "Adobe TIFF" 

1162 _close_exclusive_fp_after_loading = False 

1163 

1164 def __init__( 

1165 self, 

1166 fp: StrOrBytesPath | IO[bytes], 

1167 filename: str | bytes | None = None, 

1168 ) -> None: 

1169 self.tag_v2: ImageFileDirectory_v2 

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

1171 

1172 self.tag: ImageFileDirectory_v1 

1173 """ Legacy tag entries """ 

1174 

1175 super().__init__(fp, filename) 

1176 

1177 def _open(self) -> None: 

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

1179 

1180 # Header 

1181 assert self.fp is not None 

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") and len(val) >= 12: 

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

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

1293 try: 

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

1295 except struct.error: 

1296 break 

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

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

1299 

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

1301 return blocks 

1302 

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

1304 if self.tile and self.use_load_libtiff: 

1305 return self._load_libtiff() 

1306 return super().load() 

1307 

1308 def load_prepare(self) -> None: 

1309 if self._im is None: 

1310 Image._decompression_bomb_check(self._tile_size) 

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

1312 ImageFile.ImageFile.load_prepare(self) 

1313 

1314 def load_end(self) -> None: 

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

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

1317 if not self.is_animated: 

1318 self._close_exclusive_fp_after_loading = True 

1319 

1320 # load IFD data from fp before it is closed 

1321 exif = self.getexif() 

1322 for key in TiffTags.TAGS_V2_GROUPS: 

1323 if key not in exif: 

1324 continue 

1325 exif.get_ifd(key) 

1326 

1327 ImageOps.exif_transpose(self, in_place=True) 

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

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

1330 

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

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

1333 Calls out to libtiff""" 

1334 

1335 Image.Image.load(self) 

1336 

1337 self.load_prepare() 

1338 

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

1340 msg = "Not exactly one tile" 

1341 raise OSError(msg) 

1342 

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

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

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

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

1347 

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

1349 # file descriptor, use that instead of reading 

1350 # into a string in python. 

1351 assert self.fp is not None 

1352 try: 

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

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

1355 # should also eliminate the need for fp.tell 

1356 # in _seek 

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

1358 self.fp.flush() 

1359 except OSError: 

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

1361 # it doesn't use a file descriptor. 

1362 fp = False 

1363 

1364 if fp: 

1365 assert isinstance(args, tuple) 

1366 args_list = list(args) 

1367 args_list[2] = fp 

1368 args = tuple(args_list) 

1369 

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

1371 try: 

1372 decoder.setimage(self.im, extents) 

1373 except ValueError as e: 

1374 msg = "Couldn't set the image" 

1375 raise OSError(msg) from e 

1376 

1377 close_self_fp = self._exclusive_fp and not self.is_animated 

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

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

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

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

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

1383 # underlying string for stringio. 

1384 # 

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

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

1387 # deal with here by reordering. 

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

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

1390 elif fp: 

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

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

1393 if not close_self_fp: 

1394 self.fp.seek(0) 

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

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

1397 # io.BufferedReader and possible others. 

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

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

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

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

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

1403 # 4 bytes, otherwise the trace might error out 

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

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

1406 else: 

1407 # we have something else. 

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

1409 self.fp.seek(0) 

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

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

1412 

1413 self.tile = [] 

1414 self.readonly = 0 

1415 

1416 self.load_end() 

1417 

1418 if close_self_fp: 

1419 self.fp.close() 

1420 self.fp = None # might be shared 

1421 

1422 if err < 0: 

1423 msg = f"decoder error {err}" 

1424 raise OSError(msg) 

1425 

1426 return Image.Image.load(self) 

1427 

1428 def _setup(self) -> None: 

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

1430 

1431 if 0xBC01 in self.tag_v2: 

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

1433 raise OSError(msg) 

1434 

1435 # extract relevant tags 

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

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

1438 

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

1440 # the specification 

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

1442 

1443 # old style jpeg compression images most certainly are YCbCr 

1444 if self._compression == "tiff_jpeg": 

1445 photo = 6 

1446 

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

1448 

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

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

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

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

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

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

1455 

1456 # size 

1457 try: 

1458 xsize = self.tag_v2[IMAGEWIDTH] 

1459 ysize = self.tag_v2[IMAGELENGTH] 

1460 except KeyError as e: 

1461 msg = "Missing dimensions" 

1462 raise TypeError(msg) from e 

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

1464 msg = "Invalid dimensions" 

1465 raise ValueError(msg) 

1466 self._tile_size = xsize, ysize 

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

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

1469 self._size = ysize, xsize 

1470 else: 

1471 self._size = xsize, ysize 

1472 

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

1474 

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

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

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

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

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

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

1481 # for more exotic images. 

1482 sample_format = (sample_format[0],) 

1483 

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

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

1486 samples_per_pixel = self.tag_v2.get( 

1487 SAMPLESPERPIXEL, 

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

1489 ) 

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

1491 bps_count = 3 

1492 elif photo == 5: # CMYK 

1493 bps_count = 4 

1494 else: 

1495 bps_count = 1 

1496 if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0: 

1497 # If components are stored separately, 

1498 # then unspecified extra components at the end can be ignored 

1499 bps_tuple = bps_tuple[: -len(extra_tuple)] 

1500 samples_per_pixel -= len(extra_tuple) 

1501 extra_tuple = () 

1502 bps_count += len(extra_tuple) 

1503 bps_actual_count = len(bps_tuple) 

1504 

1505 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

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

1507 logger.error( 

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

1509 ) 

1510 msg = "Invalid value for samples per pixel" 

1511 raise SyntaxError(msg) 

1512 

1513 if samples_per_pixel < bps_actual_count: 

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

1515 # remove the excess. 

1516 bps_tuple = bps_tuple[:samples_per_pixel] 

1517 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

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

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

1520 bps_tuple = bps_tuple * samples_per_pixel 

1521 

1522 if len(bps_tuple) != samples_per_pixel: 

1523 msg = "unknown data organization" 

1524 raise SyntaxError(msg) 

1525 

1526 # mode: check photometric interpretation and bits per pixel 

1527 key = ( 

1528 self.tag_v2.prefix, 

1529 photo, 

1530 sample_format, 

1531 fillorder, 

1532 bps_tuple, 

1533 extra_tuple, 

1534 ) 

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

1536 try: 

1537 self._mode, rawmode = OPEN_INFO[key] 

1538 except KeyError as e: 

1539 logger.debug("- unsupported format") 

1540 msg = "unknown pixel mode" 

1541 raise SyntaxError(msg) from e 

1542 

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

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

1545 

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

1547 

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

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

1550 

1551 if xres and yres: 

1552 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1553 if resunit == 2: # dots per inch 

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

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

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

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

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

1559 # For backward compatibility, 

1560 # we also preserve the old behavior 

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

1562 else: # No absolute unit of measurement 

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

1564 

1565 # build tile descriptors 

1566 x = y = layer = 0 

1567 self.tile = [] 

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

1569 if self.use_load_libtiff: 

1570 # Decoder expects entire file as one tile. 

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

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

1573 # function. 

1574 # 

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

1576 # use the _load_libtiff function. 

1577 

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

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

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

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

1582 if fillorder == 2: 

1583 # Replace fillorder with fillorder=1 

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

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

1586 # this should always work, since all the 

1587 # fillorder==2 modes have a corresponding 

1588 # fillorder=1 mode 

1589 self._mode, rawmode = OPEN_INFO[key] 

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

1591 # unpacked straight into RGB values 

1592 if ( 

1593 photo == 6 

1594 and self._compression == "jpeg" 

1595 and self._planar_configuration == 1 

1596 ): 

1597 rawmode = "RGB" 

1598 # libtiff always returns the bytes in native order. 

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

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

1601 # byte order. 

1602 elif rawmode == "I;16": 

1603 rawmode = "I;16N" 

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

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

1606 

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

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

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

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

1611 

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

1613 # striped image 

1614 if STRIPOFFSETS in self.tag_v2: 

1615 offsets = self.tag_v2[STRIPOFFSETS] 

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

1617 w = xsize 

1618 else: 

1619 # tiled image 

1620 offsets = self.tag_v2[TILEOFFSETS] 

1621 tilewidth = self.tag_v2.get(TILEWIDTH) 

1622 h = self.tag_v2.get(TILELENGTH) 

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

1624 msg = "Invalid tile dimensions" 

1625 raise ValueError(msg) 

1626 w = tilewidth 

1627 

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

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

1630 offsets = offsets[-1:] 

1631 

1632 for offset in offsets: 

1633 if x + w > xsize: 

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

1635 else: 

1636 stride = 0 

1637 

1638 tile_rawmode = rawmode 

1639 if self._planar_configuration == 2: 

1640 # each band on it's own layer 

1641 tile_rawmode = rawmode[layer] 

1642 # adjust stride width accordingly 

1643 stride /= bps_count 

1644 

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

1646 self.tile.append( 

1647 ImageFile._Tile( 

1648 self._compression, 

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

1650 offset, 

1651 args, 

1652 ) 

1653 ) 

1654 x += w 

1655 if x >= xsize: 

1656 x, y = 0, y + h 

1657 if y >= ysize: 

1658 y = 0 

1659 layer += 1 

1660 else: 

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

1662 msg = "unknown data organization" 

1663 raise SyntaxError(msg) 

1664 

1665 # Fix up info. 

1666 if ICCPROFILE in self.tag_v2: 

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

1668 

1669 # fixup palette descriptor 

1670 

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

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

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

1674 

1675 

1676# 

1677# -------------------------------------------------------------------- 

1678# Write TIFF files 

1679 

1680# little endian is default except for image modes with 

1681# explicit big endian byte-order 

1682 

1683SAVE_INFO = { 

1684 # mode => rawmode, byteorder, photometrics, 

1685 # sampleformat, bitspersample, extra 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1702} 

1703 

1704 

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

1706 try: 

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

1708 except KeyError as e: 

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

1710 raise OSError(msg) from e 

1711 

1712 encoderinfo = im.encoderinfo 

1713 encoderconfig = im.encoderconfig 

1714 

1715 ifd = ImageFileDirectory_v2(prefix=prefix) 

1716 if encoderinfo.get("big_tiff"): 

1717 ifd._bigtiff = True 

1718 

1719 try: 

1720 compression = encoderinfo["compression"] 

1721 except KeyError: 

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

1723 if isinstance(compression, int): 

1724 # compression value may be from BMP. Ignore it 

1725 compression = None 

1726 if compression is None: 

1727 compression = "raw" 

1728 elif compression == "tiff_jpeg": 

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

1730 compression = "jpeg" 

1731 elif compression == "tiff_deflate": 

1732 compression = "tiff_adobe_deflate" 

1733 

1734 libtiff = WRITE_LIBTIFF or compression != "raw" 

1735 

1736 # required for color libtiff images 

1737 ifd[PLANAR_CONFIGURATION] = 1 

1738 

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

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

1741 

1742 # write any arbitrary tags passed in as an ImageFileDirectory 

1743 if "tiffinfo" in encoderinfo: 

1744 info = encoderinfo["tiffinfo"] 

1745 elif "exif" in encoderinfo: 

1746 info = encoderinfo["exif"] 

1747 if isinstance(info, bytes): 

1748 exif = Image.Exif() 

1749 exif.load(info) 

1750 info = exif 

1751 else: 

1752 info = {} 

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

1754 if isinstance(info, ImageFileDirectory_v1): 

1755 info = info.to_v2() 

1756 for key in info: 

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

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

1759 else: 

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

1761 try: 

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

1763 except Exception: 

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

1765 

1766 legacy_ifd = {} 

1767 if hasattr(im, "tag"): 

1768 legacy_ifd = im.tag.to_v2() 

1769 

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

1771 if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags: 

1772 # If the image used separate component planes, 

1773 # then EXTRASAMPLES should be ignored when saving contiguously 

1774 if SAMPLESPERPIXEL in supplied_tags: 

1775 supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES]) 

1776 del supplied_tags[EXTRASAMPLES] 

1777 for tag in ( 

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

1779 EXIFIFD, 

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

1781 SAMPLEFORMAT, 

1782 ): 

1783 if tag in supplied_tags: 

1784 del supplied_tags[tag] 

1785 

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

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

1788 if hasattr(im, "tag_v2"): 

1789 # preserve tags from original TIFF image file 

1790 for key in ( 

1791 RESOLUTION_UNIT, 

1792 X_RESOLUTION, 

1793 Y_RESOLUTION, 

1794 IPTC_NAA_CHUNK, 

1795 PHOTOSHOP_CHUNK, 

1796 XMP, 

1797 ): 

1798 if key in im.tag_v2: 

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

1800 TiffTags.BYTE, 

1801 TiffTags.UNDEFINED, 

1802 ): 

1803 del supplied_tags[key] 

1804 else: 

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

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

1807 

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

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

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

1811 if icc: 

1812 ifd[ICCPROFILE] = icc 

1813 

1814 for key, name in [ 

1815 (IMAGEDESCRIPTION, "description"), 

1816 (X_RESOLUTION, "resolution"), 

1817 (Y_RESOLUTION, "resolution"), 

1818 (X_RESOLUTION, "x_resolution"), 

1819 (Y_RESOLUTION, "y_resolution"), 

1820 (RESOLUTION_UNIT, "resolution_unit"), 

1821 (SOFTWARE, "software"), 

1822 (DATE_TIME, "date_time"), 

1823 (ARTIST, "artist"), 

1824 (COPYRIGHT, "copyright"), 

1825 ]: 

1826 if name in encoderinfo: 

1827 ifd[key] = encoderinfo[name] 

1828 

1829 dpi = encoderinfo.get("dpi") 

1830 if dpi: 

1831 ifd[RESOLUTION_UNIT] = 2 

1832 ifd[X_RESOLUTION] = dpi[0] 

1833 ifd[Y_RESOLUTION] = dpi[1] 

1834 

1835 if bits != (1,): 

1836 ifd[BITSPERSAMPLE] = bits 

1837 if len(bits) != 1: 

1838 ifd[SAMPLESPERPIXEL] = len(bits) 

1839 if extra is not None: 

1840 ifd[EXTRASAMPLES] = extra 

1841 if format != 1: 

1842 ifd[SAMPLEFORMAT] = format 

1843 

1844 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1845 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

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

1847 if im.mode == "1": 

1848 inverted_im = im.copy() 

1849 px = inverted_im.load() 

1850 if px is not None: 

1851 for y in range(inverted_im.height): 

1852 for x in range(inverted_im.width): 

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

1854 im = inverted_im 

1855 else: 

1856 im = ImageOps.invert(im) 

1857 

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

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

1860 colormap = [] 

1861 colors = len(lut) // 3 

1862 for i in range(3): 

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

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

1865 ifd[COLORMAP] = colormap 

1866 # data orientation 

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

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

1869 if ROWSPERSTRIP not in ifd: 

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

1871 if libtiff: 

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

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

1874 # JPEG encoder expects multiple of 8 rows 

1875 if compression == "jpeg": 

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

1877 else: 

1878 rows_per_strip = h 

1879 if rows_per_strip == 0: 

1880 rows_per_strip = 1 

1881 ifd[ROWSPERSTRIP] = rows_per_strip 

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

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

1884 if strip_byte_counts >= 2**16: 

1885 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

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

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

1888 ) 

1889 ifd[STRIPOFFSETS] = tuple( 

1890 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1891 ) # this is adjusted by IFD writer 

1892 # no compression by default: 

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

1894 

1895 if im.mode == "YCbCr": 

1896 for tag, default_value in { 

1897 YCBCRSUBSAMPLING: (1, 1), 

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

1899 }.items(): 

1900 ifd.setdefault(tag, default_value) 

1901 

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

1903 if libtiff: 

1904 if "quality" in encoderinfo: 

1905 quality = encoderinfo["quality"] 

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

1907 msg = "Invalid quality setting" 

1908 raise ValueError(msg) 

1909 if compression != "jpeg": 

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

1911 raise ValueError(msg) 

1912 ifd[JPEGQUALITY] = quality 

1913 

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

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

1916 _fp = 0 

1917 if hasattr(fp, "fileno"): 

1918 try: 

1919 fp.seek(0) 

1920 _fp = fp.fileno() 

1921 except io.UnsupportedOperation: 

1922 pass 

1923 

1924 # optional types for non core tags 

1925 types = {} 

1926 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1927 # based on the data in the strip. 

1928 # OSUBFILETYPE is deprecated. 

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

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

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

1932 # SUBIFD may also cause a segfault. 

1933 blocklist += [ 

1934 OSUBFILETYPE, 

1935 REFERENCEBLACKWHITE, 

1936 STRIPBYTECOUNTS, 

1937 STRIPOFFSETS, 

1938 TRANSFERFUNCTION, 

1939 SUBIFD, 

1940 ] 

1941 

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

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

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

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

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

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

1948 # Libtiff can only process certain core items without adding 

1949 # them to the custom dictionary. 

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

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

1952 if tag not in TiffTags.LIBTIFF_CORE: 

1953 if tag in TiffTags.TAGS_V2_GROUPS: 

1954 types[tag] = TiffTags.LONG8 

1955 elif tag in ifd.tagtype: 

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

1957 elif isinstance(value, (int, float, str, bytes)) or ( 

1958 isinstance(value, tuple) 

1959 and all(isinstance(v, (int, float, IFDRational)) for v in value) 

1960 ): 

1961 type = TiffTags.lookup(tag).type 

1962 if type: 

1963 types[tag] = type 

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

1965 if isinstance(value, str): 

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

1967 elif isinstance(value, IFDRational): 

1968 atts[tag] = float(value) 

1969 else: 

1970 atts[tag] = value 

1971 

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

1973 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1974 

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

1976 

1977 # libtiff always expects the bytes in native order. 

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

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

1980 # byte order. 

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

1982 rawmode = "I;16N" 

1983 

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

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

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

1987 tags = list(atts.items()) 

1988 tags.sort() 

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

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

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

1992 while True: 

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

1994 if not _fp: 

1995 fp.write(data) 

1996 if errcode: 

1997 break 

1998 if errcode < 0: 

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

2000 raise OSError(msg) 

2001 

2002 else: 

2003 for tag in blocklist: 

2004 del ifd[tag] 

2005 offset = ifd.save(fp) 

2006 

2007 ImageFile._save( 

2008 im, 

2009 fp, 

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

2011 ) 

2012 

2013 # -- helper for multi-page save -- 

2014 if "_debug_multipage" in encoderinfo: 

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

2016 setattr(im, "_debug_multipage", ifd) 

2017 

2018 

2019class AppendingTiffWriter(io.BytesIO): 

2020 fieldSizes = [ 

2021 0, # None 

2022 1, # byte 

2023 1, # ascii 

2024 2, # short 

2025 4, # long 

2026 8, # rational 

2027 1, # sbyte 

2028 1, # undefined 

2029 2, # sshort 

2030 4, # slong 

2031 8, # srational 

2032 4, # float 

2033 8, # double 

2034 4, # ifd 

2035 2, # unicode 

2036 4, # complex 

2037 8, # long8 

2038 ] 

2039 

2040 Tags = { 

2041 273, # StripOffsets 

2042 288, # FreeOffsets 

2043 324, # TileOffsets 

2044 519, # JPEGQTables 

2045 520, # JPEGDCTables 

2046 521, # JPEGACTables 

2047 } 

2048 

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

2050 self.f: IO[bytes] 

2051 if is_path(fn): 

2052 self.name = fn 

2053 self.close_fp = True 

2054 try: 

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

2056 except OSError: 

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

2058 else: 

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

2060 self.close_fp = False 

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

2062 self.setup() 

2063 

2064 def setup(self) -> None: 

2065 # Reset everything. 

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

2067 

2068 self.whereToWriteNewIFDOffset: int | None = None 

2069 self.offsetOfNewPage = 0 

2070 

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

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

2073 if not iimm: 

2074 # empty file - first page 

2075 self.isFirst = True 

2076 return 

2077 

2078 self.isFirst = False 

2079 if iimm not in PREFIXES: 

2080 msg = "Invalid TIFF file header" 

2081 raise RuntimeError(msg) 

2082 

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

2084 

2085 if self._bigtiff: 

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

2087 self.skipIFDs() 

2088 self.goToEnd() 

2089 

2090 def finalize(self) -> None: 

2091 if self.isFirst: 

2092 return 

2093 

2094 # fix offsets 

2095 self.f.seek(self.offsetOfNewPage) 

2096 

2097 iimm = self.f.read(4) 

2098 if not iimm: 

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

2100 return 

2101 

2102 if iimm != self.IIMM: 

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

2104 raise RuntimeError(msg) 

2105 

2106 if self._bigtiff: 

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

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

2109 ifd_offset += self.offsetOfNewPage 

2110 assert self.whereToWriteNewIFDOffset is not None 

2111 self.f.seek(self.whereToWriteNewIFDOffset) 

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

2113 self.f.seek(ifd_offset) 

2114 self.fixIFD() 

2115 

2116 def newFrame(self) -> None: 

2117 # Call this to finish a frame. 

2118 self.finalize() 

2119 self.setup() 

2120 

2121 def __enter__(self) -> AppendingTiffWriter: 

2122 return self 

2123 

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

2125 if self.close_fp: 

2126 self.close() 

2127 

2128 def tell(self) -> int: 

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

2130 

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

2132 """ 

2133 :param offset: Distance to seek. 

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

2135 end or current position. 

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

2137 """ 

2138 if whence == os.SEEK_SET: 

2139 offset += self.offsetOfNewPage 

2140 

2141 self.f.seek(offset, whence) 

2142 return self.tell() 

2143 

2144 def goToEnd(self) -> None: 

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

2146 pos = self.f.tell() 

2147 

2148 # pad to 16 byte boundary 

2149 pad_bytes = 16 - pos % 16 

2150 if 0 < pad_bytes < 16: 

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

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

2153 

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

2155 self.endian = endian 

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

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

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

2159 

2160 def skipIFDs(self) -> None: 

2161 while True: 

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

2163 if ifd_offset == 0: 

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

2165 8 if self._bigtiff else 4 

2166 ) 

2167 break 

2168 

2169 self.f.seek(ifd_offset) 

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

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

2172 

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

2174 return self.f.write(data) 

2175 

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

2177 try: 

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

2179 except KeyError: 

2180 msg = "offset is not supported" 

2181 raise RuntimeError(msg) 

2182 

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

2184 (value,) = struct.unpack( 

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

2186 ) 

2187 return value 

2188 

2189 def readShort(self) -> int: 

2190 return self._read(2) 

2191 

2192 def readLong(self) -> int: 

2193 return self._read(4) 

2194 

2195 @staticmethod 

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

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

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

2199 raise RuntimeError(msg) 

2200 

2201 def _rewriteLast( 

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

2203 ) -> None: 

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

2205 if not new_field_size: 

2206 new_field_size = field_size 

2207 bytes_written = self.f.write( 

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

2209 ) 

2210 self._verify_bytes_written(bytes_written, new_field_size) 

2211 

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

2213 self._rewriteLast(value, 2, 4) 

2214 

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

2216 return self._rewriteLast(value, 2) 

2217 

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

2219 return self._rewriteLast(value, 4) 

2220 

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

2222 bytes_written = self.f.write( 

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

2224 ) 

2225 self._verify_bytes_written(bytes_written, field_size) 

2226 

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

2228 self._write(value, 2) 

2229 

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

2231 self._write(value, 4) 

2232 

2233 def close(self) -> None: 

2234 self.finalize() 

2235 if self.close_fp: 

2236 self.f.close() 

2237 

2238 def fixIFD(self) -> None: 

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

2240 

2241 for i in range(num_tags): 

2242 tag, field_type, count = struct.unpack( 

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

2244 ) 

2245 

2246 field_size = self.fieldSizes[field_type] 

2247 total_size = field_size * count 

2248 fmt_size = 8 if self._bigtiff else 4 

2249 is_local = total_size <= fmt_size 

2250 if not is_local: 

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

2252 self._rewriteLast(offset, fmt_size) 

2253 

2254 if tag in self.Tags: 

2255 cur_pos = self.f.tell() 

2256 

2257 logger.debug( 

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

2259 TiffTags.lookup(tag).name, 

2260 tag, 

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

2262 field_type, 

2263 field_size, 

2264 count, 

2265 ) 

2266 

2267 if is_local: 

2268 self._fixOffsets(count, field_size) 

2269 self.f.seek(cur_pos + fmt_size) 

2270 else: 

2271 self.f.seek(offset) 

2272 self._fixOffsets(count, field_size) 

2273 self.f.seek(cur_pos) 

2274 

2275 elif is_local: 

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

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

2278 

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

2280 for i in range(count): 

2281 offset = self._read(field_size) 

2282 offset += self.offsetOfNewPage 

2283 

2284 new_field_size = 0 

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

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

2287 new_field_size = 8 

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

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

2290 new_field_size = 4 

2291 if new_field_size: 

2292 if count != 1: 

2293 msg = "not implemented" 

2294 raise RuntimeError(msg) # XXX TODO 

2295 

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

2297 # local (not referenced with another offset) 

2298 self._rewriteLast(offset, field_size, new_field_size) 

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

2300 rewind = -new_field_size - 4 - 2 

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

2302 self.writeShort(new_field_size) # rewrite the type 

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

2304 else: 

2305 self._rewriteLast(offset, field_size) 

2306 

2307 def fixOffsets( 

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

2309 ) -> None: 

2310 if isShort: 

2311 field_size = 2 

2312 elif isLong: 

2313 field_size = 4 

2314 else: 

2315 field_size = 0 

2316 return self._fixOffsets(count, field_size) 

2317 

2318 

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

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

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

2322 return _save(im, fp, filename) 

2323 

2324 cur_idx = im.tell() 

2325 try: 

2326 with AppendingTiffWriter(fp) as tf: 

2327 for ims in [im] + append_images: 

2328 encoderinfo = ims._attach_default_encoderinfo(im) 

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

2330 ims.encoderconfig = () 

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

2332 

2333 for idx in range(nfr): 

2334 ims.seek(idx) 

2335 ims.load() 

2336 _save(ims, tf, filename) 

2337 tf.newFrame() 

2338 ims.encoderinfo = encoderinfo 

2339 finally: 

2340 im.seek(cur_idx) 

2341 

2342 

2343# 

2344# -------------------------------------------------------------------- 

2345# Register 

2346 

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

2348Image.register_save(TiffImageFile.format, _save) 

2349Image.register_save_all(TiffImageFile.format, _save_all) 

2350 

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

2352 

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