12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165 |
- #
- # The Python Imaging Library.
- # $Id$
- #
- # TIFF file handling
- #
- # TIFF is a flexible, if somewhat aged, image file format originally
- # defined by Aldus. Although TIFF supports a wide variety of pixel
- # layouts and compression methods, the name doesn't really stand for
- # "thousands of incompatible file formats," it just feels that way.
- #
- # To read TIFF data from a stream, the stream must be seekable. For
- # progressive decoding, make sure to use TIFF files where the tag
- # directory is placed first in the file.
- #
- # History:
- # 1995-09-01 fl Created
- # 1996-05-04 fl Handle JPEGTABLES tag
- # 1996-05-18 fl Fixed COLORMAP support
- # 1997-01-05 fl Fixed PREDICTOR support
- # 1997-08-27 fl Added support for rational tags (from Perry Stoll)
- # 1998-01-10 fl Fixed seek/tell (from Jan Blom)
- # 1998-07-15 fl Use private names for internal variables
- # 1999-06-13 fl Rewritten for PIL 1.0 (1.0)
- # 2000-10-11 fl Additional fixes for Python 2.0 (1.1)
- # 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2)
- # 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3)
- # 2001-12-18 fl Added workaround for broken Matrox library
- # 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart)
- # 2003-05-19 fl Check FILLORDER tag
- # 2003-09-26 fl Added RGBa support
- # 2004-02-24 fl Added DPI support; fixed rational write support
- # 2005-02-07 fl Added workaround for broken Corel Draw 10 files
- # 2006-01-09 fl Added support for float/double tags (from Russell Nelson)
- #
- # Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved.
- # Copyright (c) 1995-1997 by Fredrik Lundh
- #
- # See the README file for information on usage and redistribution.
- #
- import io
- import itertools
- import logging
- import math
- import os
- import struct
- import warnings
- from collections.abc import MutableMapping
- from fractions import Fraction
- from numbers import Number, Rational
-
- from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
- from ._binary import i16be as i16
- from ._binary import i32be as i32
- from ._binary import o8
- from .TiffTags import TYPES
-
- logger = logging.getLogger(__name__)
-
- # Set these to true to force use of libtiff for reading or writing.
- READ_LIBTIFF = False
- WRITE_LIBTIFF = False
- IFD_LEGACY_API = True
- STRIP_SIZE = 65536
-
- II = b"II" # little-endian (Intel style)
- MM = b"MM" # big-endian (Motorola style)
-
- #
- # --------------------------------------------------------------------
- # Read TIFF files
-
- # a few tag names, just to make the code below a bit more readable
- IMAGEWIDTH = 256
- IMAGELENGTH = 257
- BITSPERSAMPLE = 258
- COMPRESSION = 259
- PHOTOMETRIC_INTERPRETATION = 262
- FILLORDER = 266
- IMAGEDESCRIPTION = 270
- STRIPOFFSETS = 273
- SAMPLESPERPIXEL = 277
- ROWSPERSTRIP = 278
- STRIPBYTECOUNTS = 279
- X_RESOLUTION = 282
- Y_RESOLUTION = 283
- PLANAR_CONFIGURATION = 284
- RESOLUTION_UNIT = 296
- TRANSFERFUNCTION = 301
- SOFTWARE = 305
- DATE_TIME = 306
- ARTIST = 315
- PREDICTOR = 317
- COLORMAP = 320
- TILEWIDTH = 322
- TILELENGTH = 323
- TILEOFFSETS = 324
- TILEBYTECOUNTS = 325
- SUBIFD = 330
- EXTRASAMPLES = 338
- SAMPLEFORMAT = 339
- JPEGTABLES = 347
- YCBCRSUBSAMPLING = 530
- REFERENCEBLACKWHITE = 532
- COPYRIGHT = 33432
- IPTC_NAA_CHUNK = 33723 # newsphoto properties
- PHOTOSHOP_CHUNK = 34377 # photoshop properties
- ICCPROFILE = 34675
- EXIFIFD = 34665
- XMP = 700
- JPEGQUALITY = 65537 # pseudo-tag by libtiff
-
- # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
- IMAGEJ_META_DATA_BYTE_COUNTS = 50838
- IMAGEJ_META_DATA = 50839
-
- COMPRESSION_INFO = {
- # Compression => pil compression name
- 1: "raw",
- 2: "tiff_ccitt",
- 3: "group3",
- 4: "group4",
- 5: "tiff_lzw",
- 6: "tiff_jpeg", # obsolete
- 7: "jpeg",
- 8: "tiff_adobe_deflate",
- 32771: "tiff_raw_16", # 16-bit padding
- 32773: "packbits",
- 32809: "tiff_thunderscan",
- 32946: "tiff_deflate",
- 34676: "tiff_sgilog",
- 34677: "tiff_sgilog24",
- 34925: "lzma",
- 50000: "zstd",
- 50001: "webp",
- }
-
- COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
-
- OPEN_INFO = {
- # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
- # ExtraSamples) => mode, rawmode
- (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
- (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
- (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
- (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
- (II, 1, (1,), 1, (1,), ()): ("1", "1"),
- (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
- (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
- (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
- (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
- (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
- (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
- (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
- (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
- (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
- (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
- (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
- (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
- (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
- (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
- (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
- (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
- (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
- (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
- (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
- (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
- (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
- (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
- (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
- (II, 1, (1,), 1, (8,), ()): ("L", "L"),
- (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
- (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
- (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
- (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
- (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
- (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
- (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
- (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
- (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
- (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
- (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
- (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
- (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
- (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
- (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
- (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
- (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
- (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
- (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
- (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
- (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
- (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
- (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
- (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
- (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
- (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
- (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
- (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
- (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
- (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
- (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
- (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
- (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
- (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
- (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
- (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
- (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
- (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
- (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
- (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
- (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
- (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
- (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
- (II, 3, (1,), 1, (8,), ()): ("P", "P"),
- (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
- (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
- (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
- (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
- (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
- (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
- (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
- (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
- (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
- (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
- (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
- (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
- # JPEG compressed images handled by LibTiff and auto-converted to RGBX
- # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
- (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
- (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
- (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
- (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
- }
-
- MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
-
- PREFIXES = [
- b"MM\x00\x2A", # Valid TIFF header with big-endian byte order
- b"II\x2A\x00", # Valid TIFF header with little-endian byte order
- b"MM\x2A\x00", # Invalid TIFF header, assume big-endian
- b"II\x00\x2A", # Invalid TIFF header, assume little-endian
- b"MM\x00\x2B", # BigTIFF with big-endian byte order
- b"II\x2B\x00", # BigTIFF with little-endian byte order
- ]
-
-
- def _accept(prefix):
- return prefix[:4] in PREFIXES
-
-
- def _limit_rational(val, max_val):
- inv = abs(val) > 1
- n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
- return n_d[::-1] if inv else n_d
-
-
- def _limit_signed_rational(val, max_val, min_val):
- frac = Fraction(val)
- n_d = frac.numerator, frac.denominator
-
- if min(n_d) < min_val:
- n_d = _limit_rational(val, abs(min_val))
-
- if max(n_d) > max_val:
- val = Fraction(*n_d)
- n_d = _limit_rational(val, max_val)
-
- return n_d
-
-
- ##
- # Wrapper for TIFF IFDs.
-
- _load_dispatch = {}
- _write_dispatch = {}
-
-
- class IFDRational(Rational):
- """Implements a rational class where 0/0 is a legal value to match
- the in the wild use of exif rationals.
-
- e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
- """
-
- """ If the denominator is 0, store this as a float('nan'), otherwise store
- as a fractions.Fraction(). Delegate as appropriate
-
- """
-
- __slots__ = ("_numerator", "_denominator", "_val")
-
- def __init__(self, value, denominator=1):
- """
- :param value: either an integer numerator, a
- float/rational/other number, or an IFDRational
- :param denominator: Optional integer denominator
- """
- if isinstance(value, IFDRational):
- self._numerator = value.numerator
- self._denominator = value.denominator
- self._val = value._val
- return
-
- if isinstance(value, Fraction):
- self._numerator = value.numerator
- self._denominator = value.denominator
- else:
- self._numerator = value
- self._denominator = denominator
-
- if denominator == 0:
- self._val = float("nan")
- elif denominator == 1:
- self._val = Fraction(value)
- else:
- self._val = Fraction(value, denominator)
-
- @property
- def numerator(self):
- return self._numerator
-
- @property
- def denominator(self):
- return self._denominator
-
- def limit_rational(self, max_denominator):
- """
-
- :param max_denominator: Integer, the maximum denominator value
- :returns: Tuple of (numerator, denominator)
- """
-
- if self.denominator == 0:
- return self.numerator, self.denominator
-
- f = self._val.limit_denominator(max_denominator)
- return f.numerator, f.denominator
-
- def __repr__(self):
- return str(float(self._val))
-
- def __hash__(self):
- return self._val.__hash__()
-
- def __eq__(self, other):
- val = self._val
- if isinstance(other, IFDRational):
- other = other._val
- if isinstance(other, float):
- val = float(val)
- return val == other
-
- def __getstate__(self):
- return [self._val, self._numerator, self._denominator]
-
- def __setstate__(self, state):
- IFDRational.__init__(self, 0)
- _val, _numerator, _denominator = state
- self._val = _val
- self._numerator = _numerator
- self._denominator = _denominator
-
- def _delegate(op):
- def delegate(self, *args):
- return getattr(self._val, op)(*args)
-
- return delegate
-
- """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
- 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
- 'mod','rmod', 'pow','rpow', 'pos', 'neg',
- 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
- 'ceil', 'floor', 'round']
- print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
- """
-
- __add__ = _delegate("__add__")
- __radd__ = _delegate("__radd__")
- __sub__ = _delegate("__sub__")
- __rsub__ = _delegate("__rsub__")
- __mul__ = _delegate("__mul__")
- __rmul__ = _delegate("__rmul__")
- __truediv__ = _delegate("__truediv__")
- __rtruediv__ = _delegate("__rtruediv__")
- __floordiv__ = _delegate("__floordiv__")
- __rfloordiv__ = _delegate("__rfloordiv__")
- __mod__ = _delegate("__mod__")
- __rmod__ = _delegate("__rmod__")
- __pow__ = _delegate("__pow__")
- __rpow__ = _delegate("__rpow__")
- __pos__ = _delegate("__pos__")
- __neg__ = _delegate("__neg__")
- __abs__ = _delegate("__abs__")
- __trunc__ = _delegate("__trunc__")
- __lt__ = _delegate("__lt__")
- __gt__ = _delegate("__gt__")
- __le__ = _delegate("__le__")
- __ge__ = _delegate("__ge__")
- __bool__ = _delegate("__bool__")
- __ceil__ = _delegate("__ceil__")
- __floor__ = _delegate("__floor__")
- __round__ = _delegate("__round__")
- # Python >= 3.11
- if hasattr(Fraction, "__int__"):
- __int__ = _delegate("__int__")
-
-
- class ImageFileDirectory_v2(MutableMapping):
- """This class represents a TIFF tag directory. To speed things up, we
- don't decode tags unless they're asked for.
-
- Exposes a dictionary interface of the tags in the directory::
-
- ifd = ImageFileDirectory_v2()
- ifd[key] = 'Some Data'
- ifd.tagtype[key] = TiffTags.ASCII
- print(ifd[key])
- 'Some Data'
-
- Individual values are returned as the strings or numbers, sequences are
- returned as tuples of the values.
-
- The tiff metadata type of each item is stored in a dictionary of
- tag types in
- :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
- are read from a tiff file, guessed from the type added, or added
- manually.
-
- Data Structures:
-
- * ``self.tagtype = {}``
-
- * Key: numerical TIFF tag number
- * Value: integer corresponding to the data type from
- :py:data:`.TiffTags.TYPES`
-
- .. versionadded:: 3.0.0
-
- 'Internal' data structures:
-
- * ``self._tags_v2 = {}``
-
- * Key: numerical TIFF tag number
- * Value: decoded data, as tuple for multiple values
-
- * ``self._tagdata = {}``
-
- * Key: numerical TIFF tag number
- * Value: undecoded byte string from file
-
- * ``self._tags_v1 = {}``
-
- * Key: numerical TIFF tag number
- * Value: decoded data in the v1 format
-
- Tags will be found in the private attributes ``self._tagdata``, and in
- ``self._tags_v2`` once decoded.
-
- ``self.legacy_api`` is a value for internal use, and shouldn't be changed
- from outside code. In cooperation with
- :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
- is true, then decoded tags will be populated into both ``_tags_v1`` and
- ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
- save routine. Tags should be read from ``_tags_v1`` if
- ``legacy_api == true``.
-
- """
-
- def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
- """Initialize an ImageFileDirectory.
-
- To construct an ImageFileDirectory from a real file, pass the 8-byte
- magic header to the constructor. To only set the endianness, pass it
- as the 'prefix' keyword argument.
-
- :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
- endianness.
- :param prefix: Override the endianness of the file.
- """
- if not _accept(ifh):
- msg = f"not a TIFF file (header {repr(ifh)} not valid)"
- raise SyntaxError(msg)
- self._prefix = prefix if prefix is not None else ifh[:2]
- if self._prefix == MM:
- self._endian = ">"
- elif self._prefix == II:
- self._endian = "<"
- else:
- msg = "not a TIFF IFD"
- raise SyntaxError(msg)
- self._bigtiff = ifh[2] == 43
- self.group = group
- self.tagtype = {}
- """ Dictionary of tag types """
- self.reset()
- (self.next,) = (
- self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:])
- )
- self._legacy_api = False
-
- prefix = property(lambda self: self._prefix)
- offset = property(lambda self: self._offset)
- legacy_api = property(lambda self: self._legacy_api)
-
- @legacy_api.setter
- def legacy_api(self, value):
- msg = "Not allowing setting of legacy api"
- raise Exception(msg)
-
- def reset(self):
- self._tags_v1 = {} # will remain empty if legacy_api is false
- self._tags_v2 = {} # main tag storage
- self._tagdata = {}
- self.tagtype = {} # added 2008-06-05 by Florian Hoech
- self._next = None
- self._offset = None
-
- def __str__(self):
- return str(dict(self))
-
- def named(self):
- """
- :returns: dict of name|key: value
-
- Returns the complete tag dictionary, with named tags where possible.
- """
- return {
- TiffTags.lookup(code, self.group).name: value
- for code, value in self.items()
- }
-
- def __len__(self):
- return len(set(self._tagdata) | set(self._tags_v2))
-
- def __getitem__(self, tag):
- if tag not in self._tags_v2: # unpack on the fly
- data = self._tagdata[tag]
- typ = self.tagtype[tag]
- size, handler = self._load_dispatch[typ]
- self[tag] = handler(self, data, self.legacy_api) # check type
- val = self._tags_v2[tag]
- if self.legacy_api and not isinstance(val, (tuple, bytes)):
- val = (val,)
- return val
-
- def __contains__(self, tag):
- return tag in self._tags_v2 or tag in self._tagdata
-
- def __setitem__(self, tag, value):
- self._setitem(tag, value, self.legacy_api)
-
- def _setitem(self, tag, value, legacy_api):
- basetypes = (Number, bytes, str)
-
- info = TiffTags.lookup(tag, self.group)
- values = [value] if isinstance(value, basetypes) else value
-
- if tag not in self.tagtype:
- if info.type:
- self.tagtype[tag] = info.type
- else:
- self.tagtype[tag] = TiffTags.UNDEFINED
- if all(isinstance(v, IFDRational) for v in values):
- self.tagtype[tag] = (
- TiffTags.RATIONAL
- if all(v >= 0 for v in values)
- else TiffTags.SIGNED_RATIONAL
- )
- elif all(isinstance(v, int) for v in values):
- if all(0 <= v < 2**16 for v in values):
- self.tagtype[tag] = TiffTags.SHORT
- elif all(-(2**15) < v < 2**15 for v in values):
- self.tagtype[tag] = TiffTags.SIGNED_SHORT
- else:
- self.tagtype[tag] = (
- TiffTags.LONG
- if all(v >= 0 for v in values)
- else TiffTags.SIGNED_LONG
- )
- elif all(isinstance(v, float) for v in values):
- self.tagtype[tag] = TiffTags.DOUBLE
- elif all(isinstance(v, str) for v in values):
- self.tagtype[tag] = TiffTags.ASCII
- elif all(isinstance(v, bytes) for v in values):
- self.tagtype[tag] = TiffTags.BYTE
-
- if self.tagtype[tag] == TiffTags.UNDEFINED:
- values = [
- v.encode("ascii", "replace") if isinstance(v, str) else v
- for v in values
- ]
- elif self.tagtype[tag] == TiffTags.RATIONAL:
- values = [float(v) if isinstance(v, int) else v for v in values]
-
- is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
- if not is_ifd:
- values = tuple(info.cvt_enum(value) for value in values)
-
- dest = self._tags_v1 if legacy_api else self._tags_v2
-
- # Three branches:
- # Spec'd length == 1, Actual length 1, store as element
- # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
- # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
- # Don't mess with the legacy api, since it's frozen.
- if not is_ifd and (
- (info.length == 1)
- or self.tagtype[tag] == TiffTags.BYTE
- or (info.length is None and len(values) == 1 and not legacy_api)
- ):
- # Don't mess with the legacy api, since it's frozen.
- if legacy_api and self.tagtype[tag] in [
- TiffTags.RATIONAL,
- TiffTags.SIGNED_RATIONAL,
- ]: # rationals
- values = (values,)
- try:
- (dest[tag],) = values
- except ValueError:
- # We've got a builtin tag with 1 expected entry
- warnings.warn(
- f"Metadata Warning, tag {tag} had too many entries: "
- f"{len(values)}, expected 1"
- )
- dest[tag] = values[0]
-
- else:
- # Spec'd length > 1 or undefined
- # Unspec'd, and length > 1
- dest[tag] = values
-
- def __delitem__(self, tag):
- self._tags_v2.pop(tag, None)
- self._tags_v1.pop(tag, None)
- self._tagdata.pop(tag, None)
-
- def __iter__(self):
- return iter(set(self._tagdata) | set(self._tags_v2))
-
- def _unpack(self, fmt, data):
- return struct.unpack(self._endian + fmt, data)
-
- def _pack(self, fmt, *values):
- return struct.pack(self._endian + fmt, *values)
-
- def _register_loader(idx, size):
- def decorator(func):
- from .TiffTags import TYPES
-
- if func.__name__.startswith("load_"):
- TYPES[idx] = func.__name__[5:].replace("_", " ")
- _load_dispatch[idx] = size, func # noqa: F821
- return func
-
- return decorator
-
- def _register_writer(idx):
- def decorator(func):
- _write_dispatch[idx] = func # noqa: F821
- return func
-
- return decorator
-
- def _register_basic(idx_fmt_name):
- from .TiffTags import TYPES
-
- idx, fmt, name = idx_fmt_name
- TYPES[idx] = name
- size = struct.calcsize("=" + fmt)
- _load_dispatch[idx] = ( # noqa: F821
- size,
- lambda self, data, legacy_api=True: (
- self._unpack(f"{len(data) // size}{fmt}", data)
- ),
- )
- _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
- b"".join(self._pack(fmt, value) for value in values)
- )
-
- list(
- map(
- _register_basic,
- [
- (TiffTags.SHORT, "H", "short"),
- (TiffTags.LONG, "L", "long"),
- (TiffTags.SIGNED_BYTE, "b", "signed byte"),
- (TiffTags.SIGNED_SHORT, "h", "signed short"),
- (TiffTags.SIGNED_LONG, "l", "signed long"),
- (TiffTags.FLOAT, "f", "float"),
- (TiffTags.DOUBLE, "d", "double"),
- (TiffTags.IFD, "L", "long"),
- (TiffTags.LONG8, "Q", "long8"),
- ],
- )
- )
-
- @_register_loader(1, 1) # Basic type, except for the legacy API.
- def load_byte(self, data, legacy_api=True):
- return data
-
- @_register_writer(1) # Basic type, except for the legacy API.
- def write_byte(self, data):
- if isinstance(data, IFDRational):
- data = int(data)
- if isinstance(data, int):
- data = bytes((data,))
- return data
-
- @_register_loader(2, 1)
- def load_string(self, data, legacy_api=True):
- if data.endswith(b"\0"):
- data = data[:-1]
- return data.decode("latin-1", "replace")
-
- @_register_writer(2)
- def write_string(self, value):
- # remerge of https://github.com/python-pillow/Pillow/pull/1416
- if isinstance(value, int):
- value = str(value)
- if not isinstance(value, bytes):
- value = value.encode("ascii", "replace")
- return value + b"\0"
-
- @_register_loader(5, 8)
- def load_rational(self, data, legacy_api=True):
- vals = self._unpack(f"{len(data) // 4}L", data)
-
- def combine(a, b):
- return (a, b) if legacy_api else IFDRational(a, b)
-
- return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
-
- @_register_writer(5)
- def write_rational(self, *values):
- return b"".join(
- self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
- )
-
- @_register_loader(7, 1)
- def load_undefined(self, data, legacy_api=True):
- return data
-
- @_register_writer(7)
- def write_undefined(self, value):
- if isinstance(value, int):
- value = str(value).encode("ascii", "replace")
- return value
-
- @_register_loader(10, 8)
- def load_signed_rational(self, data, legacy_api=True):
- vals = self._unpack(f"{len(data) // 4}l", data)
-
- def combine(a, b):
- return (a, b) if legacy_api else IFDRational(a, b)
-
- return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
-
- @_register_writer(10)
- def write_signed_rational(self, *values):
- return b"".join(
- self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
- for frac in values
- )
-
- def _ensure_read(self, fp, size):
- ret = fp.read(size)
- if len(ret) != size:
- msg = (
- "Corrupt EXIF data. "
- f"Expecting to read {size} bytes but only got {len(ret)}. "
- )
- raise OSError(msg)
- return ret
-
- def load(self, fp):
- self.reset()
- self._offset = fp.tell()
-
- try:
- tag_count = (
- self._unpack("Q", self._ensure_read(fp, 8))
- if self._bigtiff
- else self._unpack("H", self._ensure_read(fp, 2))
- )[0]
- for i in range(tag_count):
- tag, typ, count, data = (
- self._unpack("HHQ8s", self._ensure_read(fp, 20))
- if self._bigtiff
- else self._unpack("HHL4s", self._ensure_read(fp, 12))
- )
-
- tagname = TiffTags.lookup(tag, self.group).name
- typname = TYPES.get(typ, "unknown")
- msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
-
- try:
- unit_size, handler = self._load_dispatch[typ]
- except KeyError:
- logger.debug(msg + f" - unsupported type {typ}")
- continue # ignore unsupported type
- size = count * unit_size
- if size > (8 if self._bigtiff else 4):
- here = fp.tell()
- (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
- msg += f" Tag Location: {here} - Data Location: {offset}"
- fp.seek(offset)
- data = ImageFile._safe_read(fp, size)
- fp.seek(here)
- else:
- data = data[:size]
-
- if len(data) != size:
- warnings.warn(
- "Possibly corrupt EXIF data. "
- f"Expecting to read {size} bytes but only got {len(data)}."
- f" Skipping tag {tag}"
- )
- logger.debug(msg)
- continue
-
- if not data:
- logger.debug(msg)
- continue
-
- self._tagdata[tag] = data
- self.tagtype[tag] = typ
-
- msg += " - value: " + (
- "<table: %d bytes>" % size if size > 32 else repr(data)
- )
- logger.debug(msg)
-
- (self.next,) = (
- self._unpack("Q", self._ensure_read(fp, 8))
- if self._bigtiff
- else self._unpack("L", self._ensure_read(fp, 4))
- )
- except OSError as msg:
- warnings.warn(str(msg))
- return
-
- def tobytes(self, offset=0):
- # FIXME What about tagdata?
- result = self._pack("H", len(self._tags_v2))
-
- entries = []
- offset = offset + len(result) + len(self._tags_v2) * 12 + 4
- stripoffsets = None
-
- # pass 1: convert tags to binary format
- # always write tags in ascending order
- for tag, value in sorted(self._tags_v2.items()):
- if tag == STRIPOFFSETS:
- stripoffsets = len(entries)
- typ = self.tagtype.get(tag)
- logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}")
- is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
- if is_ifd:
- if self._endian == "<":
- ifh = b"II\x2A\x00\x08\x00\x00\x00"
- else:
- ifh = b"MM\x00\x2A\x00\x00\x00\x08"
- ifd = ImageFileDirectory_v2(ifh, group=tag)
- values = self._tags_v2[tag]
- for ifd_tag, ifd_value in values.items():
- ifd[ifd_tag] = ifd_value
- data = ifd.tobytes(offset)
- else:
- values = value if isinstance(value, tuple) else (value,)
- data = self._write_dispatch[typ](self, *values)
-
- tagname = TiffTags.lookup(tag, self.group).name
- typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
- msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})"
- msg += " - value: " + (
- "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
- )
- logger.debug(msg)
-
- # count is sum of lengths for string and arbitrary data
- if is_ifd:
- count = 1
- elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
- count = len(data)
- else:
- count = len(values)
- # figure out if data fits into the entry
- if len(data) <= 4:
- entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
- else:
- entries.append((tag, typ, count, self._pack("L", offset), data))
- offset += (len(data) + 1) // 2 * 2 # pad to word
-
- # update strip offset data to point beyond auxiliary data
- if stripoffsets is not None:
- tag, typ, count, value, data = entries[stripoffsets]
- if data:
- msg = "multistrip support not yet implemented"
- raise NotImplementedError(msg)
- value = self._pack("L", self._unpack("L", value)[0] + offset)
- entries[stripoffsets] = tag, typ, count, value, data
-
- # pass 2: write entries to file
- for tag, typ, count, value, data in entries:
- logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}")
- result += self._pack("HHL4s", tag, typ, count, value)
-
- # -- overwrite here for multi-page --
- result += b"\0\0\0\0" # end of entries
-
- # pass 3: write auxiliary data to file
- for tag, typ, count, value, data in entries:
- result += data
- if len(data) & 1:
- result += b"\0"
-
- return result
-
- def save(self, fp):
- if fp.tell() == 0: # skip TIFF header on subsequent pages
- # tiff header -- PIL always starts the first IFD at offset 8
- fp.write(self._prefix + self._pack("HL", 42, 8))
-
- offset = fp.tell()
- result = self.tobytes(offset)
- fp.write(result)
- return offset + len(result)
-
-
- ImageFileDirectory_v2._load_dispatch = _load_dispatch
- ImageFileDirectory_v2._write_dispatch = _write_dispatch
- for idx, name in TYPES.items():
- name = name.replace(" ", "_")
- setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1])
- setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx])
- del _load_dispatch, _write_dispatch, idx, name
-
-
- # Legacy ImageFileDirectory support.
- class ImageFileDirectory_v1(ImageFileDirectory_v2):
- """This class represents the **legacy** interface to a TIFF tag directory.
-
- Exposes a dictionary interface of the tags in the directory::
-
- ifd = ImageFileDirectory_v1()
- ifd[key] = 'Some Data'
- ifd.tagtype[key] = TiffTags.ASCII
- print(ifd[key])
- ('Some Data',)
-
- Also contains a dictionary of tag types as read from the tiff image file,
- :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
-
- Values are returned as a tuple.
-
- .. deprecated:: 3.0.0
- """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._legacy_api = True
-
- tags = property(lambda self: self._tags_v1)
- tagdata = property(lambda self: self._tagdata)
-
- # defined in ImageFileDirectory_v2
- tagtype: dict
- """Dictionary of tag types"""
-
- @classmethod
- def from_v2(cls, original):
- """Returns an
- :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
- instance with the same data as is contained in the original
- :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
- instance.
-
- :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
-
- """
-
- ifd = cls(prefix=original.prefix)
- ifd._tagdata = original._tagdata
- ifd.tagtype = original.tagtype
- ifd.next = original.next # an indicator for multipage tiffs
- return ifd
-
- def to_v2(self):
- """Returns an
- :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
- instance with the same data as is contained in the original
- :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
- instance.
-
- :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
-
- """
-
- ifd = ImageFileDirectory_v2(prefix=self.prefix)
- ifd._tagdata = dict(self._tagdata)
- ifd.tagtype = dict(self.tagtype)
- ifd._tags_v2 = dict(self._tags_v2)
- return ifd
-
- def __contains__(self, tag):
- return tag in self._tags_v1 or tag in self._tagdata
-
- def __len__(self):
- return len(set(self._tagdata) | set(self._tags_v1))
-
- def __iter__(self):
- return iter(set(self._tagdata) | set(self._tags_v1))
-
- def __setitem__(self, tag, value):
- for legacy_api in (False, True):
- self._setitem(tag, value, legacy_api)
-
- def __getitem__(self, tag):
- if tag not in self._tags_v1: # unpack on the fly
- data = self._tagdata[tag]
- typ = self.tagtype[tag]
- size, handler = self._load_dispatch[typ]
- for legacy in (False, True):
- self._setitem(tag, handler(self, data, legacy), legacy)
- val = self._tags_v1[tag]
- if not isinstance(val, (tuple, bytes)):
- val = (val,)
- return val
-
-
- # undone -- switch this pointer when IFD_LEGACY_API == False
- ImageFileDirectory = ImageFileDirectory_v1
-
-
- ##
- # Image plugin for TIFF files.
-
-
- class TiffImageFile(ImageFile.ImageFile):
- format = "TIFF"
- format_description = "Adobe TIFF"
- _close_exclusive_fp_after_loading = False
-
- def __init__(self, fp=None, filename=None):
- self.tag_v2 = None
- """ Image file directory (tag dictionary) """
-
- self.tag = None
- """ Legacy tag entries """
-
- super().__init__(fp, filename)
-
- def _open(self):
- """Open the first image in a TIFF file"""
-
- # Header
- ifh = self.fp.read(8)
- if ifh[2] == 43:
- ifh += self.fp.read(8)
-
- self.tag_v2 = ImageFileDirectory_v2(ifh)
-
- # legacy IFD entries will be filled in later
- self.ifd = None
-
- # setup frame pointers
- self.__first = self.__next = self.tag_v2.next
- self.__frame = -1
- self._fp = self.fp
- self._frame_pos = []
- self._n_frames = None
-
- logger.debug("*** TiffImageFile._open ***")
- logger.debug(f"- __first: {self.__first}")
- logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes)
-
- # and load the first frame
- self._seek(0)
-
- @property
- def n_frames(self):
- if self._n_frames is None:
- current = self.tell()
- self._seek(len(self._frame_pos))
- while self._n_frames is None:
- self._seek(self.tell() + 1)
- self.seek(current)
- return self._n_frames
-
- def seek(self, frame):
- """Select a given frame as current image"""
- if not self._seek_check(frame):
- return
- self._seek(frame)
- # Create a new core image object on second and
- # subsequent frames in the image. Image may be
- # different size/mode.
- Image._decompression_bomb_check(self.size)
- self.im = Image.core.new(self.mode, self.size)
-
- def _seek(self, frame):
- self.fp = self._fp
-
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- self.fp.tell()
-
- while len(self._frame_pos) <= frame:
- if not self.__next:
- msg = "no more images in TIFF file"
- raise EOFError(msg)
- logger.debug(
- f"Seeking to frame {frame}, on frame {self.__frame}, "
- f"__next {self.__next}, location: {self.fp.tell()}"
- )
- self.fp.seek(self.__next)
- self._frame_pos.append(self.__next)
- logger.debug("Loading tags, location: %s" % self.fp.tell())
- self.tag_v2.load(self.fp)
- if self.tag_v2.next in self._frame_pos:
- # This IFD has already been processed
- # Declare this to be the end of the image
- self.__next = 0
- else:
- self.__next = self.tag_v2.next
- if self.__next == 0:
- self._n_frames = frame + 1
- if len(self._frame_pos) == 1:
- self.is_animated = self.__next != 0
- self.__frame += 1
- self.fp.seek(self._frame_pos[frame])
- self.tag_v2.load(self.fp)
- self._reload_exif()
- # fill the legacy tag/ifd entries
- self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
- self.__frame = frame
- self._setup()
-
- def tell(self):
- """Return the current frame number"""
- return self.__frame
-
- def getxmp(self):
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
-
- def get_photoshop_blocks(self):
- """
- Returns a dictionary of Photoshop "Image Resource Blocks".
- The keys are the image resource ID. For more information, see
- https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727
-
- :returns: Photoshop "Image Resource Blocks" in a dictionary.
- """
- blocks = {}
- val = self.tag_v2.get(0x8649)
- if val:
- while val[:4] == b"8BIM":
- id = i16(val[4:6])
- n = math.ceil((val[6] + 1) / 2) * 2
- size = i32(val[6 + n : 10 + n])
- data = val[10 + n : 10 + n + size]
- blocks[id] = {"data": data}
-
- val = val[math.ceil((10 + n + size) / 2) * 2 :]
- return blocks
-
- def load(self):
- if self.tile and self.use_load_libtiff:
- return self._load_libtiff()
- return super().load()
-
- def load_end(self):
- if self._tile_orientation:
- method = {
- 2: Image.Transpose.FLIP_LEFT_RIGHT,
- 3: Image.Transpose.ROTATE_180,
- 4: Image.Transpose.FLIP_TOP_BOTTOM,
- 5: Image.Transpose.TRANSPOSE,
- 6: Image.Transpose.ROTATE_270,
- 7: Image.Transpose.TRANSVERSE,
- 8: Image.Transpose.ROTATE_90,
- }.get(self._tile_orientation)
- if method is not None:
- self.im = self.im.transpose(method)
- self._size = self.im.size
-
- # allow closing if we're on the first frame, there's no next
- # This is the ImageFile.load path only, libtiff specific below.
- if not self.is_animated:
- self._close_exclusive_fp_after_loading = True
-
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- self.fp.tell()
-
- # load IFD data from fp before it is closed
- exif = self.getexif()
- for key in TiffTags.TAGS_V2_GROUPS:
- if key not in exif:
- continue
- exif.get_ifd(key)
-
- def _load_libtiff(self):
- """Overload method triggered when we detect a compressed tiff
- Calls out to libtiff"""
-
- Image.Image.load(self)
-
- self.load_prepare()
-
- if not len(self.tile) == 1:
- msg = "Not exactly one tile"
- raise OSError(msg)
-
- # (self._compression, (extents tuple),
- # 0, (rawmode, self._compression, fp))
- extents = self.tile[0][1]
- args = list(self.tile[0][3])
-
- # To be nice on memory footprint, if there's a
- # file descriptor, use that instead of reading
- # into a string in python.
- # libtiff closes the file descriptor, so pass in a dup.
- try:
- fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())
- # flush the file descriptor, prevents error on pypy 2.4+
- # should also eliminate the need for fp.tell
- # in _seek
- if hasattr(self.fp, "flush"):
- self.fp.flush()
- except OSError:
- # io.BytesIO have a fileno, but returns an OSError if
- # it doesn't use a file descriptor.
- fp = False
-
- if fp:
- args[2] = fp
-
- decoder = Image._getdecoder(
- self.mode, "libtiff", tuple(args), self.decoderconfig
- )
- try:
- decoder.setimage(self.im, extents)
- except ValueError as e:
- msg = "Couldn't set the image"
- raise OSError(msg) from e
-
- close_self_fp = self._exclusive_fp and not self.is_animated
- if hasattr(self.fp, "getvalue"):
- # We've got a stringio like thing passed in. Yay for all in memory.
- # The decoder needs the entire file in one shot, so there's not
- # a lot we can do here other than give it the entire file.
- # unless we could do something like get the address of the
- # underlying string for stringio.
- #
- # Rearranging for supporting byteio items, since they have a fileno
- # that returns an OSError if there's no underlying fp. Easier to
- # deal with here by reordering.
- logger.debug("have getvalue. just sending in a string from getvalue")
- n, err = decoder.decode(self.fp.getvalue())
- elif fp:
- # we've got a actual file on disk, pass in the fp.
- logger.debug("have fileno, calling fileno version of the decoder.")
- if not close_self_fp:
- self.fp.seek(0)
- # 4 bytes, otherwise the trace might error out
- n, err = decoder.decode(b"fpfp")
- else:
- # we have something else.
- logger.debug("don't have fileno or getvalue. just reading")
- self.fp.seek(0)
- # UNDONE -- so much for that buffer size thing.
- n, err = decoder.decode(self.fp.read())
-
- if fp:
- try:
- os.close(fp)
- except OSError:
- pass
-
- self.tile = []
- self.readonly = 0
-
- self.load_end()
-
- # libtiff closed the fp in a, we need to close self.fp, if possible
- if close_self_fp:
- self.fp.close()
- self.fp = None # might be shared
-
- if err < 0:
- raise OSError(err)
-
- return Image.Image.load(self)
-
- def _setup(self):
- """Setup this image object based on current tags"""
-
- if 0xBC01 in self.tag_v2:
- msg = "Windows Media Photo files not yet supported"
- raise OSError(msg)
-
- # extract relevant tags
- self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
- self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)
-
- # photometric is a required tag, but not everyone is reading
- # the specification
- photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
-
- # old style jpeg compression images most certainly are YCbCr
- if self._compression == "tiff_jpeg":
- photo = 6
-
- fillorder = self.tag_v2.get(FILLORDER, 1)
-
- logger.debug("*** Summary ***")
- logger.debug(f"- compression: {self._compression}")
- logger.debug(f"- photometric_interpretation: {photo}")
- logger.debug(f"- planar_configuration: {self._planar_configuration}")
- logger.debug(f"- fill_order: {fillorder}")
- logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
-
- # size
- xsize = int(self.tag_v2.get(IMAGEWIDTH))
- ysize = int(self.tag_v2.get(IMAGELENGTH))
- self._size = xsize, ysize
-
- logger.debug(f"- size: {self.size}")
-
- sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
- if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
- # SAMPLEFORMAT is properly per band, so an RGB image will
- # be (1,1,1). But, we don't support per band pixel types,
- # and anything more than one band is a uint8. So, just
- # take the first element. Revisit this if adding support
- # for more exotic images.
- sample_format = (1,)
-
- bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
- extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
- if photo in (2, 6, 8): # RGB, YCbCr, LAB
- bps_count = 3
- elif photo == 5: # CMYK
- bps_count = 4
- else:
- bps_count = 1
- bps_count += len(extra_tuple)
- bps_actual_count = len(bps_tuple)
- samples_per_pixel = self.tag_v2.get(
- SAMPLESPERPIXEL,
- 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
- )
-
- if samples_per_pixel > MAX_SAMPLESPERPIXEL:
- # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
- logger.error(
- "More samples per pixel than can be decoded: %s", samples_per_pixel
- )
- msg = "Invalid value for samples per pixel"
- raise SyntaxError(msg)
-
- if samples_per_pixel < bps_actual_count:
- # If a file has more values in bps_tuple than expected,
- # remove the excess.
- bps_tuple = bps_tuple[:samples_per_pixel]
- elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
- # If a file has only one value in bps_tuple, when it should have more,
- # presume it is the same number of bits for all of the samples.
- bps_tuple = bps_tuple * samples_per_pixel
-
- if len(bps_tuple) != samples_per_pixel:
- msg = "unknown data organization"
- raise SyntaxError(msg)
-
- # mode: check photometric interpretation and bits per pixel
- key = (
- self.tag_v2.prefix,
- photo,
- sample_format,
- fillorder,
- bps_tuple,
- extra_tuple,
- )
- logger.debug(f"format key: {key}")
- try:
- self.mode, rawmode = OPEN_INFO[key]
- except KeyError as e:
- logger.debug("- unsupported format")
- msg = "unknown pixel mode"
- raise SyntaxError(msg) from e
-
- logger.debug(f"- raw mode: {rawmode}")
- logger.debug(f"- pil mode: {self.mode}")
-
- self.info["compression"] = self._compression
-
- xres = self.tag_v2.get(X_RESOLUTION, 1)
- yres = self.tag_v2.get(Y_RESOLUTION, 1)
-
- if xres and yres:
- resunit = self.tag_v2.get(RESOLUTION_UNIT)
- if resunit == 2: # dots per inch
- self.info["dpi"] = (xres, yres)
- elif resunit == 3: # dots per centimeter. convert to dpi
- self.info["dpi"] = (xres * 2.54, yres * 2.54)
- elif resunit is None: # used to default to 1, but now 2)
- self.info["dpi"] = (xres, yres)
- # For backward compatibility,
- # we also preserve the old behavior
- self.info["resolution"] = xres, yres
- else: # No absolute unit of measurement
- self.info["resolution"] = xres, yres
-
- # build tile descriptors
- x = y = layer = 0
- self.tile = []
- self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
- if self.use_load_libtiff:
- # Decoder expects entire file as one tile.
- # There's a buffer size limit in load (64k)
- # so large g4 images will fail if we use that
- # function.
- #
- # Setup the one tile for the whole image, then
- # use the _load_libtiff function.
-
- # libtiff handles the fillmode for us, so 1;IR should
- # actually be 1;I. Including the R double reverses the
- # bits, so stripes of the image are reversed. See
- # https://github.com/python-pillow/Pillow/issues/279
- if fillorder == 2:
- # Replace fillorder with fillorder=1
- key = key[:3] + (1,) + key[4:]
- logger.debug(f"format key: {key}")
- # this should always work, since all the
- # fillorder==2 modes have a corresponding
- # fillorder=1 mode
- self.mode, rawmode = OPEN_INFO[key]
- # libtiff always returns the bytes in native order.
- # we're expecting image byte order. So, if the rawmode
- # contains I;16, we need to convert from native to image
- # byte order.
- if rawmode == "I;16":
- rawmode = "I;16N"
- if ";16B" in rawmode:
- rawmode = rawmode.replace(";16B", ";16N")
- if ";16L" in rawmode:
- rawmode = rawmode.replace(";16L", ";16N")
-
- # YCbCr images with new jpeg compression with pixels in one plane
- # unpacked straight into RGB values
- if (
- photo == 6
- and self._compression == "jpeg"
- and self._planar_configuration == 1
- ):
- rawmode = "RGB"
-
- # Offset in the tile tuple is 0, we go from 0,0 to
- # w,h, and we only do this once -- eds
- a = (rawmode, self._compression, False, self.tag_v2.offset)
- self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a))
-
- elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
- # striped image
- if STRIPOFFSETS in self.tag_v2:
- offsets = self.tag_v2[STRIPOFFSETS]
- h = self.tag_v2.get(ROWSPERSTRIP, ysize)
- w = self.size[0]
- else:
- # tiled image
- offsets = self.tag_v2[TILEOFFSETS]
- w = self.tag_v2.get(TILEWIDTH)
- h = self.tag_v2.get(TILELENGTH)
-
- for offset in offsets:
- if x + w > xsize:
- stride = w * sum(bps_tuple) / 8 # bytes per line
- else:
- stride = 0
-
- tile_rawmode = rawmode
- if self._planar_configuration == 2:
- # each band on it's own layer
- tile_rawmode = rawmode[layer]
- # adjust stride width accordingly
- stride /= bps_count
-
- a = (tile_rawmode, int(stride), 1)
- self.tile.append(
- (
- self._compression,
- (x, y, min(x + w, xsize), min(y + h, ysize)),
- offset,
- a,
- )
- )
- x = x + w
- if x >= self.size[0]:
- x, y = 0, y + h
- if y >= self.size[1]:
- x = y = 0
- layer += 1
- else:
- logger.debug("- unsupported data organization")
- msg = "unknown data organization"
- raise SyntaxError(msg)
-
- # Fix up info.
- if ICCPROFILE in self.tag_v2:
- self.info["icc_profile"] = self.tag_v2[ICCPROFILE]
-
- # fixup palette descriptor
-
- if self.mode in ["P", "PA"]:
- palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
- self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
-
- self._tile_orientation = self.tag_v2.get(0x0112)
-
-
- #
- # --------------------------------------------------------------------
- # Write TIFF files
-
- # little endian is default except for image modes with
- # explicit big endian byte-order
-
- SAVE_INFO = {
- # mode => rawmode, byteorder, photometrics,
- # sampleformat, bitspersample, extra
- "1": ("1", II, 1, 1, (1,), None),
- "L": ("L", II, 1, 1, (8,), None),
- "LA": ("LA", II, 1, 1, (8, 8), 2),
- "P": ("P", II, 3, 1, (8,), None),
- "PA": ("PA", II, 3, 1, (8, 8), 2),
- "I": ("I;32S", II, 1, 2, (32,), None),
- "I;16": ("I;16", II, 1, 1, (16,), None),
- "I;16S": ("I;16S", II, 1, 2, (16,), None),
- "F": ("F;32F", II, 1, 3, (32,), None),
- "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
- "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
- "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
- "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
- "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
- "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
- "I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
- "I;16B": ("I;16B", MM, 1, 1, (16,), None),
- "I;16BS": ("I;16BS", MM, 1, 2, (16,), None),
- "F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
- }
-
-
- def _save(im, fp, filename):
- try:
- rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
- except KeyError as e:
- msg = f"cannot write mode {im.mode} as TIFF"
- raise OSError(msg) from e
-
- ifd = ImageFileDirectory_v2(prefix=prefix)
-
- encoderinfo = im.encoderinfo
- encoderconfig = im.encoderconfig
- try:
- compression = encoderinfo["compression"]
- except KeyError:
- compression = im.info.get("compression")
- if isinstance(compression, int):
- # compression value may be from BMP. Ignore it
- compression = None
- if compression is None:
- compression = "raw"
- elif compression == "tiff_jpeg":
- # OJPEG is obsolete, so use new-style JPEG compression instead
- compression = "jpeg"
- elif compression == "tiff_deflate":
- compression = "tiff_adobe_deflate"
-
- libtiff = WRITE_LIBTIFF or compression != "raw"
-
- # required for color libtiff images
- ifd[PLANAR_CONFIGURATION] = 1
-
- ifd[IMAGEWIDTH] = im.size[0]
- ifd[IMAGELENGTH] = im.size[1]
-
- # write any arbitrary tags passed in as an ImageFileDirectory
- if "tiffinfo" in encoderinfo:
- info = encoderinfo["tiffinfo"]
- elif "exif" in encoderinfo:
- info = encoderinfo["exif"]
- if isinstance(info, bytes):
- exif = Image.Exif()
- exif.load(info)
- info = exif
- else:
- info = {}
- logger.debug("Tiffinfo Keys: %s" % list(info))
- if isinstance(info, ImageFileDirectory_v1):
- info = info.to_v2()
- for key in info:
- if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS:
- ifd[key] = info.get_ifd(key)
- else:
- ifd[key] = info.get(key)
- try:
- ifd.tagtype[key] = info.tagtype[key]
- except Exception:
- pass # might not be an IFD. Might not have populated type
-
- # additions written by Greg Couch, gregc@cgl.ucsf.edu
- # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
- if hasattr(im, "tag_v2"):
- # preserve tags from original TIFF image file
- for key in (
- RESOLUTION_UNIT,
- X_RESOLUTION,
- Y_RESOLUTION,
- IPTC_NAA_CHUNK,
- PHOTOSHOP_CHUNK,
- XMP,
- ):
- if key in im.tag_v2:
- ifd[key] = im.tag_v2[key]
- ifd.tagtype[key] = im.tag_v2.tagtype[key]
-
- # preserve ICC profile (should also work when saving other formats
- # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
- icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
- if icc:
- ifd[ICCPROFILE] = icc
-
- for key, name in [
- (IMAGEDESCRIPTION, "description"),
- (X_RESOLUTION, "resolution"),
- (Y_RESOLUTION, "resolution"),
- (X_RESOLUTION, "x_resolution"),
- (Y_RESOLUTION, "y_resolution"),
- (RESOLUTION_UNIT, "resolution_unit"),
- (SOFTWARE, "software"),
- (DATE_TIME, "date_time"),
- (ARTIST, "artist"),
- (COPYRIGHT, "copyright"),
- ]:
- if name in encoderinfo:
- ifd[key] = encoderinfo[name]
-
- dpi = encoderinfo.get("dpi")
- if dpi:
- ifd[RESOLUTION_UNIT] = 2
- ifd[X_RESOLUTION] = dpi[0]
- ifd[Y_RESOLUTION] = dpi[1]
-
- if bits != (1,):
- ifd[BITSPERSAMPLE] = bits
- if len(bits) != 1:
- ifd[SAMPLESPERPIXEL] = len(bits)
- if extra is not None:
- ifd[EXTRASAMPLES] = extra
- if format != 1:
- ifd[SAMPLEFORMAT] = format
-
- if PHOTOMETRIC_INTERPRETATION not in ifd:
- ifd[PHOTOMETRIC_INTERPRETATION] = photo
- elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
- if im.mode == "1":
- inverted_im = im.copy()
- px = inverted_im.load()
- for y in range(inverted_im.height):
- for x in range(inverted_im.width):
- px[x, y] = 0 if px[x, y] == 255 else 255
- im = inverted_im
- else:
- im = ImageOps.invert(im)
-
- if im.mode in ["P", "PA"]:
- lut = im.im.getpalette("RGB", "RGB;L")
- colormap = []
- colors = len(lut) // 3
- for i in range(3):
- colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]
- colormap += [0] * (256 - colors)
- ifd[COLORMAP] = colormap
- # data orientation
- stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
- # aim for given strip size (64 KB by default) when using libtiff writer
- if libtiff:
- im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
- rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
- # JPEG encoder expects multiple of 8 rows
- if compression == "jpeg":
- rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
- else:
- rows_per_strip = im.size[1]
- if rows_per_strip == 0:
- rows_per_strip = 1
- strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
- strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
- ifd[ROWSPERSTRIP] = rows_per_strip
- if strip_byte_counts >= 2**16:
- ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
- ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
- stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
- )
- ifd[STRIPOFFSETS] = tuple(
- range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
- ) # this is adjusted by IFD writer
- # no compression by default:
- ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
-
- if im.mode == "YCbCr":
- for tag, value in {
- YCBCRSUBSAMPLING: (1, 1),
- REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
- }.items():
- ifd.setdefault(tag, value)
-
- blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
- if libtiff:
- if "quality" in encoderinfo:
- quality = encoderinfo["quality"]
- if not isinstance(quality, int) or quality < 0 or quality > 100:
- msg = "Invalid quality setting"
- raise ValueError(msg)
- if compression != "jpeg":
- msg = "quality setting only supported for 'jpeg' compression"
- raise ValueError(msg)
- ifd[JPEGQUALITY] = quality
-
- logger.debug("Saving using libtiff encoder")
- logger.debug("Items: %s" % sorted(ifd.items()))
- _fp = 0
- if hasattr(fp, "fileno"):
- try:
- fp.seek(0)
- _fp = os.dup(fp.fileno())
- except io.UnsupportedOperation:
- pass
-
- # optional types for non core tags
- types = {}
- # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
- # based on the data in the strip.
- # The other tags expect arrays with a certain length (fixed or depending on
- # BITSPERSAMPLE, etc), passing arrays with a different length will result in
- # segfaults. Block these tags until we add extra validation.
- # SUBIFD may also cause a segfault.
- blocklist += [
- REFERENCEBLACKWHITE,
- STRIPBYTECOUNTS,
- STRIPOFFSETS,
- TRANSFERFUNCTION,
- SUBIFD,
- ]
-
- # bits per sample is a single short in the tiff directory, not a list.
- atts = {BITSPERSAMPLE: bits[0]}
- # Merge the ones that we have with (optional) more bits from
- # the original file, e.g x,y resolution so that we can
- # save(load('')) == original file.
- legacy_ifd = {}
- if hasattr(im, "tag"):
- legacy_ifd = im.tag.to_v2()
-
- # SAMPLEFORMAT is determined by the image format and should not be copied
- # from legacy_ifd.
- supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
- if SAMPLEFORMAT in supplied_tags:
- del supplied_tags[SAMPLEFORMAT]
-
- for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
- # Libtiff can only process certain core items without adding
- # them to the custom dictionary.
- # Custom items are supported for int, float, unicode, string and byte
- # values. Other types and tuples require a tagtype.
- if tag not in TiffTags.LIBTIFF_CORE:
- if not getattr(Image.core, "libtiff_support_custom_tags", False):
- continue
-
- if tag in ifd.tagtype:
- types[tag] = ifd.tagtype[tag]
- elif not (isinstance(value, (int, float, str, bytes))):
- continue
- else:
- type = TiffTags.lookup(tag).type
- if type:
- types[tag] = type
- if tag not in atts and tag not in blocklist:
- if isinstance(value, str):
- atts[tag] = value.encode("ascii", "replace") + b"\0"
- elif isinstance(value, IFDRational):
- atts[tag] = float(value)
- else:
- atts[tag] = value
-
- if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
- atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
-
- logger.debug("Converted items: %s" % sorted(atts.items()))
-
- # libtiff always expects the bytes in native order.
- # we're storing image byte order. So, if the rawmode
- # contains I;16, we need to convert from native to image
- # byte order.
- if im.mode in ("I;16B", "I;16"):
- rawmode = "I;16N"
-
- # Pass tags as sorted list so that the tags are set in a fixed order.
- # This is required by libtiff for some tags. For example, the JPEGQUALITY
- # pseudo tag requires that the COMPRESS tag was already set.
- tags = list(atts.items())
- tags.sort()
- a = (rawmode, compression, _fp, filename, tags, types)
- e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
- e.setimage(im.im, (0, 0) + im.size)
- while True:
- # undone, change to self.decodermaxblock:
- errcode, data = e.encode(16 * 1024)[1:]
- if not _fp:
- fp.write(data)
- if errcode:
- break
- if _fp:
- try:
- os.close(_fp)
- except OSError:
- pass
- if errcode < 0:
- msg = f"encoder error {errcode} when writing image file"
- raise OSError(msg)
-
- else:
- for tag in blocklist:
- del ifd[tag]
- offset = ifd.save(fp)
-
- ImageFile._save(
- im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
- )
-
- # -- helper for multi-page save --
- if "_debug_multipage" in encoderinfo:
- # just to access o32 and o16 (using correct byte order)
- im._debug_multipage = ifd
-
-
- class AppendingTiffWriter:
- fieldSizes = [
- 0, # None
- 1, # byte
- 1, # ascii
- 2, # short
- 4, # long
- 8, # rational
- 1, # sbyte
- 1, # undefined
- 2, # sshort
- 4, # slong
- 8, # srational
- 4, # float
- 8, # double
- ]
-
- # StripOffsets = 273
- # FreeOffsets = 288
- # TileOffsets = 324
- # JPEGQTables = 519
- # JPEGDCTables = 520
- # JPEGACTables = 521
- Tags = {273, 288, 324, 519, 520, 521}
-
- def __init__(self, fn, new=False):
- if hasattr(fn, "read"):
- self.f = fn
- self.close_fp = False
- else:
- self.name = fn
- self.close_fp = True
- try:
- self.f = open(fn, "w+b" if new else "r+b")
- except OSError:
- self.f = open(fn, "w+b")
- self.beginning = self.f.tell()
- self.setup()
-
- def setup(self):
- # Reset everything.
- self.f.seek(self.beginning, os.SEEK_SET)
-
- self.whereToWriteNewIFDOffset = None
- self.offsetOfNewPage = 0
-
- self.IIMM = iimm = self.f.read(4)
- if not iimm:
- # empty file - first page
- self.isFirst = True
- return
-
- self.isFirst = False
- if iimm == b"II\x2a\x00":
- self.setEndian("<")
- elif iimm == b"MM\x00\x2a":
- self.setEndian(">")
- else:
- msg = "Invalid TIFF file header"
- raise RuntimeError(msg)
-
- self.skipIFDs()
- self.goToEnd()
-
- def finalize(self):
- if self.isFirst:
- return
-
- # fix offsets
- self.f.seek(self.offsetOfNewPage)
-
- iimm = self.f.read(4)
- if not iimm:
- # msg = "nothing written into new page"
- # raise RuntimeError(msg)
- # Make it easy to finish a frame without committing to a new one.
- return
-
- if iimm != self.IIMM:
- msg = "IIMM of new page doesn't match IIMM of first page"
- raise RuntimeError(msg)
-
- ifd_offset = self.readLong()
- ifd_offset += self.offsetOfNewPage
- self.f.seek(self.whereToWriteNewIFDOffset)
- self.writeLong(ifd_offset)
- self.f.seek(ifd_offset)
- self.fixIFD()
-
- def newFrame(self):
- # Call this to finish a frame.
- self.finalize()
- self.setup()
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- if self.close_fp:
- self.close()
- return False
-
- def tell(self):
- return self.f.tell() - self.offsetOfNewPage
-
- def seek(self, offset, whence=io.SEEK_SET):
- if whence == os.SEEK_SET:
- offset += self.offsetOfNewPage
-
- self.f.seek(offset, whence)
- return self.tell()
-
- def goToEnd(self):
- self.f.seek(0, os.SEEK_END)
- pos = self.f.tell()
-
- # pad to 16 byte boundary
- pad_bytes = 16 - pos % 16
- if 0 < pad_bytes < 16:
- self.f.write(bytes(pad_bytes))
- self.offsetOfNewPage = self.f.tell()
-
- def setEndian(self, endian):
- self.endian = endian
- self.longFmt = self.endian + "L"
- self.shortFmt = self.endian + "H"
- self.tagFormat = self.endian + "HHL"
-
- def skipIFDs(self):
- while True:
- ifd_offset = self.readLong()
- if ifd_offset == 0:
- self.whereToWriteNewIFDOffset = self.f.tell() - 4
- break
-
- self.f.seek(ifd_offset)
- num_tags = self.readShort()
- self.f.seek(num_tags * 12, os.SEEK_CUR)
-
- def write(self, data):
- return self.f.write(data)
-
- def readShort(self):
- (value,) = struct.unpack(self.shortFmt, self.f.read(2))
- return value
-
- def readLong(self):
- (value,) = struct.unpack(self.longFmt, self.f.read(4))
- return value
-
- def rewriteLastShortToLong(self, value):
- self.f.seek(-2, os.SEEK_CUR)
- bytes_written = self.f.write(struct.pack(self.longFmt, value))
- if bytes_written is not None and bytes_written != 4:
- msg = f"wrote only {bytes_written} bytes but wanted 4"
- raise RuntimeError(msg)
-
- def rewriteLastShort(self, value):
- self.f.seek(-2, os.SEEK_CUR)
- bytes_written = self.f.write(struct.pack(self.shortFmt, value))
- if bytes_written is not None and bytes_written != 2:
- msg = f"wrote only {bytes_written} bytes but wanted 2"
- raise RuntimeError(msg)
-
- def rewriteLastLong(self, value):
- self.f.seek(-4, os.SEEK_CUR)
- bytes_written = self.f.write(struct.pack(self.longFmt, value))
- if bytes_written is not None and bytes_written != 4:
- msg = f"wrote only {bytes_written} bytes but wanted 4"
- raise RuntimeError(msg)
-
- def writeShort(self, value):
- bytes_written = self.f.write(struct.pack(self.shortFmt, value))
- if bytes_written is not None and bytes_written != 2:
- msg = f"wrote only {bytes_written} bytes but wanted 2"
- raise RuntimeError(msg)
-
- def writeLong(self, value):
- bytes_written = self.f.write(struct.pack(self.longFmt, value))
- if bytes_written is not None and bytes_written != 4:
- msg = f"wrote only {bytes_written} bytes but wanted 4"
- raise RuntimeError(msg)
-
- def close(self):
- self.finalize()
- self.f.close()
-
- def fixIFD(self):
- num_tags = self.readShort()
-
- for i in range(num_tags):
- tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
-
- field_size = self.fieldSizes[field_type]
- total_size = field_size * count
- is_local = total_size <= 4
- if not is_local:
- offset = self.readLong()
- offset += self.offsetOfNewPage
- self.rewriteLastLong(offset)
-
- if tag in self.Tags:
- cur_pos = self.f.tell()
-
- if is_local:
- self.fixOffsets(
- count, isShort=(field_size == 2), isLong=(field_size == 4)
- )
- self.f.seek(cur_pos + 4)
- else:
- self.f.seek(offset)
- self.fixOffsets(
- count, isShort=(field_size == 2), isLong=(field_size == 4)
- )
- self.f.seek(cur_pos)
-
- offset = cur_pos = None
-
- elif is_local:
- # skip the locally stored value that is not an offset
- self.f.seek(4, os.SEEK_CUR)
-
- def fixOffsets(self, count, isShort=False, isLong=False):
- if not isShort and not isLong:
- msg = "offset is neither short nor long"
- raise RuntimeError(msg)
-
- for i in range(count):
- offset = self.readShort() if isShort else self.readLong()
- offset += self.offsetOfNewPage
- if isShort and offset >= 65536:
- # offset is now too large - we must convert shorts to longs
- if count != 1:
- msg = "not implemented"
- raise RuntimeError(msg) # XXX TODO
-
- # simple case - the offset is just one and therefore it is
- # local (not referenced with another offset)
- self.rewriteLastShortToLong(offset)
- self.f.seek(-10, os.SEEK_CUR)
- self.writeShort(TiffTags.LONG) # rewrite the type to LONG
- self.f.seek(8, os.SEEK_CUR)
- elif isShort:
- self.rewriteLastShort(offset)
- else:
- self.rewriteLastLong(offset)
-
-
- def _save_all(im, fp, filename):
- encoderinfo = im.encoderinfo.copy()
- encoderconfig = im.encoderconfig
- append_images = list(encoderinfo.get("append_images", []))
- if not hasattr(im, "n_frames") and not append_images:
- return _save(im, fp, filename)
-
- cur_idx = im.tell()
- try:
- with AppendingTiffWriter(fp) as tf:
- for ims in [im] + append_images:
- ims.encoderinfo = encoderinfo
- ims.encoderconfig = encoderconfig
- if not hasattr(ims, "n_frames"):
- nfr = 1
- else:
- nfr = ims.n_frames
-
- for idx in range(nfr):
- ims.seek(idx)
- ims.load()
- _save(ims, tf, filename)
- tf.newFrame()
- finally:
- im.seek(cur_idx)
-
-
- #
- # --------------------------------------------------------------------
- # Register
-
- Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
- Image.register_save(TiffImageFile.format, _save)
- Image.register_save_all(TiffImageFile.format, _save_all)
-
- Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
-
- Image.register_mime(TiffImageFile.format, "image/tiff")
|