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

1244 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: 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 # Python >= 3.11 

469 if hasattr(Fraction, "__int__"): 

470 __int__ = _delegate("__int__") 

471 

472 

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

474 

475 

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

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

478 from .TiffTags import TYPES 

479 

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

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

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

483 return func 

484 

485 return decorator 

486 

487 

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

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

490 _write_dispatch[idx] = func # noqa: F821 

491 return func 

492 

493 return decorator 

494 

495 

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

497 from .TiffTags import TYPES 

498 

499 idx, fmt, name = idx_fmt_name 

500 TYPES[idx] = name 

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

502 

503 def basic_handler( 

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

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

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

507 

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

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

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

511 ) 

512 

513 

514if TYPE_CHECKING: 

515 _IFDv2Base = MutableMapping[int, Any] 

516else: 

517 _IFDv2Base = MutableMapping 

518 

519 

520class ImageFileDirectory_v2(_IFDv2Base): 

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

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

523 

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

525 

526 ifd = ImageFileDirectory_v2() 

527 ifd[key] = 'Some Data' 

528 ifd.tagtype[key] = TiffTags.ASCII 

529 print(ifd[key]) 

530 'Some Data' 

531 

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

533 returned as tuples of the values. 

534 

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

536 tag types in 

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

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

539 manually. 

540 

541 Data Structures: 

542 

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

544 

545 * Key: numerical TIFF tag number 

546 * Value: integer corresponding to the data type from 

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

548 

549 .. versionadded:: 3.0.0 

550 

551 'Internal' data structures: 

552 

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

554 

555 * Key: numerical TIFF tag number 

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

557 

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

559 

560 * Key: numerical TIFF tag number 

561 * Value: undecoded byte string from file 

562 

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

564 

565 * Key: numerical TIFF tag number 

566 * Value: decoded data in the v1 format 

567 

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

569 ``self._tags_v2`` once decoded. 

570 

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

572 from outside code. In cooperation with 

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

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

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

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

577 ``legacy_api == true``. 

578 

579 """ 

580 

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

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

583 

584 def __init__( 

585 self, 

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

587 prefix: bytes | None = None, 

588 group: int | None = None, 

589 ) -> None: 

590 """Initialize an ImageFileDirectory. 

591 

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

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

594 as the 'prefix' keyword argument. 

595 

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

597 endianness. 

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

599 """ 

600 if not _accept(ifh): 

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

602 raise SyntaxError(msg) 

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

604 if self._prefix == MM: 

605 self._endian = ">" 

606 elif self._prefix == II: 

607 self._endian = "<" 

608 else: 

609 msg = "not a TIFF IFD" 

610 raise SyntaxError(msg) 

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

612 self.group = group 

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

614 """ Dictionary of tag types """ 

615 self.reset() 

616 self.next = ( 

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

618 if self._bigtiff 

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

620 ) 

621 self._legacy_api = False 

622 

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

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

625 

626 @property 

627 def legacy_api(self) -> bool: 

628 return self._legacy_api 

629 

630 @legacy_api.setter 

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

632 msg = "Not allowing setting of legacy api" 

633 raise Exception(msg) 

634 

635 def reset(self) -> None: 

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

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

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

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

640 self._next = None 

641 self._offset: int | None = None 

642 

643 def __str__(self) -> str: 

644 return str(dict(self)) 

645 

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

647 """ 

648 :returns: dict of name|key: value 

649 

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

651 """ 

652 return { 

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

654 for code, value in self.items() 

655 } 

656 

657 def __len__(self) -> int: 

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

659 

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

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

662 data = self._tagdata[tag] 

663 typ = self.tagtype[tag] 

664 size, handler = self._load_dispatch[typ] 

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

666 val = self._tags_v2[tag] 

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

668 val = (val,) 

669 return val 

670 

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

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

673 

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

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

676 

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

678 basetypes = (Number, bytes, str) 

679 

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

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

682 

683 if tag not in self.tagtype: 

684 if info.type: 

685 self.tagtype[tag] = info.type 

686 else: 

687 self.tagtype[tag] = TiffTags.UNDEFINED 

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

689 for v in values: 

690 assert isinstance(v, IFDRational) 

691 if v < 0: 

692 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL 

693 break 

694 else: 

695 self.tagtype[tag] = TiffTags.RATIONAL 

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

697 short = True 

698 signed_short = True 

699 long = True 

700 for v in values: 

701 assert isinstance(v, int) 

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

703 short = False 

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

705 signed_short = False 

706 if long and v < 0: 

707 long = False 

708 if short: 

709 self.tagtype[tag] = TiffTags.SHORT 

710 elif signed_short: 

711 self.tagtype[tag] = TiffTags.SIGNED_SHORT 

712 elif long: 

713 self.tagtype[tag] = TiffTags.LONG 

714 else: 

715 self.tagtype[tag] = TiffTags.SIGNED_LONG 

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

717 self.tagtype[tag] = TiffTags.DOUBLE 

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

719 self.tagtype[tag] = TiffTags.ASCII 

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

721 self.tagtype[tag] = TiffTags.BYTE 

722 

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

724 values = [ 

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

726 for v in values 

727 ] 

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

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

730 

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

732 if not is_ifd: 

733 values = tuple( 

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

735 for value in values 

736 ) 

737 

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

739 

740 # Three branches: 

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

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

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

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

745 if not is_ifd and ( 

746 (info.length == 1) 

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

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

749 ): 

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

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

752 TiffTags.RATIONAL, 

753 TiffTags.SIGNED_RATIONAL, 

754 ]: # rationals 

755 values = (values,) 

756 try: 

757 (dest[tag],) = values 

758 except ValueError: 

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

760 warnings.warn( 

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

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

763 ) 

764 dest[tag] = values[0] 

765 

766 else: 

767 # Spec'd length > 1 or undefined 

768 # Unspec'd, and length > 1 

769 dest[tag] = values 

770 

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

772 self._tags_v2.pop(tag, None) 

773 self._tags_v1.pop(tag, None) 

774 self._tagdata.pop(tag, None) 

775 

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

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

778 

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

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

781 

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

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

784 

785 list( 

786 map( 

787 _register_basic, 

788 [ 

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

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

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

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

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

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

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

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

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

798 ], 

799 ) 

800 ) 

801 

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

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

804 return data 

805 

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

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

808 if isinstance(data, IFDRational): 

809 data = int(data) 

810 if isinstance(data, int): 

811 data = bytes((data,)) 

812 return data 

813 

814 @_register_loader(2, 1) 

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

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

817 data = data[:-1] 

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

819 

820 @_register_writer(2) 

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

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

823 if isinstance(value, int): 

824 value = str(value) 

825 if not isinstance(value, bytes): 

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

827 return value + b"\0" 

828 

829 @_register_loader(5, 8) 

830 def load_rational( 

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

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

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

834 

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

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

837 

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

839 

840 @_register_writer(5) 

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

842 return b"".join( 

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

844 ) 

845 

846 @_register_loader(7, 1) 

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

848 return data 

849 

850 @_register_writer(7) 

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

852 if isinstance(value, IFDRational): 

853 value = int(value) 

854 if isinstance(value, int): 

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

856 return value 

857 

858 @_register_loader(10, 8) 

859 def load_signed_rational( 

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

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

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

863 

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

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

866 

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

868 

869 @_register_writer(10) 

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

871 return b"".join( 

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

873 for frac in values 

874 ) 

875 

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

877 ret = fp.read(size) 

878 if len(ret) != size: 

879 msg = ( 

880 "Corrupt EXIF data. " 

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

882 ) 

883 raise OSError(msg) 

884 return ret 

885 

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

887 self.reset() 

888 self._offset = fp.tell() 

889 

890 try: 

891 tag_count = ( 

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

893 if self._bigtiff 

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

895 )[0] 

896 for i in range(tag_count): 

897 tag, typ, count, data = ( 

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

899 if self._bigtiff 

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

901 ) 

902 

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

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

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

906 

907 try: 

908 unit_size, handler = self._load_dispatch[typ] 

909 except KeyError: 

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

911 continue # ignore unsupported type 

912 size = count * unit_size 

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

914 here = fp.tell() 

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

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

917 fp.seek(offset) 

918 data = ImageFile._safe_read(fp, size) 

919 fp.seek(here) 

920 else: 

921 data = data[:size] 

922 

923 if len(data) != size: 

924 warnings.warn( 

925 "Possibly corrupt EXIF data. " 

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

927 f" Skipping tag {tag}" 

928 ) 

929 logger.debug(msg) 

930 continue 

931 

932 if not data: 

933 logger.debug(msg) 

934 continue 

935 

936 self._tagdata[tag] = data 

937 self.tagtype[tag] = typ 

938 

939 msg += " - value: " 

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

941 

942 logger.debug(msg) 

943 

944 (self.next,) = ( 

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

946 if self._bigtiff 

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

948 ) 

949 except OSError as msg: 

950 warnings.warn(str(msg)) 

951 return 

952 

953 def _get_ifh(self) -> bytes: 

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

955 if self._bigtiff: 

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

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

958 

959 return ifh 

960 

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

962 # FIXME What about tagdata? 

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

964 

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

966 

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

968 fmt_size = 8 if self._bigtiff else 4 

969 offset += ( 

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

971 ) 

972 stripoffsets = None 

973 

974 # pass 1: convert tags to binary format 

975 # always write tags in ascending order 

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

977 if tag == STRIPOFFSETS: 

978 stripoffsets = len(entries) 

979 typ = self.tagtype[tag] 

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

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

982 if is_ifd: 

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

984 values = self._tags_v2[tag] 

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

986 ifd[ifd_tag] = ifd_value 

987 data = ifd.tobytes(offset) 

988 else: 

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

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

991 

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

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

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

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

996 logger.debug(msg) 

997 

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

999 if is_ifd: 

1000 count = 1 

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

1002 count = len(data) 

1003 else: 

1004 count = len(values) 

1005 # figure out if data fits into the entry 

1006 if len(data) <= fmt_size: 

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

1008 else: 

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

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

1011 

1012 # update strip offset data to point beyond auxiliary data 

1013 if stripoffsets is not None: 

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

1015 if data: 

1016 size, handler = self._load_dispatch[typ] 

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

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

1019 else: 

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

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

1022 

1023 # pass 2: write entries to file 

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

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

1026 result += self._pack( 

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

1028 ) 

1029 

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

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

1032 

1033 # pass 3: write auxiliary data to file 

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

1035 result += data 

1036 if len(data) & 1: 

1037 result += b"\0" 

1038 

1039 return result 

1040 

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

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

1043 fp.write(self._get_ifh()) 

1044 

1045 offset = fp.tell() 

1046 result = self.tobytes(offset) 

1047 fp.write(result) 

1048 return offset + len(result) 

1049 

1050 

1051ImageFileDirectory_v2._load_dispatch = _load_dispatch 

1052ImageFileDirectory_v2._write_dispatch = _write_dispatch 

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

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

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

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

1057del _load_dispatch, _write_dispatch, idx, name 

1058 

1059 

1060# Legacy ImageFileDirectory support. 

1061class ImageFileDirectory_v1(ImageFileDirectory_v2): 

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

1063 

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

1065 

1066 ifd = ImageFileDirectory_v1() 

1067 ifd[key] = 'Some Data' 

1068 ifd.tagtype[key] = TiffTags.ASCII 

1069 print(ifd[key]) 

1070 ('Some Data',) 

1071 

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

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

1074 

1075 Values are returned as a tuple. 

1076 

1077 .. deprecated:: 3.0.0 

1078 """ 

1079 

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

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

1082 self._legacy_api = True 

1083 

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

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

1086 

1087 # defined in ImageFileDirectory_v2 

1088 tagtype: dict[int, int] 

1089 """Dictionary of tag types""" 

1090 

1091 @classmethod 

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

1093 """Returns an 

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

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

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

1097 instance. 

1098 

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

1100 

1101 """ 

1102 

1103 ifd = cls(prefix=original.prefix) 

1104 ifd._tagdata = original._tagdata 

1105 ifd.tagtype = original.tagtype 

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

1107 return ifd 

1108 

1109 def to_v2(self) -> ImageFileDirectory_v2: 

1110 """Returns an 

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

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

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

1114 instance. 

1115 

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

1117 

1118 """ 

1119 

1120 ifd = ImageFileDirectory_v2(prefix=self.prefix) 

1121 ifd._tagdata = dict(self._tagdata) 

1122 ifd.tagtype = dict(self.tagtype) 

1123 ifd._tags_v2 = dict(self._tags_v2) 

1124 return ifd 

1125 

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

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

1128 

1129 def __len__(self) -> int: 

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

1131 

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

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

1134 

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

1136 for legacy_api in (False, True): 

1137 self._setitem(tag, value, legacy_api) 

1138 

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

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

1141 data = self._tagdata[tag] 

1142 typ = self.tagtype[tag] 

1143 size, handler = self._load_dispatch[typ] 

1144 for legacy in (False, True): 

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

1146 val = self._tags_v1[tag] 

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

1148 val = (val,) 

1149 return val 

1150 

1151 

1152# undone -- switch this pointer 

1153ImageFileDirectory = ImageFileDirectory_v1 

1154 

1155 

1156## 

1157# Image plugin for TIFF files. 

1158 

1159 

1160class TiffImageFile(ImageFile.ImageFile): 

1161 format = "TIFF" 

1162 format_description = "Adobe TIFF" 

1163 _close_exclusive_fp_after_loading = False 

1164 

1165 def __init__( 

1166 self, 

1167 fp: StrOrBytesPath | IO[bytes], 

1168 filename: str | bytes | None = None, 

1169 ) -> None: 

1170 self.tag_v2: ImageFileDirectory_v2 

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

1172 

1173 self.tag: ImageFileDirectory_v1 

1174 """ Legacy tag entries """ 

1175 

1176 super().__init__(fp, filename) 

1177 

1178 def _open(self) -> None: 

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

1180 

1181 # Header 

1182 assert self.fp is not None 

1183 ifh = self.fp.read(8) 

1184 if ifh[2] == 43: 

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

1186 

1187 self.tag_v2 = ImageFileDirectory_v2(ifh) 

1188 

1189 # setup frame pointers 

1190 self.__first = self.__next = self.tag_v2.next 

1191 self.__frame = -1 

1192 self._fp = self.fp 

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

1194 self._n_frames: int | None = None 

1195 

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

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

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

1199 

1200 # and load the first frame 

1201 self._seek(0) 

1202 

1203 @property 

1204 def n_frames(self) -> int: 

1205 current_n_frames = self._n_frames 

1206 if current_n_frames is None: 

1207 current = self.tell() 

1208 self._seek(len(self._frame_pos)) 

1209 while self._n_frames is None: 

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

1211 self.seek(current) 

1212 assert self._n_frames is not None 

1213 return self._n_frames 

1214 

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

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

1217 if not self._seek_check(frame): 

1218 return 

1219 self._seek(frame) 

1220 if self._im is not None and ( 

1221 self.im.size != self._tile_size 

1222 or self.im.mode != self.mode 

1223 or self.readonly 

1224 ): 

1225 self._im = None 

1226 

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

1228 if isinstance(self._fp, DeferredError): 

1229 raise self._fp.ex 

1230 self.fp = self._fp 

1231 

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

1233 if not self.__next: 

1234 msg = "no more images in TIFF file" 

1235 raise EOFError(msg) 

1236 logger.debug( 

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

1238 frame, 

1239 self.__frame, 

1240 self.__next, 

1241 self.fp.tell(), 

1242 ) 

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

1244 msg = "Unable to seek to frame" 

1245 raise ValueError(msg) 

1246 self.fp.seek(self.__next) 

1247 self._frame_pos.append(self.__next) 

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

1249 self.tag_v2.load(self.fp) 

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

1251 # This IFD has already been processed 

1252 # Declare this to be the end of the image 

1253 self.__next = 0 

1254 else: 

1255 self.__next = self.tag_v2.next 

1256 if self.__next == 0: 

1257 self._n_frames = frame + 1 

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

1259 self.is_animated = self.__next != 0 

1260 self.__frame += 1 

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

1262 self.tag_v2.load(self.fp) 

1263 if XMP in self.tag_v2: 

1264 xmp = self.tag_v2[XMP] 

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

1266 xmp = xmp[0] 

1267 self.info["xmp"] = xmp 

1268 elif "xmp" in self.info: 

1269 del self.info["xmp"] 

1270 self._reload_exif() 

1271 # fill the legacy tag/ifd entries 

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

1273 self.__frame = frame 

1274 self._setup() 

1275 

1276 def tell(self) -> int: 

1277 """Return the current frame number""" 

1278 return self.__frame 

1279 

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

1281 """ 

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

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

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

1285 

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

1287 """ 

1288 blocks = {} 

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

1290 if val: 

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

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

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

1294 try: 

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

1296 except struct.error: 

1297 break 

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

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

1300 

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

1302 return blocks 

1303 

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

1305 if self.tile and self.use_load_libtiff: 

1306 return self._load_libtiff() 

1307 return super().load() 

1308 

1309 def load_prepare(self) -> None: 

1310 if self._im is None: 

1311 Image._decompression_bomb_check(self._tile_size) 

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

1313 ImageFile.ImageFile.load_prepare(self) 

1314 

1315 def load_end(self) -> None: 

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

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

1318 if not self.is_animated: 

1319 self._close_exclusive_fp_after_loading = True 

1320 

1321 # load IFD data from fp before it is closed 

1322 exif = self.getexif() 

1323 for key in TiffTags.TAGS_V2_GROUPS: 

1324 if key not in exif: 

1325 continue 

1326 exif.get_ifd(key) 

1327 

1328 ImageOps.exif_transpose(self, in_place=True) 

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

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

1331 

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

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

1334 Calls out to libtiff""" 

1335 

1336 Image.Image.load(self) 

1337 

1338 self.load_prepare() 

1339 

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

1341 msg = "Not exactly one tile" 

1342 raise OSError(msg) 

1343 

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

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

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

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

1348 

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

1350 # file descriptor, use that instead of reading 

1351 # into a string in python. 

1352 assert self.fp is not None 

1353 try: 

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

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

1356 # should also eliminate the need for fp.tell 

1357 # in _seek 

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

1359 self.fp.flush() 

1360 except OSError: 

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

1362 # it doesn't use a file descriptor. 

1363 fp = False 

1364 

1365 if fp: 

1366 assert isinstance(args, tuple) 

1367 args_list = list(args) 

1368 args_list[2] = fp 

1369 args = tuple(args_list) 

1370 

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

1372 try: 

1373 decoder.setimage(self.im, extents) 

1374 except ValueError as e: 

1375 msg = "Couldn't set the image" 

1376 raise OSError(msg) from e 

1377 

1378 close_self_fp = self._exclusive_fp and not self.is_animated 

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

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

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

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

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

1384 # underlying string for stringio. 

1385 # 

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

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

1388 # deal with here by reordering. 

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

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

1391 elif fp: 

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

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

1394 if not close_self_fp: 

1395 self.fp.seek(0) 

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

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

1398 # io.BufferedReader and possible others. 

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

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

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

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

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

1404 # 4 bytes, otherwise the trace might error out 

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

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

1407 else: 

1408 # we have something else. 

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

1410 self.fp.seek(0) 

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

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

1413 

1414 self.tile = [] 

1415 self.readonly = 0 

1416 

1417 self.load_end() 

1418 

1419 if close_self_fp: 

1420 self.fp.close() 

1421 self.fp = None # might be shared 

1422 

1423 if err < 0: 

1424 msg = f"decoder error {err}" 

1425 raise OSError(msg) 

1426 

1427 return Image.Image.load(self) 

1428 

1429 def _setup(self) -> None: 

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

1431 

1432 if 0xBC01 in self.tag_v2: 

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

1434 raise OSError(msg) 

1435 

1436 # extract relevant tags 

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

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

1439 

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

1441 # the specification 

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

1443 

1444 # old style jpeg compression images most certainly are YCbCr 

1445 if self._compression == "tiff_jpeg": 

1446 photo = 6 

1447 

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

1449 

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

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

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

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

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

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

1456 

1457 # size 

1458 try: 

1459 xsize = self.tag_v2[IMAGEWIDTH] 

1460 ysize = self.tag_v2[IMAGELENGTH] 

1461 except KeyError as e: 

1462 msg = "Missing dimensions" 

1463 raise TypeError(msg) from e 

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

1465 msg = "Invalid dimensions" 

1466 raise ValueError(msg) 

1467 self._tile_size = xsize, ysize 

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

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

1470 self._size = ysize, xsize 

1471 else: 

1472 self._size = xsize, ysize 

1473 

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

1475 

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

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

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

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

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

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

1482 # for more exotic images. 

1483 sample_format = (sample_format[0],) 

1484 

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

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

1487 samples_per_pixel = self.tag_v2.get( 

1488 SAMPLESPERPIXEL, 

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

1490 ) 

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

1492 bps_count = 3 

1493 elif photo == 5: # CMYK 

1494 bps_count = 4 

1495 else: 

1496 bps_count = 1 

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

1498 # If components are stored separately, 

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

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

1501 samples_per_pixel -= len(extra_tuple) 

1502 extra_tuple = () 

1503 bps_count += len(extra_tuple) 

1504 bps_actual_count = len(bps_tuple) 

1505 

1506 if samples_per_pixel > MAX_SAMPLESPERPIXEL: 

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

1508 logger.error( 

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

1510 ) 

1511 msg = "Invalid value for samples per pixel" 

1512 raise SyntaxError(msg) 

1513 

1514 if samples_per_pixel < bps_actual_count: 

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

1516 # remove the excess. 

1517 bps_tuple = bps_tuple[:samples_per_pixel] 

1518 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: 

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

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

1521 bps_tuple = bps_tuple * samples_per_pixel 

1522 

1523 if len(bps_tuple) != samples_per_pixel: 

1524 msg = "unknown data organization" 

1525 raise SyntaxError(msg) 

1526 

1527 # mode: check photometric interpretation and bits per pixel 

1528 key = ( 

1529 self.tag_v2.prefix, 

1530 photo, 

1531 sample_format, 

1532 fillorder, 

1533 bps_tuple, 

1534 extra_tuple, 

1535 ) 

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

1537 try: 

1538 self._mode, rawmode = OPEN_INFO[key] 

1539 except KeyError as e: 

1540 logger.debug("- unsupported format") 

1541 msg = "unknown pixel mode" 

1542 raise SyntaxError(msg) from e 

1543 

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

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

1546 

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

1548 

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

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

1551 

1552 if xres and yres: 

1553 resunit = self.tag_v2.get(RESOLUTION_UNIT) 

1554 if resunit == 2: # dots per inch 

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

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

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

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

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

1560 # For backward compatibility, 

1561 # we also preserve the old behavior 

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

1563 else: # No absolute unit of measurement 

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

1565 

1566 # build tile descriptors 

1567 x = y = layer = 0 

1568 self.tile = [] 

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

1570 if self.use_load_libtiff: 

1571 # Decoder expects entire file as one tile. 

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

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

1574 # function. 

1575 # 

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

1577 # use the _load_libtiff function. 

1578 

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

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

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

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

1583 if fillorder == 2: 

1584 # Replace fillorder with fillorder=1 

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

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

1587 # this should always work, since all the 

1588 # fillorder==2 modes have a corresponding 

1589 # fillorder=1 mode 

1590 self._mode, rawmode = OPEN_INFO[key] 

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

1592 # unpacked straight into RGB values 

1593 if ( 

1594 photo == 6 

1595 and self._compression == "jpeg" 

1596 and self._planar_configuration == 1 

1597 ): 

1598 rawmode = "RGB" 

1599 # libtiff always returns the bytes in native order. 

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

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

1602 # byte order. 

1603 elif rawmode == "I;16": 

1604 rawmode = "I;16N" 

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

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

1607 

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

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

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

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

1612 

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

1614 # striped image 

1615 if STRIPOFFSETS in self.tag_v2: 

1616 offsets = self.tag_v2[STRIPOFFSETS] 

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

1618 w = xsize 

1619 else: 

1620 # tiled image 

1621 offsets = self.tag_v2[TILEOFFSETS] 

1622 tilewidth = self.tag_v2.get(TILEWIDTH) 

1623 h = self.tag_v2.get(TILELENGTH) 

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

1625 msg = "Invalid tile dimensions" 

1626 raise ValueError(msg) 

1627 w = tilewidth 

1628 

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

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

1631 offsets = offsets[-1:] 

1632 

1633 for offset in offsets: 

1634 if x + w > xsize: 

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

1636 else: 

1637 stride = 0 

1638 

1639 tile_rawmode = rawmode 

1640 if self._planar_configuration == 2: 

1641 # each band on it's own layer 

1642 tile_rawmode = rawmode[layer] 

1643 # adjust stride width accordingly 

1644 stride /= bps_count 

1645 

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

1647 self.tile.append( 

1648 ImageFile._Tile( 

1649 self._compression, 

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

1651 offset, 

1652 args, 

1653 ) 

1654 ) 

1655 x += w 

1656 if x >= xsize: 

1657 x, y = 0, y + h 

1658 if y >= ysize: 

1659 y = 0 

1660 layer += 1 

1661 else: 

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

1663 msg = "unknown data organization" 

1664 raise SyntaxError(msg) 

1665 

1666 # Fix up info. 

1667 if ICCPROFILE in self.tag_v2: 

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

1669 

1670 # fixup palette descriptor 

1671 

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

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

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

1675 

1676 

1677# 

1678# -------------------------------------------------------------------- 

1679# Write TIFF files 

1680 

1681# little endian is default except for image modes with 

1682# explicit big endian byte-order 

1683 

1684SAVE_INFO = { 

1685 # mode => rawmode, byteorder, photometrics, 

1686 # sampleformat, bitspersample, extra 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1703} 

1704 

1705 

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

1707 try: 

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

1709 except KeyError as e: 

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

1711 raise OSError(msg) from e 

1712 

1713 encoderinfo = im.encoderinfo 

1714 encoderconfig = im.encoderconfig 

1715 

1716 ifd = ImageFileDirectory_v2(prefix=prefix) 

1717 if encoderinfo.get("big_tiff"): 

1718 ifd._bigtiff = True 

1719 

1720 try: 

1721 compression = encoderinfo["compression"] 

1722 except KeyError: 

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

1724 if isinstance(compression, int): 

1725 # compression value may be from BMP. Ignore it 

1726 compression = None 

1727 if compression is None: 

1728 compression = "raw" 

1729 elif compression == "tiff_jpeg": 

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

1731 compression = "jpeg" 

1732 elif compression == "tiff_deflate": 

1733 compression = "tiff_adobe_deflate" 

1734 

1735 libtiff = WRITE_LIBTIFF or compression != "raw" 

1736 

1737 # required for color libtiff images 

1738 ifd[PLANAR_CONFIGURATION] = 1 

1739 

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

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

1742 

1743 # write any arbitrary tags passed in as an ImageFileDirectory 

1744 if "tiffinfo" in encoderinfo: 

1745 info = encoderinfo["tiffinfo"] 

1746 elif "exif" in encoderinfo: 

1747 info = encoderinfo["exif"] 

1748 if isinstance(info, bytes): 

1749 exif = Image.Exif() 

1750 exif.load(info) 

1751 info = exif 

1752 else: 

1753 info = {} 

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

1755 if isinstance(info, ImageFileDirectory_v1): 

1756 info = info.to_v2() 

1757 for key in info: 

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

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

1760 else: 

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

1762 try: 

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

1764 except Exception: 

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

1766 

1767 legacy_ifd = {} 

1768 if hasattr(im, "tag"): 

1769 legacy_ifd = im.tag.to_v2() 

1770 

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

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

1773 # If the image used separate component planes, 

1774 # then EXTRASAMPLES should be ignored when saving contiguously 

1775 if SAMPLESPERPIXEL in supplied_tags: 

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

1777 del supplied_tags[EXTRASAMPLES] 

1778 for tag in ( 

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

1780 EXIFIFD, 

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

1782 SAMPLEFORMAT, 

1783 ): 

1784 if tag in supplied_tags: 

1785 del supplied_tags[tag] 

1786 

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

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

1789 if hasattr(im, "tag_v2"): 

1790 # preserve tags from original TIFF image file 

1791 for key in ( 

1792 RESOLUTION_UNIT, 

1793 X_RESOLUTION, 

1794 Y_RESOLUTION, 

1795 IPTC_NAA_CHUNK, 

1796 PHOTOSHOP_CHUNK, 

1797 XMP, 

1798 ): 

1799 if key in im.tag_v2: 

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

1801 TiffTags.BYTE, 

1802 TiffTags.UNDEFINED, 

1803 ): 

1804 del supplied_tags[key] 

1805 else: 

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

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

1808 

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

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

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

1812 if icc: 

1813 ifd[ICCPROFILE] = icc 

1814 

1815 for key, name in [ 

1816 (IMAGEDESCRIPTION, "description"), 

1817 (X_RESOLUTION, "resolution"), 

1818 (Y_RESOLUTION, "resolution"), 

1819 (X_RESOLUTION, "x_resolution"), 

1820 (Y_RESOLUTION, "y_resolution"), 

1821 (RESOLUTION_UNIT, "resolution_unit"), 

1822 (SOFTWARE, "software"), 

1823 (DATE_TIME, "date_time"), 

1824 (ARTIST, "artist"), 

1825 (COPYRIGHT, "copyright"), 

1826 ]: 

1827 if name in encoderinfo: 

1828 ifd[key] = encoderinfo[name] 

1829 

1830 dpi = encoderinfo.get("dpi") 

1831 if dpi: 

1832 ifd[RESOLUTION_UNIT] = 2 

1833 ifd[X_RESOLUTION] = dpi[0] 

1834 ifd[Y_RESOLUTION] = dpi[1] 

1835 

1836 if bits != (1,): 

1837 ifd[BITSPERSAMPLE] = bits 

1838 if len(bits) != 1: 

1839 ifd[SAMPLESPERPIXEL] = len(bits) 

1840 if extra is not None: 

1841 ifd[EXTRASAMPLES] = extra 

1842 if format != 1: 

1843 ifd[SAMPLEFORMAT] = format 

1844 

1845 if PHOTOMETRIC_INTERPRETATION not in ifd: 

1846 ifd[PHOTOMETRIC_INTERPRETATION] = photo 

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

1848 if im.mode == "1": 

1849 inverted_im = im.copy() 

1850 px = inverted_im.load() 

1851 if px is not None: 

1852 for y in range(inverted_im.height): 

1853 for x in range(inverted_im.width): 

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

1855 im = inverted_im 

1856 else: 

1857 im = ImageOps.invert(im) 

1858 

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

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

1861 colormap = [] 

1862 colors = len(lut) // 3 

1863 for i in range(3): 

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

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

1866 ifd[COLORMAP] = colormap 

1867 # data orientation 

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

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

1870 if ROWSPERSTRIP not in ifd: 

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

1872 if libtiff: 

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

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

1875 # JPEG encoder expects multiple of 8 rows 

1876 if compression == "jpeg": 

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

1878 else: 

1879 rows_per_strip = h 

1880 if rows_per_strip == 0: 

1881 rows_per_strip = 1 

1882 ifd[ROWSPERSTRIP] = rows_per_strip 

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

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

1885 if strip_byte_counts >= 2**16: 

1886 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG 

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

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

1889 ) 

1890 ifd[STRIPOFFSETS] = tuple( 

1891 range(0, strip_byte_counts * strips_per_image, strip_byte_counts) 

1892 ) # this is adjusted by IFD writer 

1893 # no compression by default: 

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

1895 

1896 if im.mode == "YCbCr": 

1897 for tag, default_value in { 

1898 YCBCRSUBSAMPLING: (1, 1), 

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

1900 }.items(): 

1901 ifd.setdefault(tag, default_value) 

1902 

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

1904 if libtiff: 

1905 if "quality" in encoderinfo: 

1906 quality = encoderinfo["quality"] 

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

1908 msg = "Invalid quality setting" 

1909 raise ValueError(msg) 

1910 if compression != "jpeg": 

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

1912 raise ValueError(msg) 

1913 ifd[JPEGQUALITY] = quality 

1914 

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

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

1917 _fp = 0 

1918 if hasattr(fp, "fileno"): 

1919 try: 

1920 fp.seek(0) 

1921 _fp = fp.fileno() 

1922 except io.UnsupportedOperation: 

1923 pass 

1924 

1925 # optional types for non core tags 

1926 types = {} 

1927 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library 

1928 # based on the data in the strip. 

1929 # OSUBFILETYPE is deprecated. 

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

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

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

1933 # SUBIFD may also cause a segfault. 

1934 blocklist += [ 

1935 OSUBFILETYPE, 

1936 REFERENCEBLACKWHITE, 

1937 STRIPBYTECOUNTS, 

1938 STRIPOFFSETS, 

1939 TRANSFERFUNCTION, 

1940 SUBIFD, 

1941 ] 

1942 

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

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

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

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

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

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

1949 # Libtiff can only process certain core items without adding 

1950 # them to the custom dictionary. 

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

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

1953 if tag not in TiffTags.LIBTIFF_CORE: 

1954 if tag in TiffTags.TAGS_V2_GROUPS: 

1955 types[tag] = TiffTags.LONG8 

1956 elif tag in ifd.tagtype: 

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

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

1959 isinstance(value, tuple) 

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

1961 ): 

1962 type = TiffTags.lookup(tag).type 

1963 if type: 

1964 types[tag] = type 

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

1966 if isinstance(value, str): 

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

1968 elif isinstance(value, IFDRational): 

1969 atts[tag] = float(value) 

1970 else: 

1971 atts[tag] = value 

1972 

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

1974 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] 

1975 

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

1977 

1978 # libtiff always expects the bytes in native order. 

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

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

1981 # byte order. 

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

1983 rawmode = "I;16N" 

1984 

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

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

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

1988 tags = list(atts.items()) 

1989 tags.sort() 

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

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

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

1993 while True: 

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

1995 if not _fp: 

1996 fp.write(data) 

1997 if errcode: 

1998 break 

1999 if errcode < 0: 

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

2001 raise OSError(msg) 

2002 

2003 else: 

2004 for tag in blocklist: 

2005 del ifd[tag] 

2006 offset = ifd.save(fp) 

2007 

2008 ImageFile._save( 

2009 im, 

2010 fp, 

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

2012 ) 

2013 

2014 # -- helper for multi-page save -- 

2015 if "_debug_multipage" in encoderinfo: 

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

2017 setattr(im, "_debug_multipage", ifd) 

2018 

2019 

2020class AppendingTiffWriter(io.BytesIO): 

2021 fieldSizes = [ 

2022 0, # None 

2023 1, # byte 

2024 1, # ascii 

2025 2, # short 

2026 4, # long 

2027 8, # rational 

2028 1, # sbyte 

2029 1, # undefined 

2030 2, # sshort 

2031 4, # slong 

2032 8, # srational 

2033 4, # float 

2034 8, # double 

2035 4, # ifd 

2036 2, # unicode 

2037 4, # complex 

2038 8, # long8 

2039 ] 

2040 

2041 Tags = { 

2042 273, # StripOffsets 

2043 288, # FreeOffsets 

2044 324, # TileOffsets 

2045 519, # JPEGQTables 

2046 520, # JPEGDCTables 

2047 521, # JPEGACTables 

2048 } 

2049 

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

2051 self.f: IO[bytes] 

2052 if is_path(fn): 

2053 self.name = fn 

2054 self.close_fp = True 

2055 try: 

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

2057 except OSError: 

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

2059 else: 

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

2061 self.close_fp = False 

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

2063 self.setup() 

2064 

2065 def setup(self) -> None: 

2066 # Reset everything. 

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

2068 

2069 self.whereToWriteNewIFDOffset: int | None = None 

2070 self.offsetOfNewPage = 0 

2071 

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

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

2074 if not iimm: 

2075 # empty file - first page 

2076 self.isFirst = True 

2077 return 

2078 

2079 self.isFirst = False 

2080 if iimm not in PREFIXES: 

2081 msg = "Invalid TIFF file header" 

2082 raise RuntimeError(msg) 

2083 

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

2085 

2086 if self._bigtiff: 

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

2088 self.skipIFDs() 

2089 self.goToEnd() 

2090 

2091 def finalize(self) -> None: 

2092 if self.isFirst: 

2093 return 

2094 

2095 # fix offsets 

2096 self.f.seek(self.offsetOfNewPage) 

2097 

2098 iimm = self.f.read(4) 

2099 if not iimm: 

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

2101 return 

2102 

2103 if iimm != self.IIMM: 

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

2105 raise RuntimeError(msg) 

2106 

2107 if self._bigtiff: 

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

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

2110 ifd_offset += self.offsetOfNewPage 

2111 assert self.whereToWriteNewIFDOffset is not None 

2112 self.f.seek(self.whereToWriteNewIFDOffset) 

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

2114 self.f.seek(ifd_offset) 

2115 self.fixIFD() 

2116 

2117 def newFrame(self) -> None: 

2118 # Call this to finish a frame. 

2119 self.finalize() 

2120 self.setup() 

2121 

2122 def __enter__(self) -> AppendingTiffWriter: 

2123 return self 

2124 

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

2126 if self.close_fp: 

2127 self.close() 

2128 

2129 def tell(self) -> int: 

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

2131 

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

2133 """ 

2134 :param offset: Distance to seek. 

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

2136 end or current position. 

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

2138 """ 

2139 if whence == os.SEEK_SET: 

2140 offset += self.offsetOfNewPage 

2141 

2142 self.f.seek(offset, whence) 

2143 return self.tell() 

2144 

2145 def goToEnd(self) -> None: 

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

2147 pos = self.f.tell() 

2148 

2149 # pad to 16 byte boundary 

2150 pad_bytes = 16 - pos % 16 

2151 if 0 < pad_bytes < 16: 

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

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

2154 

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

2156 self.endian = endian 

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

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

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

2160 

2161 def skipIFDs(self) -> None: 

2162 while True: 

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

2164 if ifd_offset == 0: 

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

2166 8 if self._bigtiff else 4 

2167 ) 

2168 break 

2169 

2170 self.f.seek(ifd_offset) 

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

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

2173 

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

2175 return self.f.write(data) 

2176 

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

2178 try: 

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

2180 except KeyError: 

2181 msg = "offset is not supported" 

2182 raise RuntimeError(msg) 

2183 

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

2185 (value,) = struct.unpack( 

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

2187 ) 

2188 return value 

2189 

2190 def readShort(self) -> int: 

2191 return self._read(2) 

2192 

2193 def readLong(self) -> int: 

2194 return self._read(4) 

2195 

2196 @staticmethod 

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

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

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

2200 raise RuntimeError(msg) 

2201 

2202 def _rewriteLast( 

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

2204 ) -> None: 

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

2206 if not new_field_size: 

2207 new_field_size = field_size 

2208 bytes_written = self.f.write( 

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

2210 ) 

2211 self._verify_bytes_written(bytes_written, new_field_size) 

2212 

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

2214 self._rewriteLast(value, 2, 4) 

2215 

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

2217 return self._rewriteLast(value, 2) 

2218 

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

2220 return self._rewriteLast(value, 4) 

2221 

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

2223 bytes_written = self.f.write( 

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

2225 ) 

2226 self._verify_bytes_written(bytes_written, field_size) 

2227 

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

2229 self._write(value, 2) 

2230 

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

2232 self._write(value, 4) 

2233 

2234 def close(self) -> None: 

2235 self.finalize() 

2236 if self.close_fp: 

2237 self.f.close() 

2238 

2239 def fixIFD(self) -> None: 

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

2241 

2242 for i in range(num_tags): 

2243 tag, field_type, count = struct.unpack( 

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

2245 ) 

2246 

2247 field_size = self.fieldSizes[field_type] 

2248 total_size = field_size * count 

2249 fmt_size = 8 if self._bigtiff else 4 

2250 is_local = total_size <= fmt_size 

2251 if not is_local: 

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

2253 self._rewriteLast(offset, fmt_size) 

2254 

2255 if tag in self.Tags: 

2256 cur_pos = self.f.tell() 

2257 

2258 logger.debug( 

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

2260 TiffTags.lookup(tag).name, 

2261 tag, 

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

2263 field_type, 

2264 field_size, 

2265 count, 

2266 ) 

2267 

2268 if is_local: 

2269 self._fixOffsets(count, field_size) 

2270 self.f.seek(cur_pos + fmt_size) 

2271 else: 

2272 self.f.seek(offset) 

2273 self._fixOffsets(count, field_size) 

2274 self.f.seek(cur_pos) 

2275 

2276 elif is_local: 

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

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

2279 

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

2281 for i in range(count): 

2282 offset = self._read(field_size) 

2283 offset += self.offsetOfNewPage 

2284 

2285 new_field_size = 0 

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

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

2288 new_field_size = 8 

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

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

2291 new_field_size = 4 

2292 if new_field_size: 

2293 if count != 1: 

2294 msg = "not implemented" 

2295 raise RuntimeError(msg) # XXX TODO 

2296 

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

2298 # local (not referenced with another offset) 

2299 self._rewriteLast(offset, field_size, new_field_size) 

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

2301 rewind = -new_field_size - 4 - 2 

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

2303 self.writeShort(new_field_size) # rewrite the type 

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

2305 else: 

2306 self._rewriteLast(offset, field_size) 

2307 

2308 def fixOffsets( 

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

2310 ) -> None: 

2311 if isShort: 

2312 field_size = 2 

2313 elif isLong: 

2314 field_size = 4 

2315 else: 

2316 field_size = 0 

2317 return self._fixOffsets(count, field_size) 

2318 

2319 

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

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

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

2323 return _save(im, fp, filename) 

2324 

2325 cur_idx = im.tell() 

2326 try: 

2327 with AppendingTiffWriter(fp) as tf: 

2328 for ims in [im] + append_images: 

2329 encoderinfo = ims._attach_default_encoderinfo(im) 

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

2331 ims.encoderconfig = () 

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

2333 

2334 for idx in range(nfr): 

2335 ims.seek(idx) 

2336 ims.load() 

2337 _save(ims, tf, filename) 

2338 tf.newFrame() 

2339 ims.encoderinfo = encoderinfo 

2340 finally: 

2341 im.seek(cur_idx) 

2342 

2343 

2344# 

2345# -------------------------------------------------------------------- 

2346# Register 

2347 

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

2349Image.register_save(TiffImageFile.format, _save) 

2350Image.register_save_all(TiffImageFile.format, _save_all) 

2351 

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

2353 

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