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

1246 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, Self 

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: dict[int, tuple[int, _LoaderFunc]] = {} 

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

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 __float__ = _delegate("__float__") 

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 if offset >= 2**63: 

916 warnings.warn("Tag offset too large") 

917 continue 

918 fp.seek(offset) 

919 data = ImageFile._safe_read(fp, size) 

920 fp.seek(here) 

921 else: 

922 data = data[:size] 

923 

924 if len(data) != size: 

925 warnings.warn( 

926 "Possibly corrupt EXIF data. " 

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

928 f" Skipping tag {tag}" 

929 ) 

930 logger.debug(msg) 

931 continue 

932 

933 if not data: 

934 logger.debug(msg) 

935 continue 

936 

937 self._tagdata[tag] = data 

938 self.tagtype[tag] = typ 

939 

940 msg += " - value: " 

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

942 

943 logger.debug(msg) 

944 

945 (self.next,) = ( 

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

947 if self._bigtiff 

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

949 ) 

950 except OSError as msg: 

951 warnings.warn(str(msg)) 

952 return 

953 

954 def _get_ifh(self) -> bytes: 

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

956 if self._bigtiff: 

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

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

959 

960 return ifh 

961 

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

963 # FIXME What about tagdata? 

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

965 

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

967 

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

969 fmt_size = 8 if self._bigtiff else 4 

970 offset += ( 

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

972 ) 

973 stripoffsets = None 

974 

975 # pass 1: convert tags to binary format 

976 # always write tags in ascending order 

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

978 if tag == STRIPOFFSETS: 

979 stripoffsets = len(entries) 

980 typ = self.tagtype[tag] 

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

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

983 if is_ifd: 

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

985 values = self._tags_v2[tag] 

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

987 ifd[ifd_tag] = ifd_value 

988 data = ifd.tobytes(offset) 

989 else: 

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

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

992 

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

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

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

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

997 logger.debug(msg) 

998 

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

1000 if is_ifd: 

1001 count = 1 

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

1003 count = len(data) 

1004 else: 

1005 count = len(values) 

1006 # figure out if data fits into the entry 

1007 if len(data) <= fmt_size: 

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

1009 else: 

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

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

1012 

1013 # update strip offset data to point beyond auxiliary data 

1014 if stripoffsets is not None: 

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

1016 if data: 

1017 size, handler = self._load_dispatch[typ] 

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

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

1020 else: 

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

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

1023 

1024 # pass 2: write entries to file 

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

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

1027 result += self._pack( 

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

1029 ) 

1030 

1031 # -- overwrite here for multi-page -- 

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

1033 

1034 # pass 3: write auxiliary data to file 

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

1036 result += data 

1037 if len(data) & 1: 

1038 result += b"\0" 

1039 

1040 return result 

1041 

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

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

1044 fp.write(self._get_ifh()) 

1045 

1046 offset = fp.tell() 

1047 result = self.tobytes(offset) 

1048 fp.write(result) 

1049 return offset + len(result) 

1050 

1051 

1052ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1053ImageFileDirectory_v2._write_dispatch = _write_dispatch 

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

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

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

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

1058del _load_dispatch, _write_dispatch, idx, name 

1059 

1060 

1061# Legacy ImageFileDirectory support. 

1062class ImageFileDirectory_v1(ImageFileDirectory_v2): 

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

1064 

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

1066 

1067 ifd = ImageFileDirectory_v1() 

1068 ifd[key] = 'Some Data' 

1069 ifd.tagtype[key] = TiffTags.ASCII 

1070 print(ifd[key]) 

1071 ('Some Data',) 

1072 

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

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

1075 

1076 Values are returned as a tuple. 

1077 

1078 .. deprecated:: 3.0.0 

1079 """ 

1080 

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

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

1083 self._legacy_api = True 

1084 

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

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

1087 

1088 # defined in ImageFileDirectory_v2 

1089 tagtype: dict[int, int] 

1090 """Dictionary of tag types""" 

1091 

1092 @classmethod 

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

1094 """Returns an 

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

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

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

1098 instance. 

1099 

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

1101 

1102 """ 

1103 

1104 ifd = cls(prefix=original.prefix) 

1105 ifd._tagdata = original._tagdata 

1106 ifd.tagtype = original.tagtype 

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

1108 return ifd 

1109 

1110 def to_v2(self) -> ImageFileDirectory_v2: 

1111 """Returns an 

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

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

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

1115 instance. 

1116 

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

1118 

1119 """ 

1120 

1121 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1122 ifd._tagdata = dict(self._tagdata) 

1123 ifd.tagtype = dict(self.tagtype) 

1124 ifd._tags_v2 = dict(self._tags_v2) 

1125 return ifd 

1126 

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

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

1129 

1130 def __len__(self) -> int: 

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

1132 

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

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

1135 

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

1137 for legacy_api in (False, True): 

1138 self._setitem(tag, value, legacy_api) 

1139 

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

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

1142 data = self._tagdata[tag] 

1143 typ = self.tagtype[tag] 

1144 size, handler = self._load_dispatch[typ] 

1145 for legacy in (False, True): 

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

1147 val = self._tags_v1[tag] 

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

1149 val = (val,) 

1150 return val 

1151 

1152 

1153# undone -- switch this pointer 

1154ImageFileDirectory = ImageFileDirectory_v1 

1155 

1156 

1157## 

1158# Image plugin for TIFF files. 

1159 

1160 

1161class TiffImageFile(ImageFile.ImageFile): 

1162 format = "TIFF" 

1163 format_description = "Adobe TIFF" 

1164 _close_exclusive_fp_after_loading = False 

1165 

1166 def __init__( 

1167 self, 

1168 fp: StrOrBytesPath | IO[bytes], 

1169 filename: str | bytes | None = None, 

1170 ) -> None: 

1171 self.tag_v2: ImageFileDirectory_v2 

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

1173 

1174 self.tag: ImageFileDirectory_v1 

1175 """ Legacy tag entries """ 

1176 

1177 super().__init__(fp, filename) 

1178 

1179 def _open(self) -> None: 

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

1181 

1182 # Header 

1183 assert self.fp is not None 

1184 ifh = self.fp.read(8) 

1185 if ifh[2] == 43: 

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

1187 

1188 self.tag_v2 = ImageFileDirectory_v2(ifh) 

1189 

1190 # setup frame pointers 

1191 self.__first = self.__next = self.tag_v2.next 

1192 self.__frame = -1 

1193 self._fp = self.fp 

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

1195 self._n_frames: int | None = None 

1196 

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

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

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

1200 

1201 # and load the first frame 

1202 self._seek(0) 

1203 

1204 @property 

1205 def n_frames(self) -> int: 

1206 current_n_frames = self._n_frames 

1207 if current_n_frames is None: 

1208 current = self.tell() 

1209 self._seek(len(self._frame_pos)) 

1210 while self._n_frames is None: 

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

1212 self.seek(current) 

1213 assert self._n_frames is not None 

1214 return self._n_frames 

1215 

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

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

1218 if not self._seek_check(frame): 

1219 return 

1220 self._seek(frame) 

1221 if self._im is not None and ( 

1222 self.im.size != self._tile_size 

1223 or self.im.mode != self.mode 

1224 or self.readonly 

1225 ): 

1226 self._im = None 

1227 

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

1229 if isinstance(self._fp, DeferredError): 

1230 raise self._fp.ex 

1231 self.fp = self._fp 

1232 

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

1234 if not self.__next: 

1235 msg = "no more images in TIFF file" 

1236 raise EOFError(msg) 

1237 logger.debug( 

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

1239 frame, 

1240 self.__frame, 

1241 self.__next, 

1242 self.fp.tell(), 

1243 ) 

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

1245 msg = "Unable to seek to frame" 

1246 raise ValueError(msg) 

1247 self.fp.seek(self.__next) 

1248 self._frame_pos.append(self.__next) 

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

1250 self.tag_v2.load(self.fp) 

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

1252 # This IFD has already been processed 

1253 # Declare this to be the end of the image 

1254 self.__next = 0 

1255 else: 

1256 self.__next = self.tag_v2.next 

1257 if self.__next == 0: 

1258 self._n_frames = frame + 1 

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

1260 self.is_animated = self.__next != 0 

1261 self.__frame += 1 

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

1263 self.tag_v2.load(self.fp) 

1264 if XMP in self.tag_v2: 

1265 xmp = self.tag_v2[XMP] 

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

1267 xmp = xmp[0] 

1268 self.info["xmp"] = xmp 

1269 elif "xmp" in self.info: 

1270 del self.info["xmp"] 

1271 self._reload_exif() 

1272 # fill the legacy tag/ifd entries 

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

1274 self.__frame = frame 

1275 self._setup() 

1276 

1277 def tell(self) -> int: 

1278 """Return the current frame number""" 

1279 return self.__frame 

1280 

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

1282 """ 

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

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

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

1286 

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

1288 """ 

1289 blocks = {} 

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

1291 if val: 

1292 while val.startswith(b"8BIM") and len(val) >= 12: 

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

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

1295 try: 

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

1297 except struct.error: 

1298 break 

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

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

1301 

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

1303 return blocks 

1304 

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

1306 if self.tile and self.use_load_libtiff: 

1307 return self._load_libtiff() 

1308 return super().load() 

1309 

1310 def load_prepare(self) -> None: 

1311 if self._im is None: 

1312 Image._decompression_bomb_check(self._tile_size) 

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

1314 ImageFile.ImageFile.load_prepare(self) 

1315 

1316 def load_end(self) -> None: 

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

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

1319 if not self.is_animated: 

1320 self._close_exclusive_fp_after_loading = True 

1321 

1322 # load IFD data from fp before it is closed 

1323 exif = self.getexif() 

1324 for key in TiffTags.TAGS_V2_GROUPS: 

1325 if key not in exif: 

1326 continue 

1327 exif.get_ifd(key) 

1328 

1329 ImageOps.exif_transpose(self, in_place=True) 

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

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

1332 

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

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

1335 Calls out to libtiff""" 

1336 

1337 Image.Image.load(self) 

1338 

1339 self.load_prepare() 

1340 

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

1342 msg = "Not exactly one tile" 

1343 raise OSError(msg) 

1344 

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

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

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

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

1349 

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

1351 # file descriptor, use that instead of reading 

1352 # into a string in python. 

1353 assert self.fp is not None 

1354 try: 

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

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

1357 # should also eliminate the need for fp.tell 

1358 # in _seek 

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

1360 self.fp.flush() 

1361 except OSError: 

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

1363 # it doesn't use a file descriptor. 

1364 fp = False 

1365 

1366 if fp: 

1367 assert isinstance(args, tuple) 

1368 args_list = list(args) 

1369 args_list[2] = fp 

1370 args = tuple(args_list) 

1371 

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

1373 try: 

1374 decoder.setimage(self.im, extents) 

1375 except ValueError as e: 

1376 msg = "Couldn't set the image" 

1377 raise OSError(msg) from e 

1378 

1379 close_self_fp = self._exclusive_fp and not self.is_animated 

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

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

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

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

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

1385 # underlying string for stringio. 

1386 # 

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

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

1389 # deal with here by reordering. 

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

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

1392 elif fp: 

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

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

1395 if not close_self_fp: 

1396 self.fp.seek(0) 

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

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

1399 # io.BufferedReader and possible others. 

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

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

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

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

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

1405 # 4 bytes, otherwise the trace might error out 

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

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

1408 else: 

1409 # we have something else. 

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

1411 self.fp.seek(0) 

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

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

1414 

1415 self.tile = [] 

1416 self.readonly = 0 

1417 

1418 self.load_end() 

1419 

1420 if close_self_fp: 

1421 self.fp.close() 

1422 self.fp = None # might be shared 

1423 

1424 if err < 0: 

1425 msg = f"decoder error {err}" 

1426 raise OSError(msg) 

1427 

1428 return Image.Image.load(self) 

1429 

1430 def _setup(self) -> None: 

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

1432 

1433 if 0xBC01 in self.tag_v2: 

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

1435 raise OSError(msg) 

1436 

1437 # extract relevant tags 

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

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

1440 

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

1442 # the specification 

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

1444 

1445 # old style jpeg compression images most certainly are YCbCr 

1446 if self._compression == "tiff_jpeg": 

1447 photo = 6 

1448 

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

1450 

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

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

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

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

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

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

1457 

1458 # size 

1459 try: 

1460 xsize = self.tag_v2[IMAGEWIDTH] 

1461 ysize = self.tag_v2[IMAGELENGTH] 

1462 except KeyError as e: 

1463 msg = "Missing dimensions" 

1464 raise TypeError(msg) from e 

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

1466 msg = "Invalid dimensions" 

1467 raise ValueError(msg) 

1468 self._tile_size = xsize, ysize 

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

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

1471 self._size = ysize, xsize 

1472 else: 

1473 self._size = xsize, ysize 

1474 

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

1476 

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

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

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

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

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

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

1483 # for more exotic images. 

1484 sample_format = (sample_format[0],) 

1485 

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

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

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 if photo in (2, 6, 8): # RGB, YCbCr, LAB 

1493 bps_count = 3 

1494 elif photo == 5: # CMYK 

1495 bps_count = 4 

1496 else: 

1497 bps_count = 1 

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

1499 # If components are stored separately, 

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

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

1502 samples_per_pixel -= len(extra_tuple) 

1503 extra_tuple = () 

1504 bps_count += len(extra_tuple) 

1505 bps_actual_count = len(bps_tuple) 

1506 

1507 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

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

1509 logger.error( 

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

1511 ) 

1512 msg = "Invalid value for samples per pixel" 

1513 raise SyntaxError(msg) 

1514 

1515 if samples_per_pixel < bps_actual_count: 

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

1517 # remove the excess. 

1518 bps_tuple = bps_tuple[:samples_per_pixel] 

1519 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

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

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

1522 bps_tuple = bps_tuple * samples_per_pixel 

1523 

1524 if len(bps_tuple) != samples_per_pixel: 

1525 msg = "unknown data organization" 

1526 raise SyntaxError(msg) 

1527 

1528 # mode: check photometric interpretation and bits per pixel 

1529 key = ( 

1530 self.tag_v2.prefix, 

1531 photo, 

1532 sample_format, 

1533 fillorder, 

1534 bps_tuple, 

1535 extra_tuple, 

1536 ) 

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

1538 try: 

1539 self._mode, rawmode = OPEN_INFO[key] 

1540 except KeyError as e: 

1541 logger.debug("- unsupported format") 

1542 msg = "unknown pixel mode" 

1543 raise SyntaxError(msg) from e 

1544 

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

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

1547 

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

1549 

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

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

1552 

1553 if xres and yres: 

1554 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1555 if resunit == 2: # dots per inch 

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

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

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

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

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

1561 # For backward compatibility, 

1562 # we also preserve the old behavior 

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

1564 else: # No absolute unit of measurement 

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

1566 

1567 # build tile descriptors 

1568 x = y = layer = 0 

1569 self.tile = [] 

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

1571 if self.use_load_libtiff: 

1572 # Decoder expects entire file as one tile. 

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

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

1575 # function. 

1576 # 

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

1578 # use the _load_libtiff function. 

1579 

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

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

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

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

1584 if fillorder == 2: 

1585 # Replace fillorder with fillorder=1 

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

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

1588 # this should always work, since all the 

1589 # fillorder==2 modes have a corresponding 

1590 # fillorder=1 mode 

1591 self._mode, rawmode = OPEN_INFO[key] 

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

1593 # unpacked straight into RGB values 

1594 if ( 

1595 photo == 6 

1596 and self._compression == "jpeg" 

1597 and self._planar_configuration == 1 

1598 ): 

1599 rawmode = "RGB" 

1600 # libtiff always returns the bytes in native order. 

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

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

1603 # byte order. 

1604 elif rawmode == "I;16": 

1605 rawmode = "I;16N" 

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

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

1608 

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

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

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

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

1613 

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

1615 # striped image 

1616 if STRIPOFFSETS in self.tag_v2: 

1617 offsets = self.tag_v2[STRIPOFFSETS] 

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

1619 w = xsize 

1620 else: 

1621 # tiled image 

1622 offsets = self.tag_v2[TILEOFFSETS] 

1623 tilewidth = self.tag_v2.get(TILEWIDTH) 

1624 h = self.tag_v2.get(TILELENGTH) 

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

1626 msg = "Invalid tile dimensions" 

1627 raise ValueError(msg) 

1628 w = tilewidth 

1629 

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

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

1632 offsets = offsets[-1:] 

1633 

1634 for offset in offsets: 

1635 if x + w > xsize: 

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

1637 else: 

1638 stride = 0 

1639 

1640 tile_rawmode = rawmode 

1641 if self._planar_configuration == 2: 

1642 # each band on it's own layer 

1643 tile_rawmode = rawmode[layer] 

1644 # adjust stride width accordingly 

1645 stride /= bps_count 

1646 

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

1648 self.tile.append( 

1649 ImageFile._Tile( 

1650 self._compression, 

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

1652 offset, 

1653 args, 

1654 ) 

1655 ) 

1656 x += w 

1657 if x >= xsize: 

1658 x, y = 0, y + h 

1659 if y >= ysize: 

1660 y = 0 

1661 layer += 1 

1662 else: 

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

1664 msg = "unknown data organization" 

1665 raise SyntaxError(msg) 

1666 

1667 # Fix up info. 

1668 if ICCPROFILE in self.tag_v2: 

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

1670 

1671 # fixup palette descriptor 

1672 

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

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

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

1676 

1677 

1678# 

1679# -------------------------------------------------------------------- 

1680# Write TIFF files 

1681 

1682# little endian is default except for image modes with 

1683# explicit big endian byte-order 

1684 

1685SAVE_INFO = { 

1686 # mode => rawmode, byteorder, photometrics, 

1687 # sampleformat, bitspersample, extra 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1704} 

1705 

1706 

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

1708 try: 

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

1710 except KeyError as e: 

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

1712 raise OSError(msg) from e 

1713 

1714 encoderinfo = im.encoderinfo 

1715 encoderconfig = im.encoderconfig 

1716 

1717 ifd = ImageFileDirectory_v2(prefix=prefix) 

1718 if encoderinfo.get("big_tiff"): 

1719 ifd._bigtiff = True 

1720 

1721 try: 

1722 compression = encoderinfo["compression"] 

1723 except KeyError: 

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

1725 if isinstance(compression, int): 

1726 # compression value may be from BMP. Ignore it 

1727 compression = None 

1728 if compression is None: 

1729 compression = "raw" 

1730 elif compression == "tiff_jpeg": 

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

1732 compression = "jpeg" 

1733 elif compression == "tiff_deflate": 

1734 compression = "tiff_adobe_deflate" 

1735 

1736 libtiff = WRITE_LIBTIFF or compression != "raw" 

1737 

1738 # required for color libtiff images 

1739 ifd[PLANAR_CONFIGURATION] = 1 

1740 

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

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

1743 

1744 # write any arbitrary tags passed in as an ImageFileDirectory 

1745 if "tiffinfo" in encoderinfo: 

1746 info = encoderinfo["tiffinfo"] 

1747 elif "exif" in encoderinfo: 

1748 info = encoderinfo["exif"] 

1749 if isinstance(info, bytes): 

1750 exif = Image.Exif() 

1751 exif.load(info) 

1752 info = exif 

1753 else: 

1754 info = {} 

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

1756 if isinstance(info, ImageFileDirectory_v1): 

1757 info = info.to_v2() 

1758 for key in info: 

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

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

1761 else: 

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

1763 try: 

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

1765 except Exception: 

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

1767 

1768 legacy_ifd = {} 

1769 if hasattr(im, "tag"): 

1770 legacy_ifd = im.tag.to_v2() 

1771 

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

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

1774 # If the image used separate component planes, 

1775 # then EXTRASAMPLES should be ignored when saving contiguously 

1776 if SAMPLESPERPIXEL in supplied_tags: 

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

1778 del supplied_tags[EXTRASAMPLES] 

1779 for tag in ( 

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

1781 EXIFIFD, 

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

1783 SAMPLEFORMAT, 

1784 ): 

1785 if tag in supplied_tags: 

1786 del supplied_tags[tag] 

1787 

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

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

1790 if hasattr(im, "tag_v2"): 

1791 # preserve tags from original TIFF image file 

1792 for key in ( 

1793 RESOLUTION_UNIT, 

1794 X_RESOLUTION, 

1795 Y_RESOLUTION, 

1796 IPTC_NAA_CHUNK, 

1797 PHOTOSHOP_CHUNK, 

1798 XMP, 

1799 ): 

1800 if key in im.tag_v2: 

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

1802 TiffTags.BYTE, 

1803 TiffTags.UNDEFINED, 

1804 ): 

1805 del supplied_tags[key] 

1806 else: 

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

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

1809 

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

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

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

1813 if icc: 

1814 ifd[ICCPROFILE] = icc 

1815 

1816 for key, name in [ 

1817 (IMAGEDESCRIPTION, "description"), 

1818 (X_RESOLUTION, "resolution"), 

1819 (Y_RESOLUTION, "resolution"), 

1820 (X_RESOLUTION, "x_resolution"), 

1821 (Y_RESOLUTION, "y_resolution"), 

1822 (RESOLUTION_UNIT, "resolution_unit"), 

1823 (SOFTWARE, "software"), 

1824 (DATE_TIME, "date_time"), 

1825 (ARTIST, "artist"), 

1826 (COPYRIGHT, "copyright"), 

1827 ]: 

1828 if name in encoderinfo: 

1829 ifd[key] = encoderinfo[name] 

1830 

1831 dpi = encoderinfo.get("dpi") 

1832 if dpi: 

1833 ifd[RESOLUTION_UNIT] = 2 

1834 ifd[X_RESOLUTION] = dpi[0] 

1835 ifd[Y_RESOLUTION] = dpi[1] 

1836 

1837 if bits != (1,): 

1838 ifd[BITSPERSAMPLE] = bits 

1839 if len(bits) != 1: 

1840 ifd[SAMPLESPERPIXEL] = len(bits) 

1841 if extra is not None: 

1842 ifd[EXTRASAMPLES] = extra 

1843 if format != 1: 

1844 ifd[SAMPLEFORMAT] = format 

1845 

1846 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1847 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

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

1849 if im.mode == "1": 

1850 inverted_im = im.copy() 

1851 px = inverted_im.load() 

1852 if px is not None: 

1853 for y in range(inverted_im.height): 

1854 for x in range(inverted_im.width): 

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

1856 im = inverted_im 

1857 else: 

1858 im = ImageOps.invert(im) 

1859 

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

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

1862 colormap = [] 

1863 colors = len(lut) // 3 

1864 for i in range(3): 

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

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

1867 ifd[COLORMAP] = colormap 

1868 # data orientation 

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

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

1871 if ROWSPERSTRIP not in ifd: 

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

1873 if libtiff: 

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

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

1876 # JPEG encoder expects multiple of 8 rows 

1877 if compression == "jpeg": 

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

1879 else: 

1880 rows_per_strip = h 

1881 if rows_per_strip == 0: 

1882 rows_per_strip = 1 

1883 ifd[ROWSPERSTRIP] = rows_per_strip 

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

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

1886 if strip_byte_counts >= 2**16: 

1887 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

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

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

1890 ) 

1891 ifd[STRIPOFFSETS] = tuple( 

1892 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1893 ) # this is adjusted by IFD writer 

1894 # no compression by default: 

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

1896 

1897 if im.mode == "YCbCr": 

1898 for tag, default_value in { 

1899 YCBCRSUBSAMPLING: (1, 1), 

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

1901 }.items(): 

1902 ifd.setdefault(tag, default_value) 

1903 

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

1905 if libtiff: 

1906 if "quality" in encoderinfo: 

1907 quality = encoderinfo["quality"] 

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

1909 msg = "Invalid quality setting" 

1910 raise ValueError(msg) 

1911 if compression != "jpeg": 

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

1913 raise ValueError(msg) 

1914 ifd[JPEGQUALITY] = quality 

1915 

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

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

1918 _fp = 0 

1919 if hasattr(fp, "fileno"): 

1920 try: 

1921 fp.seek(0) 

1922 _fp = fp.fileno() 

1923 except io.UnsupportedOperation: 

1924 pass 

1925 

1926 # optional types for non core tags 

1927 types = {} 

1928 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1929 # based on the data in the strip. 

1930 # OSUBFILETYPE is deprecated. 

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

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

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

1934 # SUBIFD may also cause a segfault. 

1935 blocklist += [ 

1936 OSUBFILETYPE, 

1937 REFERENCEBLACKWHITE, 

1938 STRIPBYTECOUNTS, 

1939 STRIPOFFSETS, 

1940 TRANSFERFUNCTION, 

1941 SUBIFD, 

1942 ] 

1943 

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

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

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

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

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

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

1950 # Libtiff can only process certain core items without adding 

1951 # them to the custom dictionary. 

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

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

1954 if tag not in TiffTags.LIBTIFF_CORE: 

1955 if tag in TiffTags.TAGS_V2_GROUPS: 

1956 types[tag] = TiffTags.LONG8 

1957 elif tag in ifd.tagtype: 

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

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

1960 isinstance(value, tuple) 

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

1962 ): 

1963 type = TiffTags.lookup(tag).type 

1964 if type: 

1965 types[tag] = type 

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

1967 if isinstance(value, str): 

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

1969 elif isinstance(value, IFDRational): 

1970 atts[tag] = float(value) 

1971 else: 

1972 atts[tag] = value 

1973 

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

1975 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1976 

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

1978 

1979 # libtiff always expects the bytes in native order. 

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

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

1982 # byte order. 

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

1984 rawmode = "I;16N" 

1985 

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

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

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

1989 tags = list(atts.items()) 

1990 tags.sort() 

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

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

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

1994 while True: 

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

1996 if not _fp: 

1997 fp.write(data) 

1998 if errcode: 

1999 break 

2000 if errcode < 0: 

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

2002 raise OSError(msg) 

2003 

2004 else: 

2005 for tag in blocklist: 

2006 del ifd[tag] 

2007 offset = ifd.save(fp) 

2008 

2009 ImageFile._save( 

2010 im, 

2011 fp, 

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

2013 ) 

2014 

2015 # -- helper for multi-page save -- 

2016 if "_debug_multipage" in encoderinfo: 

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

2018 setattr(im, "_debug_multipage", ifd) 

2019 

2020 

2021class AppendingTiffWriter(io.BytesIO): 

2022 fieldSizes = [ 

2023 0, # None 

2024 1, # byte 

2025 1, # ascii 

2026 2, # short 

2027 4, # long 

2028 8, # rational 

2029 1, # sbyte 

2030 1, # undefined 

2031 2, # sshort 

2032 4, # slong 

2033 8, # srational 

2034 4, # float 

2035 8, # double 

2036 4, # ifd 

2037 2, # unicode 

2038 4, # complex 

2039 8, # long8 

2040 ] 

2041 

2042 Tags = { 

2043 273, # StripOffsets 

2044 288, # FreeOffsets 

2045 324, # TileOffsets 

2046 519, # JPEGQTables 

2047 520, # JPEGDCTables 

2048 521, # JPEGACTables 

2049 } 

2050 

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

2052 self.f: IO[bytes] 

2053 if is_path(fn): 

2054 self.name = fn 

2055 self.close_fp = True 

2056 try: 

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

2058 except OSError: 

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

2060 else: 

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

2062 self.close_fp = False 

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

2064 self.setup() 

2065 

2066 def setup(self) -> None: 

2067 # Reset everything. 

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

2069 

2070 self.whereToWriteNewIFDOffset: int | None = None 

2071 self.offsetOfNewPage = 0 

2072 

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

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

2075 if not iimm: 

2076 # empty file - first page 

2077 self.isFirst = True 

2078 return 

2079 

2080 self.isFirst = False 

2081 if iimm not in PREFIXES: 

2082 msg = "Invalid TIFF file header" 

2083 raise RuntimeError(msg) 

2084 

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

2086 

2087 if self._bigtiff: 

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

2089 self.skipIFDs() 

2090 self.goToEnd() 

2091 

2092 def finalize(self) -> None: 

2093 if self.isFirst: 

2094 return 

2095 

2096 # fix offsets 

2097 self.f.seek(self.offsetOfNewPage) 

2098 

2099 iimm = self.f.read(4) 

2100 if not iimm: 

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

2102 return 

2103 

2104 if iimm != self.IIMM: 

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

2106 raise RuntimeError(msg) 

2107 

2108 if self._bigtiff: 

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

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

2111 ifd_offset += self.offsetOfNewPage 

2112 assert self.whereToWriteNewIFDOffset is not None 

2113 self.f.seek(self.whereToWriteNewIFDOffset) 

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

2115 self.f.seek(ifd_offset) 

2116 self.fixIFD() 

2117 

2118 def newFrame(self) -> None: 

2119 # Call this to finish a frame. 

2120 self.finalize() 

2121 self.setup() 

2122 

2123 def __enter__(self) -> Self: 

2124 return self 

2125 

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

2127 if self.close_fp: 

2128 self.close() 

2129 

2130 def tell(self) -> int: 

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

2132 

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

2134 """ 

2135 :param offset: Distance to seek. 

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

2137 end or current position. 

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

2139 """ 

2140 if whence == os.SEEK_SET: 

2141 offset += self.offsetOfNewPage 

2142 

2143 self.f.seek(offset, whence) 

2144 return self.tell() 

2145 

2146 def goToEnd(self) -> None: 

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

2148 pos = self.f.tell() 

2149 

2150 # pad to 16 byte boundary 

2151 pad_bytes = 16 - pos % 16 

2152 if 0 < pad_bytes < 16: 

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

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

2155 

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

2157 self.endian = endian 

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

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

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

2161 

2162 def skipIFDs(self) -> None: 

2163 while True: 

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

2165 if ifd_offset == 0: 

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

2167 8 if self._bigtiff else 4 

2168 ) 

2169 break 

2170 

2171 self.f.seek(ifd_offset) 

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

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

2174 

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

2176 return self.f.write(data) 

2177 

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

2179 try: 

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

2181 except KeyError: 

2182 msg = "offset is not supported" 

2183 raise RuntimeError(msg) 

2184 

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

2186 (value,) = struct.unpack( 

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

2188 ) 

2189 return value 

2190 

2191 def readShort(self) -> int: 

2192 return self._read(2) 

2193 

2194 def readLong(self) -> int: 

2195 return self._read(4) 

2196 

2197 @staticmethod 

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

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

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

2201 raise RuntimeError(msg) 

2202 

2203 def _rewriteLast( 

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

2205 ) -> None: 

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

2207 if not new_field_size: 

2208 new_field_size = field_size 

2209 bytes_written = self.f.write( 

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

2211 ) 

2212 self._verify_bytes_written(bytes_written, new_field_size) 

2213 

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

2215 self._rewriteLast(value, 2, 4) 

2216 

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

2218 return self._rewriteLast(value, 2) 

2219 

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

2221 return self._rewriteLast(value, 4) 

2222 

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

2224 bytes_written = self.f.write( 

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

2226 ) 

2227 self._verify_bytes_written(bytes_written, field_size) 

2228 

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

2230 self._write(value, 2) 

2231 

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

2233 self._write(value, 4) 

2234 

2235 def close(self) -> None: 

2236 self.finalize() 

2237 if self.close_fp: 

2238 self.f.close() 

2239 

2240 def fixIFD(self) -> None: 

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

2242 

2243 for i in range(num_tags): 

2244 tag, field_type, count = struct.unpack( 

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

2246 ) 

2247 

2248 field_size = self.fieldSizes[field_type] 

2249 total_size = field_size * count 

2250 fmt_size = 8 if self._bigtiff else 4 

2251 is_local = total_size <= fmt_size 

2252 if not is_local: 

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

2254 self._rewriteLast(offset, fmt_size) 

2255 

2256 if tag in self.Tags: 

2257 cur_pos = self.f.tell() 

2258 

2259 logger.debug( 

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

2261 TiffTags.lookup(tag).name, 

2262 tag, 

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

2264 field_type, 

2265 field_size, 

2266 count, 

2267 ) 

2268 

2269 if is_local: 

2270 self._fixOffsets(count, field_size) 

2271 self.f.seek(cur_pos + fmt_size) 

2272 else: 

2273 self.f.seek(offset) 

2274 self._fixOffsets(count, field_size) 

2275 self.f.seek(cur_pos) 

2276 

2277 elif is_local: 

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

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

2280 

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

2282 for i in range(count): 

2283 offset = self._read(field_size) 

2284 offset += self.offsetOfNewPage 

2285 

2286 new_field_size = 0 

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

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

2289 new_field_size = 8 

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

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

2292 new_field_size = 4 

2293 if new_field_size: 

2294 if count != 1: 

2295 msg = "not implemented" 

2296 raise RuntimeError(msg) # XXX TODO 

2297 

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

2299 # local (not referenced with another offset) 

2300 self._rewriteLast(offset, field_size, new_field_size) 

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

2302 rewind = -new_field_size - 4 - 2 

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

2304 self.writeShort(new_field_size) # rewrite the type 

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

2306 else: 

2307 self._rewriteLast(offset, field_size) 

2308 

2309 def fixOffsets( 

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

2311 ) -> None: 

2312 if isShort: 

2313 field_size = 2 

2314 elif isLong: 

2315 field_size = 4 

2316 else: 

2317 field_size = 0 

2318 return self._fixOffsets(count, field_size) 

2319 

2320 

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

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

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

2324 return _save(im, fp, filename) 

2325 

2326 cur_idx = im.tell() 

2327 try: 

2328 with AppendingTiffWriter(fp) as tf: 

2329 for ims in [im] + append_images: 

2330 encoderinfo = ims._attach_default_encoderinfo(im) 

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

2332 ims.encoderconfig = () 

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

2334 

2335 for idx in range(nfr): 

2336 ims.seek(idx) 

2337 ims.load() 

2338 _save(ims, tf, filename) 

2339 tf.newFrame() 

2340 ims.encoderinfo = encoderinfo 

2341 finally: 

2342 im.seek(cur_idx) 

2343 

2344 

2345# 

2346# -------------------------------------------------------------------- 

2347# Register 

2348 

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

2350Image.register_save(TiffImageFile.format, _save) 

2351Image.register_save_all(TiffImageFile.format, _save_all) 

2352 

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

2354 

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