@ai-ecoverse/py-pillow 12.3.0-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +4 -0
  3. package/lib/python3.14/site-packages/PIL/AvifImagePlugin.py +295 -0
  4. package/lib/python3.14/site-packages/PIL/BdfFontFile.py +124 -0
  5. package/lib/python3.14/site-packages/PIL/BlpImagePlugin.py +498 -0
  6. package/lib/python3.14/site-packages/PIL/BmpImagePlugin.py +514 -0
  7. package/lib/python3.14/site-packages/PIL/BufrStubImagePlugin.py +72 -0
  8. package/lib/python3.14/site-packages/PIL/ContainerIO.py +173 -0
  9. package/lib/python3.14/site-packages/PIL/CurImagePlugin.py +103 -0
  10. package/lib/python3.14/site-packages/PIL/DcxImagePlugin.py +84 -0
  11. package/lib/python3.14/site-packages/PIL/DdsImagePlugin.py +632 -0
  12. package/lib/python3.14/site-packages/PIL/EpsImagePlugin.py +484 -0
  13. package/lib/python3.14/site-packages/PIL/ExifTags.py +384 -0
  14. package/lib/python3.14/site-packages/PIL/FitsImagePlugin.py +153 -0
  15. package/lib/python3.14/site-packages/PIL/FliImagePlugin.py +184 -0
  16. package/lib/python3.14/site-packages/PIL/FontFile.py +160 -0
  17. package/lib/python3.14/site-packages/PIL/FpxImagePlugin.py +262 -0
  18. package/lib/python3.14/site-packages/PIL/FtexImagePlugin.py +115 -0
  19. package/lib/python3.14/site-packages/PIL/GbrImagePlugin.py +103 -0
  20. package/lib/python3.14/site-packages/PIL/GdImageFile.py +104 -0
  21. package/lib/python3.14/site-packages/PIL/GifImagePlugin.py +1223 -0
  22. package/lib/python3.14/site-packages/PIL/GimpGradientFile.py +154 -0
  23. package/lib/python3.14/site-packages/PIL/GimpPaletteFile.py +75 -0
  24. package/lib/python3.14/site-packages/PIL/GribStubImagePlugin.py +72 -0
  25. package/lib/python3.14/site-packages/PIL/Hdf5StubImagePlugin.py +72 -0
  26. package/lib/python3.14/site-packages/PIL/IcnsImagePlugin.py +401 -0
  27. package/lib/python3.14/site-packages/PIL/IcoImagePlugin.py +396 -0
  28. package/lib/python3.14/site-packages/PIL/ImImagePlugin.py +390 -0
  29. package/lib/python3.14/site-packages/PIL/Image.py +4385 -0
  30. package/lib/python3.14/site-packages/PIL/ImageChops.py +311 -0
  31. package/lib/python3.14/site-packages/PIL/ImageCms.py +1078 -0
  32. package/lib/python3.14/site-packages/PIL/ImageColor.py +320 -0
  33. package/lib/python3.14/site-packages/PIL/ImageDraw.py +1002 -0
  34. package/lib/python3.14/site-packages/PIL/ImageDraw2.py +244 -0
  35. package/lib/python3.14/site-packages/PIL/ImageEnhance.py +113 -0
  36. package/lib/python3.14/site-packages/PIL/ImageFile.py +938 -0
  37. package/lib/python3.14/site-packages/PIL/ImageFilter.py +617 -0
  38. package/lib/python3.14/site-packages/PIL/ImageFont.py +1330 -0
  39. package/lib/python3.14/site-packages/PIL/ImageGrab.py +234 -0
  40. package/lib/python3.14/site-packages/PIL/ImageMath.py +314 -0
  41. package/lib/python3.14/site-packages/PIL/ImageMode.py +85 -0
  42. package/lib/python3.14/site-packages/PIL/ImageMorph.py +317 -0
  43. package/lib/python3.14/site-packages/PIL/ImageOps.py +749 -0
  44. package/lib/python3.14/site-packages/PIL/ImagePalette.py +290 -0
  45. package/lib/python3.14/site-packages/PIL/ImagePath.py +20 -0
  46. package/lib/python3.14/site-packages/PIL/ImageQt.py +219 -0
  47. package/lib/python3.14/site-packages/PIL/ImageSequence.py +88 -0
  48. package/lib/python3.14/site-packages/PIL/ImageShow.py +367 -0
  49. package/lib/python3.14/site-packages/PIL/ImageStat.py +167 -0
  50. package/lib/python3.14/site-packages/PIL/ImageText.py +509 -0
  51. package/lib/python3.14/site-packages/PIL/ImageTk.py +266 -0
  52. package/lib/python3.14/site-packages/PIL/ImageTransform.py +136 -0
  53. package/lib/python3.14/site-packages/PIL/ImageWin.py +247 -0
  54. package/lib/python3.14/site-packages/PIL/ImtImagePlugin.py +103 -0
  55. package/lib/python3.14/site-packages/PIL/IptcImagePlugin.py +226 -0
  56. package/lib/python3.14/site-packages/PIL/Jpeg2KImagePlugin.py +466 -0
  57. package/lib/python3.14/site-packages/PIL/JpegImagePlugin.py +889 -0
  58. package/lib/python3.14/site-packages/PIL/JpegPresets.py +242 -0
  59. package/lib/python3.14/site-packages/PIL/McIdasImagePlugin.py +78 -0
  60. package/lib/python3.14/site-packages/PIL/MicImagePlugin.py +103 -0
  61. package/lib/python3.14/site-packages/PIL/MpegImagePlugin.py +84 -0
  62. package/lib/python3.14/site-packages/PIL/MpoImagePlugin.py +203 -0
  63. package/lib/python3.14/site-packages/PIL/MspImagePlugin.py +200 -0
  64. package/lib/python3.14/site-packages/PIL/PSDraw.py +238 -0
  65. package/lib/python3.14/site-packages/PIL/PaletteFile.py +54 -0
  66. package/lib/python3.14/site-packages/PIL/PalmImagePlugin.py +217 -0
  67. package/lib/python3.14/site-packages/PIL/PcdImagePlugin.py +68 -0
  68. package/lib/python3.14/site-packages/PIL/PcfFontFile.py +256 -0
  69. package/lib/python3.14/site-packages/PIL/PcxImagePlugin.py +232 -0
  70. package/lib/python3.14/site-packages/PIL/PdfImagePlugin.py +315 -0
  71. package/lib/python3.14/site-packages/PIL/PdfParser.py +1092 -0
  72. package/lib/python3.14/site-packages/PIL/PixarImagePlugin.py +72 -0
  73. package/lib/python3.14/site-packages/PIL/PngImagePlugin.py +1578 -0
  74. package/lib/python3.14/site-packages/PIL/PpmImagePlugin.py +375 -0
  75. package/lib/python3.14/site-packages/PIL/PsdImagePlugin.py +339 -0
  76. package/lib/python3.14/site-packages/PIL/QoiImagePlugin.py +235 -0
  77. package/lib/python3.14/site-packages/PIL/SgiImagePlugin.py +231 -0
  78. package/lib/python3.14/site-packages/PIL/SpiderImagePlugin.py +332 -0
  79. package/lib/python3.14/site-packages/PIL/SunImagePlugin.py +145 -0
  80. package/lib/python3.14/site-packages/PIL/TarIO.py +61 -0
  81. package/lib/python3.14/site-packages/PIL/TgaImagePlugin.py +284 -0
  82. package/lib/python3.14/site-packages/PIL/TiffImagePlugin.py +2354 -0
  83. package/lib/python3.14/site-packages/PIL/TiffTags.py +566 -0
  84. package/lib/python3.14/site-packages/PIL/WalImageFile.py +129 -0
  85. package/lib/python3.14/site-packages/PIL/WebPImagePlugin.py +322 -0
  86. package/lib/python3.14/site-packages/PIL/WmfImagePlugin.py +183 -0
  87. package/lib/python3.14/site-packages/PIL/XVThumbImagePlugin.py +83 -0
  88. package/lib/python3.14/site-packages/PIL/XbmImagePlugin.py +98 -0
  89. package/lib/python3.14/site-packages/PIL/XpmImagePlugin.py +157 -0
  90. package/lib/python3.14/site-packages/PIL/__init__.py +87 -0
  91. package/lib/python3.14/site-packages/PIL/__main__.py +7 -0
  92. package/lib/python3.14/site-packages/PIL/__pycache__/AvifImagePlugin.cpython-314.pyc +0 -0
  93. package/lib/python3.14/site-packages/PIL/__pycache__/BdfFontFile.cpython-314.pyc +0 -0
  94. package/lib/python3.14/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-314.pyc +0 -0
  95. package/lib/python3.14/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-314.pyc +0 -0
  96. package/lib/python3.14/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-314.pyc +0 -0
  97. package/lib/python3.14/site-packages/PIL/__pycache__/ContainerIO.cpython-314.pyc +0 -0
  98. package/lib/python3.14/site-packages/PIL/__pycache__/CurImagePlugin.cpython-314.pyc +0 -0
  99. package/lib/python3.14/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-314.pyc +0 -0
  100. package/lib/python3.14/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-314.pyc +0 -0
  101. package/lib/python3.14/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-314.pyc +0 -0
  102. package/lib/python3.14/site-packages/PIL/__pycache__/ExifTags.cpython-314.pyc +0 -0
  103. package/lib/python3.14/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-314.pyc +0 -0
  104. package/lib/python3.14/site-packages/PIL/__pycache__/FliImagePlugin.cpython-314.pyc +0 -0
  105. package/lib/python3.14/site-packages/PIL/__pycache__/FontFile.cpython-314.pyc +0 -0
  106. package/lib/python3.14/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-314.pyc +0 -0
  107. package/lib/python3.14/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-314.pyc +0 -0
  108. package/lib/python3.14/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-314.pyc +0 -0
  109. package/lib/python3.14/site-packages/PIL/__pycache__/GdImageFile.cpython-314.pyc +0 -0
  110. package/lib/python3.14/site-packages/PIL/__pycache__/GifImagePlugin.cpython-314.pyc +0 -0
  111. package/lib/python3.14/site-packages/PIL/__pycache__/GimpGradientFile.cpython-314.pyc +0 -0
  112. package/lib/python3.14/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-314.pyc +0 -0
  113. package/lib/python3.14/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-314.pyc +0 -0
  114. package/lib/python3.14/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-314.pyc +0 -0
  115. package/lib/python3.14/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-314.pyc +0 -0
  116. package/lib/python3.14/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-314.pyc +0 -0
  117. package/lib/python3.14/site-packages/PIL/__pycache__/ImImagePlugin.cpython-314.pyc +0 -0
  118. package/lib/python3.14/site-packages/PIL/__pycache__/Image.cpython-314.pyc +0 -0
  119. package/lib/python3.14/site-packages/PIL/__pycache__/ImageChops.cpython-314.pyc +0 -0
  120. package/lib/python3.14/site-packages/PIL/__pycache__/ImageCms.cpython-314.pyc +0 -0
  121. package/lib/python3.14/site-packages/PIL/__pycache__/ImageColor.cpython-314.pyc +0 -0
  122. package/lib/python3.14/site-packages/PIL/__pycache__/ImageDraw.cpython-314.pyc +0 -0
  123. package/lib/python3.14/site-packages/PIL/__pycache__/ImageDraw2.cpython-314.pyc +0 -0
  124. package/lib/python3.14/site-packages/PIL/__pycache__/ImageEnhance.cpython-314.pyc +0 -0
  125. package/lib/python3.14/site-packages/PIL/__pycache__/ImageFile.cpython-314.pyc +0 -0
  126. package/lib/python3.14/site-packages/PIL/__pycache__/ImageFilter.cpython-314.pyc +0 -0
  127. package/lib/python3.14/site-packages/PIL/__pycache__/ImageFont.cpython-314.pyc +0 -0
  128. package/lib/python3.14/site-packages/PIL/__pycache__/ImageGrab.cpython-314.pyc +0 -0
  129. package/lib/python3.14/site-packages/PIL/__pycache__/ImageMath.cpython-314.pyc +0 -0
  130. package/lib/python3.14/site-packages/PIL/__pycache__/ImageMode.cpython-314.pyc +0 -0
  131. package/lib/python3.14/site-packages/PIL/__pycache__/ImageMorph.cpython-314.pyc +0 -0
  132. package/lib/python3.14/site-packages/PIL/__pycache__/ImageOps.cpython-314.pyc +0 -0
  133. package/lib/python3.14/site-packages/PIL/__pycache__/ImagePalette.cpython-314.pyc +0 -0
  134. package/lib/python3.14/site-packages/PIL/__pycache__/ImagePath.cpython-314.pyc +0 -0
  135. package/lib/python3.14/site-packages/PIL/__pycache__/ImageQt.cpython-314.pyc +0 -0
  136. package/lib/python3.14/site-packages/PIL/__pycache__/ImageSequence.cpython-314.pyc +0 -0
  137. package/lib/python3.14/site-packages/PIL/__pycache__/ImageShow.cpython-314.pyc +0 -0
  138. package/lib/python3.14/site-packages/PIL/__pycache__/ImageStat.cpython-314.pyc +0 -0
  139. package/lib/python3.14/site-packages/PIL/__pycache__/ImageText.cpython-314.pyc +0 -0
  140. package/lib/python3.14/site-packages/PIL/__pycache__/ImageTk.cpython-314.pyc +0 -0
  141. package/lib/python3.14/site-packages/PIL/__pycache__/ImageTransform.cpython-314.pyc +0 -0
  142. package/lib/python3.14/site-packages/PIL/__pycache__/ImageWin.cpython-314.pyc +0 -0
  143. package/lib/python3.14/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-314.pyc +0 -0
  144. package/lib/python3.14/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-314.pyc +0 -0
  145. package/lib/python3.14/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-314.pyc +0 -0
  146. package/lib/python3.14/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-314.pyc +0 -0
  147. package/lib/python3.14/site-packages/PIL/__pycache__/JpegPresets.cpython-314.pyc +0 -0
  148. package/lib/python3.14/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-314.pyc +0 -0
  149. package/lib/python3.14/site-packages/PIL/__pycache__/MicImagePlugin.cpython-314.pyc +0 -0
  150. package/lib/python3.14/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-314.pyc +0 -0
  151. package/lib/python3.14/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-314.pyc +0 -0
  152. package/lib/python3.14/site-packages/PIL/__pycache__/MspImagePlugin.cpython-314.pyc +0 -0
  153. package/lib/python3.14/site-packages/PIL/__pycache__/PSDraw.cpython-314.pyc +0 -0
  154. package/lib/python3.14/site-packages/PIL/__pycache__/PaletteFile.cpython-314.pyc +0 -0
  155. package/lib/python3.14/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-314.pyc +0 -0
  156. package/lib/python3.14/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-314.pyc +0 -0
  157. package/lib/python3.14/site-packages/PIL/__pycache__/PcfFontFile.cpython-314.pyc +0 -0
  158. package/lib/python3.14/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-314.pyc +0 -0
  159. package/lib/python3.14/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-314.pyc +0 -0
  160. package/lib/python3.14/site-packages/PIL/__pycache__/PdfParser.cpython-314.pyc +0 -0
  161. package/lib/python3.14/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-314.pyc +0 -0
  162. package/lib/python3.14/site-packages/PIL/__pycache__/PngImagePlugin.cpython-314.pyc +0 -0
  163. package/lib/python3.14/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-314.pyc +0 -0
  164. package/lib/python3.14/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-314.pyc +0 -0
  165. package/lib/python3.14/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-314.pyc +0 -0
  166. package/lib/python3.14/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-314.pyc +0 -0
  167. package/lib/python3.14/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-314.pyc +0 -0
  168. package/lib/python3.14/site-packages/PIL/__pycache__/SunImagePlugin.cpython-314.pyc +0 -0
  169. package/lib/python3.14/site-packages/PIL/__pycache__/TarIO.cpython-314.pyc +0 -0
  170. package/lib/python3.14/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-314.pyc +0 -0
  171. package/lib/python3.14/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-314.pyc +0 -0
  172. package/lib/python3.14/site-packages/PIL/__pycache__/TiffTags.cpython-314.pyc +0 -0
  173. package/lib/python3.14/site-packages/PIL/__pycache__/WalImageFile.cpython-314.pyc +0 -0
  174. package/lib/python3.14/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-314.pyc +0 -0
  175. package/lib/python3.14/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-314.pyc +0 -0
  176. package/lib/python3.14/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-314.pyc +0 -0
  177. package/lib/python3.14/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-314.pyc +0 -0
  178. package/lib/python3.14/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-314.pyc +0 -0
  179. package/lib/python3.14/site-packages/PIL/__pycache__/__init__.cpython-314.pyc +0 -0
  180. package/lib/python3.14/site-packages/PIL/__pycache__/__main__.cpython-314.pyc +0 -0
  181. package/lib/python3.14/site-packages/PIL/__pycache__/_binary.cpython-314.pyc +0 -0
  182. package/lib/python3.14/site-packages/PIL/__pycache__/_deprecate.cpython-314.pyc +0 -0
  183. package/lib/python3.14/site-packages/PIL/__pycache__/_tkinter_finder.cpython-314.pyc +0 -0
  184. package/lib/python3.14/site-packages/PIL/__pycache__/_typing.cpython-314.pyc +0 -0
  185. package/lib/python3.14/site-packages/PIL/__pycache__/_util.cpython-314.pyc +0 -0
  186. package/lib/python3.14/site-packages/PIL/__pycache__/_version.cpython-314.pyc +0 -0
  187. package/lib/python3.14/site-packages/PIL/__pycache__/features.cpython-314.pyc +0 -0
  188. package/lib/python3.14/site-packages/PIL/__pycache__/report.cpython-314.pyc +0 -0
  189. package/lib/python3.14/site-packages/PIL/_avif.pyi +3 -0
  190. package/lib/python3.14/site-packages/PIL/_binary.py +113 -0
  191. package/lib/python3.14/site-packages/PIL/_deprecate.py +72 -0
  192. package/lib/python3.14/site-packages/PIL/_imaging.cpython-314-wasm32-wasix.so +0 -0
  193. package/lib/python3.14/site-packages/PIL/_imaging.pyi +31 -0
  194. package/lib/python3.14/site-packages/PIL/_imagingcms.pyi +143 -0
  195. package/lib/python3.14/site-packages/PIL/_imagingft.cpython-314-wasm32-wasix.so +0 -0
  196. package/lib/python3.14/site-packages/PIL/_imagingft.pyi +70 -0
  197. package/lib/python3.14/site-packages/PIL/_imagingmath.cpython-314-wasm32-wasix.so +0 -0
  198. package/lib/python3.14/site-packages/PIL/_imagingmath.pyi +3 -0
  199. package/lib/python3.14/site-packages/PIL/_imagingmorph.cpython-314-wasm32-wasix.so +0 -0
  200. package/lib/python3.14/site-packages/PIL/_imagingmorph.pyi +3 -0
  201. package/lib/python3.14/site-packages/PIL/_imagingtk.cpython-314-wasm32-wasix.so +0 -0
  202. package/lib/python3.14/site-packages/PIL/_imagingtk.pyi +3 -0
  203. package/lib/python3.14/site-packages/PIL/_tkinter_finder.py +20 -0
  204. package/lib/python3.14/site-packages/PIL/_typing.py +45 -0
  205. package/lib/python3.14/site-packages/PIL/_util.py +29 -0
  206. package/lib/python3.14/site-packages/PIL/_version.py +4 -0
  207. package/lib/python3.14/site-packages/PIL/_webp.pyi +3 -0
  208. package/lib/python3.14/site-packages/PIL/features.py +343 -0
  209. package/lib/python3.14/site-packages/PIL/py.typed +0 -0
  210. package/lib/python3.14/site-packages/PIL/report.py +5 -0
  211. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/METADATA +180 -0
  212. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/RECORD +117 -0
  213. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/WHEEL +5 -0
  214. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/licenses/LICENSE +30 -0
  215. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/top_level.txt +1 -0
  216. package/lib/python3.14/site-packages/pillow-12.3.0.dist-info/zip-safe +1 -0
  217. package/package.json +46 -0
@@ -0,0 +1,2354 @@
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
+ #
41
+ from __future__ import annotations
42
+
43
+ import io
44
+ import itertools
45
+ import logging
46
+ import math
47
+ import os
48
+ import struct
49
+ import warnings
50
+ from collections.abc import Callable, MutableMapping
51
+ from fractions import Fraction
52
+ from numbers import Number, Rational
53
+ from typing import IO, Any, cast
54
+
55
+ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
56
+ from ._binary import i16be as i16
57
+ from ._binary import i32be as i32
58
+ from ._binary import o8
59
+ from ._util import DeferredError, is_path
60
+ from .TiffTags import TYPES
61
+
62
+ TYPE_CHECKING = False
63
+ if TYPE_CHECKING:
64
+ from collections.abc import Iterator
65
+ from typing import NoReturn
66
+
67
+ from ._typing import Buffer, IntegralLike, StrOrBytesPath
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+ # Set these to true to force use of libtiff for reading or writing.
72
+ READ_LIBTIFF = False
73
+ WRITE_LIBTIFF = False
74
+ STRIP_SIZE = 65536
75
+
76
+ II = b"II" # little-endian (Intel style)
77
+ MM = 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
84
+ OSUBFILETYPE = 255
85
+ IMAGEWIDTH = 256
86
+ IMAGELENGTH = 257
87
+ BITSPERSAMPLE = 258
88
+ COMPRESSION = 259
89
+ PHOTOMETRIC_INTERPRETATION = 262
90
+ FILLORDER = 266
91
+ IMAGEDESCRIPTION = 270
92
+ STRIPOFFSETS = 273
93
+ SAMPLESPERPIXEL = 277
94
+ ROWSPERSTRIP = 278
95
+ STRIPBYTECOUNTS = 279
96
+ X_RESOLUTION = 282
97
+ Y_RESOLUTION = 283
98
+ PLANAR_CONFIGURATION = 284
99
+ RESOLUTION_UNIT = 296
100
+ TRANSFERFUNCTION = 301
101
+ SOFTWARE = 305
102
+ DATE_TIME = 306
103
+ ARTIST = 315
104
+ PREDICTOR = 317
105
+ COLORMAP = 320
106
+ TILEWIDTH = 322
107
+ TILELENGTH = 323
108
+ TILEOFFSETS = 324
109
+ TILEBYTECOUNTS = 325
110
+ SUBIFD = 330
111
+ EXTRASAMPLES = 338
112
+ SAMPLEFORMAT = 339
113
+ JPEGTABLES = 347
114
+ YCBCRSUBSAMPLING = 530
115
+ REFERENCEBLACKWHITE = 532
116
+ COPYRIGHT = 33432
117
+ IPTC_NAA_CHUNK = 33723 # newsphoto properties
118
+ PHOTOSHOP_CHUNK = 34377 # photoshop properties
119
+ ICCPROFILE = 34675
120
+ EXIFIFD = 34665
121
+ XMP = 700
122
+ JPEGQUALITY = 65537 # pseudo-tag by libtiff
123
+
124
+ # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
125
+ IMAGEJ_META_DATA_BYTE_COUNTS = 50838
126
+ IMAGEJ_META_DATA = 50839
127
+
128
+ COMPRESSION_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
+
149
+ COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
150
+
151
+ OPEN_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
+
278
+ MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
279
+
280
+ PREFIXES = [
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
+
290
+ def _accept(prefix: bytes) -> bool:
291
+ return prefix.startswith(tuple(PREFIXES))
292
+
293
+
294
+ def _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
+
302
+ def _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
+
325
+ def _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
+
334
+ class 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
+
476
+ def _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
+
488
+ def _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
+
496
+ def _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
+
514
+ if TYPE_CHECKING:
515
+ _IFDv2Base = MutableMapping[int, Any]
516
+ else:
517
+ _IFDv2Base = MutableMapping
518
+
519
+
520
+ class 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
+
1051
+ ImageFileDirectory_v2._load_dispatch = _load_dispatch
1052
+ ImageFileDirectory_v2._write_dispatch = _write_dispatch
1053
+ for 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])
1057
+ del _load_dispatch, _write_dispatch, idx, name
1058
+
1059
+
1060
+ # Legacy ImageFileDirectory support.
1061
+ class 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
1153
+ ImageFileDirectory = ImageFileDirectory_v1
1154
+
1155
+
1156
+ ##
1157
+ # Image plugin for TIFF files.
1158
+
1159
+
1160
+ class 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
+
1684
+ SAVE_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
+
1706
+ def _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
+
2020
+ class 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
+
2320
+ def _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
+
2348
+ Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
2349
+ Image.register_save(TiffImageFile.format, _save)
2350
+ Image.register_save_all(TiffImageFile.format, _save_all)
2351
+
2352
+ Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
2353
+
2354
+ Image.register_mime(TiffImageFile.format, "image/tiff")